diff --git a/.idea/inspectionProfiles/ktlint.xml b/.idea/inspectionProfiles/ktlint.xml index 0938bfdc8..18d15ddc1 100644 --- a/.idea/inspectionProfiles/ktlint.xml +++ b/.idea/inspectionProfiles/ktlint.xml @@ -39,5 +39,6 @@ <option name="previewFile" value="true" /> </inspection_tool> <inspection_tool class="RedundantSemicolon" enabled="true" level="ERROR" enabled_by_default="true" /> + <inspection_tool class="SerializableCtor" enabled="true" level="WARNING" enabled_by_default="true" /> </profile> </component> \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 99bfa46a2..790598273 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -93,6 +93,12 @@ android { buildConfigField "String", "PERMISSION_LOCAL_BROADCAST", "\"${localBroadcastPermission}\"" } + testOptions { + unitTests.all { + useJUnitPlatform() + } + } + buildTypes { release { minifyEnabled false @@ -146,7 +152,7 @@ ext { coilKtVersion = "2.7.0" daggerVersion = "2.52" emojiVersion = "1.4.0" - fidoVersion = "4.1.0-patch2" + fidoVersion = "4.4.0" lifecycleVersion = '2.8.4' okhttpVersion = "4.12.0" markwonVersion = "4.6.2" @@ -157,6 +163,7 @@ ext { roomVersion = "2.6.1" workVersion = "2.9.1" espressoVersion = "3.6.1" + androidxTestVersion = "1.5.0" media3_version = "1.4.0" coroutines_version = "1.8.1" mockitoKotlinVersion = "5.4.0" @@ -170,10 +177,14 @@ configurations.configureEach { } dependencies { + spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.13.0' + spotbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.6.4' + implementation("androidx.compose.runtime:runtime:1.6.8") implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'androidx.datastore:datastore-core:1.1.1' implementation 'androidx.datastore:datastore-preferences:1.1.1' + implementation 'androidx.test.ext:junit-ktx:1.1.5' detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.6") implementation fileTree(include: ['*'], dir: 'libs') @@ -192,7 +203,6 @@ dependencies { implementation "androidx.work:work-runtime:${workVersion}" implementation "androidx.work:work-rxjava2:${workVersion}" implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' - androidTestImplementation "androidx.work:work-testing:${workVersion}" implementation 'com.google.android.flexbox:flexbox:3.0.0' implementation ('com.github.bitfireAT:dav4jvm:2.1.3', { exclude group: 'org.ogce', module: 'xpp3' // Android comes with its own XmlPullParser @@ -289,6 +299,12 @@ dependencies { }) implementation 'androidx.core:core-ktx:1.13.1' + implementation 'androidx.activity:activity-ktx:1.9.0' + implementation 'com.github.nextcloud.android-common:ui:0.21.0' + implementation 'com.github.nextcloud-deps:android-talk-webrtc:121.6167.0' + + gplayImplementation 'com.google.android.gms:play-services-base:18.4.0' + gplayImplementation "com.google.firebase:firebase-messaging:23.4.1" //compose implementation(platform("androidx.compose:compose-bom:2024.06.00")) @@ -305,11 +321,14 @@ dependencies { testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-core:5.12.0' - androidTestImplementation 'org.mockito:mockito-android:5.12.0' testImplementation 'androidx.arch.core:core-testing:2.2.0' - androidTestImplementation "androidx.test:core:1.6.1" + androidTestImplementation "androidx.test:core:1.5.0" + androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1" + androidTestImplementation 'androidx.test:core-ktx:1.6.1' + androidTestImplementation 'org.mockito:mockito-android:5.12.0' + androidTestImplementation "androidx.work:work-testing:${workVersion}" // Espresso core androidTestImplementation ("androidx.test.espresso:espresso-core:$espressoVersion", { exclude group: 'com.android.support', module: 'support-annotations' @@ -317,6 +336,9 @@ dependencies { androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion" androidTestImplementation "androidx.test.espresso:espresso-web:$espressoVersion" androidTestImplementation "androidx.test.espresso:espresso-accessibility:$espressoVersion" + + androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion" + androidTestImplementation('com.android.support.test.espresso:espresso-intents:3.0.2') spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.13.0' @@ -325,7 +347,7 @@ dependencies { gplayImplementation 'com.google.android.gms:play-services-base:18.5.0' gplayImplementation "com.google.firebase:firebase-messaging:24.0.0" - implementation 'androidx.activity:activity-ktx:1.9.1' + implementation 'androidx.activity:activity-ktx:1.9.1' implementation 'com.github.nextcloud.android-common:ui:0.23.0' diff --git a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/10.json b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/10.json index 08c6493c4..982fc60b2 100644 --- a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/10.json +++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/10.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 10, - "identityHash": "1b2dab0ea495c45c9c9ee6e64ba74039", + "identityHash": "93ef64fac7a9a811c4a3c2f5a6406f87", "entities": [ { "tableName": "User", @@ -135,12 +135,539 @@ }, "indices": [], "foreignKeys": [] + }, + { + "tableName": "Conversations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `name` TEXT, `displayName` TEXT, `description` TEXT, `type` TEXT, `lastPing` INTEGER NOT NULL, `participantType` TEXT, `hasPassword` INTEGER NOT NULL, `sessionId` TEXT, `actorId` TEXT, `actorType` TEXT, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, `unreadMention` INTEGER NOT NULL, `lastMessageJson` TEXT, `objectType` TEXT, `notificationLevel` TEXT, `readOnly` TEXT, `lobbyState` TEXT, `lobbyTimer` INTEGER, `lastReadMessage` INTEGER NOT NULL, `hasCall` INTEGER NOT NULL, `callFlag` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `canLeaveConversation` INTEGER, `canDeleteConversation` INTEGER, `unreadMentionDirect` INTEGER, `notificationCalls` INTEGER, `permissions` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `status` TEXT, `statusIcon` TEXT, `statusMessage` TEXT, `statusClearAt` INTEGER, `callRecording` INTEGER NOT NULL, `avatarVersion` TEXT, `isCustomAvatar` INTEGER, `callStartTime` INTEGER, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, 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": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastPing", + "columnName": "lastPing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "participantType", + "columnName": "participantType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hasPassword", + "columnName": "hasPassword", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "actorId", + "columnName": "actorId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "actorType", + "columnName": "actorType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "favorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastActivity", + "columnName": "lastActivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMessages", + "columnName": "unreadMessages", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMention", + "columnName": "unreadMention", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastMessageJson", + "columnName": "lastMessageJson", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "objectType", + "columnName": "objectType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notificationLevel", + "columnName": "notificationLevel", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "conversationReadOnlyState", + "columnName": "readOnly", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lobbyState", + "columnName": "lobbyState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lobbyTimer", + "columnName": "lobbyTimer", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastReadMessage", + "columnName": "lastReadMessage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasCall", + "columnName": "hasCall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "callFlag", + "columnName": "callFlag", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canStartCall", + "columnName": "canStartCall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canLeaveConversation", + "columnName": "canLeaveConversation", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "canDeleteConversation", + "columnName": "canDeleteConversation", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unreadMentionDirect", + "columnName": "unreadMentionDirect", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "notificationCalls", + "columnName": "notificationCalls", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageExpiration", + "columnName": "messageExpiration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "statusIcon", + "columnName": "statusIcon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "statusMessage", + "columnName": "statusMessage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "statusClearAt", + "columnName": "statusClearAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "callRecording", + "columnName": "callRecording", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "avatarVersion", + "columnName": "avatarVersion", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hasCustomAvatar", + "columnName": "isCustomAvatar", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "callStartTime", + "columnName": "callStartTime", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "recordingConsentRequired", + "columnName": "recordingConsent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteServer", + "columnName": "remoteServer", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remoteToken", + "columnName": "remoteToken", + "affinity": "TEXT", + "notNull": false + } + ], + "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, `token` TEXT, `id` INTEGER NOT NULL, `internalConversationId` TEXT, `actorType` TEXT, `actorId` TEXT, `actorDisplayName` TEXT, `timestamp` INTEGER NOT NULL, `systemMessage` TEXT, `messageType` TEXT, `isReplyable` INTEGER NOT NULL, `message` TEXT, `messageParameters` TEXT, `expirationTimestamp` INTEGER NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `markdown` INTEGER, `lastEditActorType` TEXT, `lastEditActorId` TEXT, `lastEditActorDisplayName` TEXT, `lastEditTimestamp` INTEGER, 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": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "internalConversationId", + "columnName": "internalConversationId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "actorType", + "columnName": "actorType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "actorId", + "columnName": "actorId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "actorDisplayName", + "columnName": "actorDisplayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "systemMessageType", + "columnName": "systemMessage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "messageType", + "columnName": "messageType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "replyable", + "columnName": "isReplyable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "messageParameters", + "columnName": "messageParameters", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "expirationTimestamp", + "columnName": "expirationTimestamp", + "affinity": "INTEGER", + "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": "renderMarkdown", + "columnName": "markdown", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastEditActorType", + "columnName": "lastEditActorType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastEditActorId", + "columnName": "lastEditActorId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastEditActorDisplayName", + "columnName": "lastEditActorDisplayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastEditTimestamp", + "columnName": "lastEditTimestamp", + "affinity": "INTEGER", + "notNull": false + } + ], + "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, `oldestMessageId` INTEGER NOT NULL, `newestMessageId` INTEGER NOT NULL, `hasHistory` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "internalConversationId", + "columnName": "internalConversationId", + "affinity": "TEXT", + "notNull": true + }, + { + "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": [], + "foreignKeys": [] } ], "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, '1b2dab0ea495c45c9c9ee6e64ba74039')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '93ef64fac7a9a811c4a3c2f5a6406f87')" ] } } \ No newline at end of file diff --git a/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatBlocksDaoTest.kt b/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatBlocksDaoTest.kt new file mode 100644 index 000000000..4ed2687a5 --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatBlocksDaoTest.kt @@ -0,0 +1,121 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.database.dao + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.runner.AndroidJUnit4 +import com.nextcloud.talk.data.database.model.ChatBlockEntity +import com.nextcloud.talk.data.source.local.TalkDatabase +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ChatBlocksDaoTest { + private lateinit var chatBlocksDao: ChatBlocksDao + private lateinit var db: TalkDatabase + private val tag = ChatBlocksDaoTest::class.java.simpleName + + @Before + fun createDb() { + val context = ApplicationProvider.getApplicationContext<Context>() + db = Room.inMemoryDatabaseBuilder( + context, + TalkDatabase::class.java + ).build() + chatBlocksDao = db.chatBlocksDao() + } + + @After + fun closeDb() = db.close() + + @Test + fun testGetConnectedChatBlocks() = + runTest { + + val searchedChatBlock = ChatBlockEntity( + internalConversationId = "1", + oldestMessageId = 50, + newestMessageId = 60, + hasHistory = true + ) + + val chatBlockTooOld = ChatBlockEntity( + internalConversationId = "1", + oldestMessageId = 10, + newestMessageId = 20, + hasHistory = true + ) + + val chatBlockOverlap1 = ChatBlockEntity( + internalConversationId = "1", + oldestMessageId = 45, + newestMessageId = 55, + hasHistory = true + ) + + val chatBlockWithin = ChatBlockEntity( + internalConversationId = "1", + oldestMessageId = 52, + newestMessageId = 58, + hasHistory = true + ) + + val chatBlockOverall = ChatBlockEntity( + internalConversationId = "1", + oldestMessageId = 1, + newestMessageId = 99, + hasHistory = true + ) + + val chatBlockOverlap2 = ChatBlockEntity( + internalConversationId = "1", + oldestMessageId = 59, + newestMessageId = 70, + hasHistory = true + ) + + val chatBlockTooNew = ChatBlockEntity( + internalConversationId = "1", + oldestMessageId = 80, + newestMessageId = 90, + hasHistory = true + ) + + val chatBlockWithinButOtherConversation = ChatBlockEntity( + internalConversationId = "2", + oldestMessageId = 53, + newestMessageId = 57, + hasHistory = true + ) + + chatBlocksDao.upsertChatBlock(searchedChatBlock) + + chatBlocksDao.upsertChatBlock(chatBlockTooOld) + chatBlocksDao.upsertChatBlock(chatBlockOverlap1) + chatBlocksDao.upsertChatBlock(chatBlockWithin) + chatBlocksDao.upsertChatBlock(chatBlockOverall) + chatBlocksDao.upsertChatBlock(chatBlockOverlap2) + chatBlocksDao.upsertChatBlock(chatBlockTooNew) + chatBlocksDao.upsertChatBlock(chatBlockWithinButOtherConversation) + + val results = chatBlocksDao.getConnectedChatBlocks( + "1", + searchedChatBlock.oldestMessageId, + searchedChatBlock.newestMessageId + ) + + assertEquals(5, results.first().size) + } +} diff --git a/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatMessagesDaoTest.kt b/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatMessagesDaoTest.kt new file mode 100644 index 000000000..58d2f8bab --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatMessagesDaoTest.kt @@ -0,0 +1,207 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.database.dao + +import android.content.Context +import android.util.Log +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.runner.AndroidJUnit4 +import com.nextcloud.talk.data.database.model.ChatMessageEntity +import com.nextcloud.talk.data.database.model.ConversationEntity +import com.nextcloud.talk.data.source.local.TalkDatabase +import com.nextcloud.talk.data.user.UsersDao +import com.nextcloud.talk.data.user.model.UserEntity +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ChatMessagesDaoTest { + + private lateinit var usersDao: UsersDao + private lateinit var conversationsDao: ConversationsDao + private lateinit var chatMessagesDao: ChatMessagesDao + private lateinit var db: TalkDatabase + private val tag = ChatMessagesDaoTest::class.java.simpleName + + var chatMessageCounter: Long = 1 + + @Before + fun createDb() { + val context = ApplicationProvider.getApplicationContext<Context>() + db = Room.inMemoryDatabaseBuilder( + context, + TalkDatabase::class.java + ).build() + usersDao = db.usersDao() + conversationsDao = db.conversationsDao() + chatMessagesDao = db.chatMessagesDao() + } + + @After + fun closeDb() = db.close() + + @Test + fun test() = + runTest { + usersDao.saveUser(createUserEntity("account1", "Account 1")) + usersDao.saveUser(createUserEntity("account2", "Account 2")) + + val account1 = usersDao.getUserWithUserId("account1").blockingGet() + val account2 = usersDao.getUserWithUserId("account2").blockingGet() + + // Problem: lets say we want to update the conv list -> We don#t know the primary keys! + // with account@token that would be easier! + conversationsDao.upsertConversations( + listOf( + createConversationEntity( + accountId = account1.id, + roomName = "Conversation One" + ), + createConversationEntity( + accountId = account1.id, + roomName = "Conversation Two" + ), + createConversationEntity( + accountId = account2.id, + roomName = "Conversation Three" + ) + ) + ) + + assertEquals(2, conversationsDao.getConversationsForUser(account1.id).first().size) + assertEquals(1, conversationsDao.getConversationsForUser(account2.id).first().size) + + // Lets imagine we are on conversations screen... + conversationsDao.getConversationsForUser(account1.id).first().forEach { + Log.d(tag, "- next Conversation for account1 -") + Log.d(tag, "internalId (PK): " + it.internalId) + Log.d(tag, "accountId: " + it.accountId) + Log.d(tag, "name: " + it.name) + Log.d(tag, "token: " + it.token) + } + + // User sees all conversations and clicks on a item. That's how we get a conversation + val conversation1 = conversationsDao.getConversationsForUser(account1.id).first()[0] + val conversation2 = conversationsDao.getConversationsForUser(account1.id).first()[1] + + // Having a conversation token, we can also get a conversation directly + val conversation1GotByToken = conversationsDao.getConversationForUser( + account1.id, + conversation1.token!! + ).first() + + assertEquals(conversation1, conversation1GotByToken) + + // Lets insert some messages to the conversations + chatMessagesDao.upsertChatMessages( + listOf( + createChatMessageEntity(conversation1.internalId, "hello"), + createChatMessageEntity(conversation1.internalId, "here"), + createChatMessageEntity(conversation1.internalId, "are"), + createChatMessageEntity(conversation1.internalId, "some"), + createChatMessageEntity(conversation1.internalId, "messages") + ) + ) + chatMessagesDao.upsertChatMessages( + listOf( + createChatMessageEntity(conversation2.internalId, "first message in conversation 2") + ) + ) + + chatMessagesDao.getMessagesForConversation(conversation1.internalId).first().forEach { + Log.d(tag, "- next Message for conversation1 (account1)-") + Log.d(tag, "id (PK): " + it.id) + Log.d(tag, "message: " + it.message) + } + + val chatMessagesConv1 = chatMessagesDao.getMessagesForConversation(conversation1.internalId) + assertEquals(5, chatMessagesConv1.first().size) + + val chatMessagesConv2 = chatMessagesDao.getMessagesForConversation(conversation2.internalId) + assertEquals(1, chatMessagesConv2.first().size) + + assertEquals("some", chatMessagesConv1.first()[1].message) + + val conv1chatMessage3 = chatMessagesDao.getChatMessageForConversation(conversation1.internalId, 3).first() + assertEquals("are", conv1chatMessage3.message) + + val chatMessagesConv1Since = + chatMessagesDao.getMessagesForConversationSince(conversation1.internalId, conv1chatMessage3.id) + assertEquals(3, chatMessagesConv1Since.first().size) + assertEquals("are", chatMessagesConv1Since.first()[0].message) + assertEquals("some", chatMessagesConv1Since.first()[1].message) + assertEquals("messages", chatMessagesConv1Since.first()[2].message) + + val chatMessagesConv1To = + chatMessagesDao.getMessagesForConversationBeforeAndEqual( + conversation1.internalId, + conv1chatMessage3.id, + 3 + ) + assertEquals(3, chatMessagesConv1To.first().size) + assertEquals("hello", chatMessagesConv1To.first()[2].message) + assertEquals("here", chatMessagesConv1To.first()[1].message) + assertEquals("are", chatMessagesConv1To.first()[0].message) + } + + private fun createUserEntity(userId: String, userName: String) = + UserEntity( + userId = userId, + username = userName, + baseUrl = null, + token = null, + displayName = null, + pushConfigurationState = null, + capabilities = null, + serverVersion = null, + clientCertificate = null, + externalSignalingServer = null, + current = java.lang.Boolean.FALSE, + scheduledForDeletion = java.lang.Boolean.FALSE + ) + + private fun createConversationEntity(accountId: Long, roomName: String): ConversationEntity { + val token = (0..10000000).random().toString() + + return ConversationEntity( + internalId = "$accountId@$token", + accountId = accountId, + token = token, + name = roomName + ) + } + + private fun createChatMessageEntity(internalConversationId: String, message: String): ChatMessageEntity { + val id = chatMessageCounter++ + + val emoji1 = "\uD83D\uDE00" // 😀 + val emoji2 = "\uD83D\uDE1C" // 😜 + val reactions = LinkedHashMap<String, Int>() + reactions[emoji1] = 3 + reactions[emoji2] = 4 + + val reactionsSelf = ArrayList<String>() + reactionsSelf.add(emoji1) + + val entity = ChatMessageEntity( + internalId = "$internalConversationId@$id", + internalConversationId = internalConversationId, + id = id, + message = message, + reactions = reactions, + reactionsSelf = reactionsSelf + ) + return entity + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.kt index bf1406846..728f79792 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.kt @@ -22,21 +22,21 @@ import androidx.core.content.res.ResourcesCompat import com.nextcloud.talk.R import com.nextcloud.talk.adapters.items.ConversationItem.ConversationItemViewHolder import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.chat.data.model.ChatMessage.MessageType +import com.nextcloud.talk.data.database.mappers.asModel import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.RvItemConversationWithLastMessageBinding import com.nextcloud.talk.extensions.loadConversationAvatar import com.nextcloud.talk.extensions.loadNoteToSelfAvatar import com.nextcloud.talk.extensions.loadSystemAvatar import com.nextcloud.talk.extensions.loadUserAvatar -import com.nextcloud.talk.models.json.chat.ChatMessage -import com.nextcloud.talk.models.json.conversations.Conversation -import com.nextcloud.talk.models.json.conversations.Conversation.ConversationType +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.conversations.ConversationEnums import com.nextcloud.talk.ui.StatusDrawable import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability -import com.nextcloud.talk.utils.SpreedFeatures import com.nextcloud.talk.utils.DisplayUtils - +import com.nextcloud.talk.utils.SpreedFeatures import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.davidea.flexibleadapter.items.IFilterable @@ -46,7 +46,7 @@ import eu.davidea.viewholders.FlexibleViewHolder import java.util.regex.Pattern class ConversationItem( - val model: Conversation, + val model: ConversationModel, private val user: User, private val context: Context, private val viewThemeUtils: ViewThemeUtils @@ -54,9 +54,10 @@ class ConversationItem( ISectionable<ConversationItemViewHolder, GenericTextHeaderItem?>, IFilterable<String?> { private var header: GenericTextHeaderItem? = null + private val chatMessage = model.lastMessageViaConversationList?.asModel() constructor( - conversation: Conversation, + conversation: ConversationModel, user: User, activityContext: Context, genericTextHeaderItem: GenericTextHeaderItem?, @@ -127,7 +128,7 @@ class ConversationItem( } else { holder.binding.favoriteConversationImageView.visibility = View.GONE } - if (ConversationType.ROOM_SYSTEM !== model.type) { + if (ConversationEnums.ConversationType.ROOM_SYSTEM !== model.type) { val size = DisplayUtils.convertDpToPixel(STATUS_SIZE_IN_DP, appContext) holder.binding.userStatusImage.visibility = View.VISIBLE holder.binding.userStatusImage.setImageDrawable( @@ -149,13 +150,13 @@ class ConversationItem( private fun showAvatar(holder: ConversationItemViewHolder) { holder.binding.dialogAvatar.visibility = View.VISIBLE var shouldLoadAvatar = shouldLoadAvatar(holder) - if (ConversationType.ROOM_SYSTEM == model.type) { + if (ConversationEnums.ConversationType.ROOM_SYSTEM == model.type) { holder.binding.dialogAvatar.loadSystemAvatar() shouldLoadAvatar = false } if (shouldLoadAvatar) { when (model.type) { - ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> { + ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> { if (!TextUtils.isEmpty(model.name)) { holder.binding.dialogAvatar.loadUserAvatar( user, @@ -168,11 +169,12 @@ class ConversationItem( } } - ConversationType.ROOM_GROUP_CALL, - ConversationType.FORMER_ONE_TO_ONE, - ConversationType.ROOM_PUBLIC_CALL -> + ConversationEnums.ConversationType.ROOM_GROUP_CALL, + ConversationEnums.ConversationType.FORMER_ONE_TO_ONE, + ConversationEnums.ConversationType.ROOM_PUBLIC_CALL -> holder.binding.dialogAvatar.loadConversationAvatar(user, model, false, viewThemeUtils) - ConversationType.NOTE_TO_SELF -> + + ConversationEnums.ConversationType.NOTE_TO_SELF -> holder.binding.dialogAvatar.loadNoteToSelfAvatar() else -> holder.binding.dialogAvatar.visibility = View.GONE @@ -182,7 +184,7 @@ class ConversationItem( private fun shouldLoadAvatar(holder: ConversationItemViewHolder): Boolean { return when (model.objectType) { - Conversation.ObjectType.SHARE_PASSWORD -> { + ConversationEnums.ObjectType.SHARE_PASSWORD -> { holder.binding.dialogAvatar.setImageDrawable( ContextCompat.getDrawable( context, @@ -192,7 +194,7 @@ class ConversationItem( false } - Conversation.ObjectType.FILE -> { + ConversationEnums.ObjectType.FILE -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { holder.binding.dialogAvatar.loadUserAvatar( viewThemeUtils.talk.themePlaceholderAvatar( @@ -213,7 +215,7 @@ class ConversationItem( } private fun setLastMessage(holder: ConversationItemViewHolder, appContext: Context) { - if (model.lastMessage != null) { + if (chatMessage != null) { holder.binding.dialogDate.visibility = View.VISIBLE holder.binding.dialogDate.text = DateUtils.getRelativeTimeSpanString( model.lastActivity * MILLIES, @@ -221,20 +223,20 @@ class ConversationItem( 0, DateUtils.FORMAT_ABBREV_RELATIVE ) - if (!TextUtils.isEmpty(model.lastMessage!!.systemMessage) || - ConversationType.ROOM_SYSTEM === model.type + if (!TextUtils.isEmpty(chatMessage?.systemMessage) || + ConversationEnums.ConversationType.ROOM_SYSTEM === model.type ) { - holder.binding.dialogLastMessage.text = model.lastMessage!!.text + holder.binding.dialogLastMessage.text = chatMessage?.text } else { - model.lastMessage!!.activeUser = user + chatMessage?.activeUser = user val text = if ( - model.lastMessage!!.getCalculateMessageType() === ChatMessage.MessageType.REGULAR_TEXT_MESSAGE + chatMessage?.messageType === MessageType.REGULAR_TEXT_MESSAGE.toString() ) { calculateRegularLastMessageText(appContext) } else { - model.lastMessage!!.lastMessageDisplayText + lastMessageDisplayText } holder.binding.dialogLastMessage.text = text } @@ -245,16 +247,16 @@ class ConversationItem( } private fun calculateRegularLastMessageText(appContext: Context): String { - return if (model.lastMessage!!.actorId == user.userId) { + return if (chatMessage?.actorId == user.userId) { String.format( appContext.getString(R.string.nc_formatted_message_you), - model.lastMessage!!.lastMessageDisplayText + lastMessageDisplayText ) } else { val authorDisplayName = - if (!TextUtils.isEmpty(model.lastMessage!!.actorDisplayName)) { - model.lastMessage!!.actorDisplayName - } else if ("guests" == model.lastMessage!!.actorType) { + if (!TextUtils.isEmpty(chatMessage?.actorDisplayName)) { + chatMessage?.actorDisplayName + } else if ("guests" == chatMessage?.actorType) { appContext.getString(R.string.nc_guest) } else { "" @@ -262,7 +264,7 @@ class ConversationItem( String.format( appContext.getString(R.string.nc_formatted_message), authorDisplayName, - model.lastMessage!!.lastMessageDisplayText + lastMessageDisplayText ) } } @@ -286,7 +288,7 @@ class ConversationItem( context, R.color.conversation_unread_bubble_text ) - if (model.type === ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) { + if (model.type === ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) { viewThemeUtils.material.colorChipBackground(holder.binding.dialogUnreadBubble) } else if (model.unreadMention) { if (hasSpreedFeatureCapability(user.capabilities?.spreedCapability!!, SpreedFeatures.DIRECT_MENTION_FLAG)) { @@ -323,6 +325,94 @@ class ConversationItem( this.header = header } + private val lastMessageDisplayText: String + get() { + if (chatMessage?.getCalculateMessageType() == MessageType.REGULAR_TEXT_MESSAGE || + chatMessage?.getCalculateMessageType() == MessageType.SYSTEM_MESSAGE || + chatMessage?.getCalculateMessageType() == MessageType.SINGLE_LINK_MESSAGE + ) { + return chatMessage.text + } else { + if (MessageType.SINGLE_LINK_GIPHY_MESSAGE == chatMessage?.getCalculateMessageType() || + MessageType.SINGLE_LINK_TENOR_MESSAGE == chatMessage?.getCalculateMessageType() || + MessageType.SINGLE_LINK_GIF_MESSAGE == chatMessage?.getCalculateMessageType() + ) { + return if (chatMessage?.actorId == chatMessage?.activeUser!!.userId) { + sharedApplication!!.getString(R.string.nc_sent_a_gif_you) + } else { + String.format( + sharedApplication!!.resources.getString(R.string.nc_sent_a_gif), + chatMessage?.getNullsafeActorDisplayName() + ) + } + } else if (MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == chatMessage?.getCalculateMessageType()) { + return if (chatMessage?.actorId == chatMessage?.activeUser!!.userId) { + sharedApplication!!.getString(R.string.nc_sent_an_attachment_you) + } else { + String.format( + sharedApplication!!.resources.getString(R.string.nc_sent_an_attachment), + chatMessage?.getNullsafeActorDisplayName() + ) + } + } else if (MessageType.SINGLE_NC_GEOLOCATION_MESSAGE == chatMessage?.getCalculateMessageType()) { + return if (chatMessage?.actorId == chatMessage?.activeUser!!.userId) { + sharedApplication!!.getString(R.string.nc_sent_location_you) + } else { + String.format( + sharedApplication!!.resources.getString(R.string.nc_sent_location), + chatMessage?.getNullsafeActorDisplayName() + ) + } + } else if (MessageType.VOICE_MESSAGE == chatMessage?.getCalculateMessageType()) { + return if (chatMessage?.actorId == chatMessage?.activeUser!!.userId) { + sharedApplication!!.getString(R.string.nc_sent_voice_you) + } else { + String.format( + sharedApplication!!.resources.getString(R.string.nc_sent_voice), + chatMessage?.getNullsafeActorDisplayName() + ) + } + } else if (MessageType.SINGLE_LINK_AUDIO_MESSAGE == chatMessage?.getCalculateMessageType()) { + return if (chatMessage?.actorId == chatMessage?.activeUser!!.userId) { + sharedApplication!!.getString(R.string.nc_sent_an_audio_you) + } else { + String.format( + sharedApplication!!.resources.getString(R.string.nc_sent_an_audio), + chatMessage?.getNullsafeActorDisplayName() + ) + } + } else if (MessageType.SINGLE_LINK_VIDEO_MESSAGE == chatMessage?.getCalculateMessageType()) { + return if (chatMessage?.actorId == chatMessage?.activeUser!!.userId) { + sharedApplication!!.getString(R.string.nc_sent_a_video_you) + } else { + String.format( + sharedApplication!!.resources.getString(R.string.nc_sent_a_video), + chatMessage?.getNullsafeActorDisplayName() + ) + } + } else if (MessageType.SINGLE_LINK_IMAGE_MESSAGE == chatMessage?.getCalculateMessageType()) { + return if (chatMessage?.actorId == chatMessage?.activeUser!!.userId) { + sharedApplication!!.getString(R.string.nc_sent_an_image_you) + } else { + String.format( + sharedApplication!!.resources.getString(R.string.nc_sent_an_image), + chatMessage?.getNullsafeActorDisplayName() + ) + } + } else if (MessageType.POLL_MESSAGE == chatMessage?.getCalculateMessageType()) { + return if (chatMessage?.actorId == chatMessage?.activeUser!!.userId) { + sharedApplication!!.getString(R.string.nc_sent_poll_you) + } else { + String.format( + sharedApplication!!.resources.getString(R.string.nc_sent_poll), + chatMessage?.getNullsafeActorDisplayName() + ) + } + } + } + return "" + } + class ConversationItemViewHolder(view: View?, adapter: FlexibleAdapter<*>?) : FlexibleViewHolder(view, adapter) { var binding: RvItemConversationWithLastMessageBinding diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/CallStartedViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/CallStartedViewHolder.kt index 446456ac8..c726c64e9 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/CallStartedViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/CallStartedViewHolder.kt @@ -16,7 +16,7 @@ import coil.target.Target import coil.transform.CircleCropTransformation import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.databinding.CallStartedMessageBinding -import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.utils.ApiUtils diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/CommonMessageInterface.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/CommonMessageInterface.kt index bad6960d3..262ca69a8 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/CommonMessageInterface.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/CommonMessageInterface.kt @@ -6,7 +6,7 @@ */ package com.nextcloud.talk.adapters.messages -import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.chat.data.model.ChatMessage interface CommonMessageInterface { fun onLongClickReactions(chatMessage: ChatMessage) diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt index 9f2117c91..e7a287f05 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt @@ -10,26 +10,35 @@ package com.nextcloud.talk.adapters.messages import android.annotation.SuppressLint import android.content.Context import android.text.TextUtils +import android.util.Log import android.view.View import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope import autodagger.AutoInjector import coil.load import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.talk.R +import com.nextcloud.talk.adapters.messages.OutcomingPollMessageViewHolder.Companion import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.databinding.ItemCustomIncomingLinkPreviewMessageBinding import com.nextcloud.talk.extensions.loadBotsAvatar import com.nextcloud.talk.extensions.loadChangelogBotAvatar import com.nextcloud.talk.extensions.loadFederatedUserAvatar -import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.DateUtils import com.nextcloud.talk.utils.message.MessageUtils import com.nextcloud.talk.utils.preferences.AppPreferences import com.stfalcon.chatkit.messages.MessageHolders +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) @@ -168,40 +177,62 @@ class IncomingLinkPreviewMessageViewHolder(incomingView: View, payload: Any) : } private fun setParentMessageDataOnMessageItem(message: ChatMessage) { - if (!message.isDeleted && message.parentMessage != null) { - val parentChatMessage = message.parentMessage - parentChatMessage!!.activeUser = message.activeUser - parentChatMessage.imageUrl?.let { - binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE - binding.messageQuote.quotedMessageImage.load(it) { - addHeader( - "Authorization", - ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + if (message.parentMessageId != null && !message.isDeleted) { + CoroutineScope(Dispatchers.Main).launch { + try { + val chatActivity = commonMessageInterface as ChatActivity + val urlForChatting = ApiUtils.getUrlForChat( + chatActivity.chatApiVersion, + chatActivity.conversationUser?.baseUrl, + chatActivity.roomToken ) + + val parentChatMessage = withContext(Dispatchers.IO) { + chatActivity.chatViewModel.getMessageById( + urlForChatting, + chatActivity.currentConversation!!, + message.parentMessageId!! + ).first() + } + parentChatMessage.activeUser = message.activeUser + parentChatMessage.imageUrl?.let { + binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE + binding.messageQuote.quotedMessageImage.load(it) { + addHeader( + "Authorization", + ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + ) + } + } ?: run { + binding.messageQuote.quotedMessageImage.visibility = View.GONE + } + binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName + ?: context.getText(R.string.nc_nick_guest) + binding.messageQuote.quotedMessage.text = messageUtils + .enrichChatReplyMessageText( + binding.messageQuote.quotedMessage.context, + parentChatMessage, + true, + viewThemeUtils + ) + + binding.messageQuote.quotedMessageAuthor + .setTextColor(ContextCompat.getColor(context, R.color.textColorMaxContrast)) + + if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) { + viewThemeUtils.platform.colorViewBackground( + binding.messageQuote.quoteColoredView, + ColorRole.PRIMARY + ) + } else { + binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast) + } + + binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE + } catch (e: Exception) { + Log.d(TAG, "Error when processing parent message in view holder", e) } - } ?: run { - binding.messageQuote.quotedMessageImage.visibility = View.GONE } - binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName - ?: context.getText(R.string.nc_nick_guest) - binding.messageQuote.quotedMessage.text = messageUtils - .enrichChatReplyMessageText( - binding.messageQuote.quotedMessage.context, - parentChatMessage, - true, - viewThemeUtils - ) - - binding.messageQuote.quotedMessageAuthor - .setTextColor(ContextCompat.getColor(context, R.color.textColorMaxContrast)) - - if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) { - viewThemeUtils.platform.colorViewBackground(binding.messageQuote.quoteColoredView, ColorRole.PRIMARY) - } else { - binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast) - } - - binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE } else { binding.messageQuote.quotedChatMessageView.visibility = View.GONE } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt index f6d13bd02..f2ff49947 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt @@ -20,18 +20,21 @@ import android.view.MotionEvent import android.view.View import android.webkit.WebView import android.webkit.WebViewClient +import androidx.lifecycle.lifecycleScope import autodagger.AutoInjector import coil.load import com.google.android.material.snackbar.Snackbar import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.talk.R +import com.nextcloud.talk.adapters.messages.IncomingPollMessageViewHolder.Companion import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.databinding.ItemCustomIncomingLocationMessageBinding import com.nextcloud.talk.extensions.loadBotsAvatar import com.nextcloud.talk.extensions.loadChangelogBotAvatar import com.nextcloud.talk.extensions.loadFederatedUserAvatar -import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.DateUtils @@ -39,6 +42,11 @@ import com.nextcloud.talk.utils.UriUtils import com.nextcloud.talk.utils.message.MessageUtils import com.nextcloud.talk.utils.preferences.AppPreferences import com.stfalcon.chatkit.messages.MessageHolders +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.net.URLEncoder import javax.inject.Inject @@ -150,40 +158,62 @@ class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) : } private fun setParentMessageDataOnMessageItem(message: ChatMessage) { - if (!message.isDeleted && message.parentMessage != null) { - val parentChatMessage = message.parentMessage - parentChatMessage!!.activeUser = message.activeUser - parentChatMessage.imageUrl?.let { - binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE - binding.messageQuote.quotedMessageImage.load(it) { - addHeader( - "Authorization", - ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + if (message.parentMessageId != null && !message.isDeleted) { + CoroutineScope(Dispatchers.Main).launch { + try { + val chatActivity = commonMessageInterface as ChatActivity + val urlForChatting = ApiUtils.getUrlForChat( + chatActivity.chatApiVersion, + chatActivity.conversationUser?.baseUrl, + chatActivity.roomToken ) + + val parentChatMessage = withContext(Dispatchers.IO) { + chatActivity.chatViewModel.getMessageById( + urlForChatting, + chatActivity.currentConversation!!, + message.parentMessageId!! + ).first() + } + parentChatMessage.activeUser = message.activeUser + parentChatMessage.imageUrl?.let { + binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE + binding.messageQuote.quotedMessageImage.load(it) { + addHeader( + "Authorization", + ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + ) + } + } ?: run { + binding.messageQuote.quotedMessageImage.visibility = View.GONE + } + binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName + ?: context.getText(R.string.nc_nick_guest) + binding.messageQuote.quotedMessage.text = messageUtils + .enrichChatReplyMessageText( + binding.messageQuote.quotedMessage.context, + parentChatMessage, + true, + viewThemeUtils + ) + + binding.messageQuote.quotedMessageAuthor + .setTextColor(context.resources.getColor(R.color.textColorMaxContrast, null)) + + if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) { + viewThemeUtils.platform.colorViewBackground( + binding.messageQuote.quoteColoredView, + ColorRole.PRIMARY + ) + } else { + binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast) + } + + binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE + } catch (e: Exception) { + Log.d(TAG, "Error when processing parent message in view holder", e) } - } ?: run { - binding.messageQuote.quotedMessageImage.visibility = View.GONE } - binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName - ?: context.getText(R.string.nc_nick_guest) - binding.messageQuote.quotedMessage.text = messageUtils - .enrichChatReplyMessageText( - binding.messageQuote.quotedMessage.context, - parentChatMessage, - true, - viewThemeUtils - ) - - binding.messageQuote.quotedMessageAuthor - .setTextColor(context.resources.getColor(R.color.textColorMaxContrast, null)) - - if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) { - viewThemeUtils.platform.colorViewBackground(binding.messageQuote.quoteColoredView, ColorRole.PRIMARY) - } else { - binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast) - } - - binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE } else { binding.messageQuote.quotedChatMessageView.visibility = View.GONE } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt index 18cbb2c45..3e35fe070 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt @@ -9,12 +9,15 @@ package com.nextcloud.talk.adapters.messages import android.annotation.SuppressLint import android.content.Context import android.text.TextUtils +import android.util.Log import android.view.View import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope import autodagger.AutoInjector import coil.load import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.talk.R +import com.nextcloud.talk.adapters.messages.IncomingTextMessageViewHolder.Companion import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication @@ -23,7 +26,7 @@ import com.nextcloud.talk.databinding.ItemCustomIncomingPollMessageBinding import com.nextcloud.talk.extensions.loadBotsAvatar import com.nextcloud.talk.extensions.loadChangelogBotAvatar import com.nextcloud.talk.extensions.loadFederatedUserAvatar -import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.polls.ui.PollMainDialogFragment import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.ApiUtils @@ -31,6 +34,11 @@ import com.nextcloud.talk.utils.DateUtils import com.nextcloud.talk.utils.message.MessageUtils import com.nextcloud.talk.utils.preferences.AppPreferences import com.stfalcon.chatkit.messages.MessageHolders +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) @@ -176,40 +184,61 @@ class IncomingPollMessageViewHolder(incomingView: View, payload: Any) : } private fun setParentMessageDataOnMessageItem(message: ChatMessage) { - if (!message.isDeleted && message.parentMessage != null) { - val parentChatMessage = message.parentMessage - parentChatMessage!!.activeUser = message.activeUser - parentChatMessage.imageUrl?.let { - binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE - binding.messageQuote.quotedMessageImage.load(it) { - addHeader( - "Authorization", - ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + if (message.parentMessageId != null && !message.isDeleted) { + CoroutineScope(Dispatchers.Main).launch { + try { + val chatActivity = commonMessageInterface as ChatActivity + val urlForChatting = ApiUtils.getUrlForChat( + chatActivity.chatApiVersion, + chatActivity.conversationUser?.baseUrl, + chatActivity.roomToken ) + + val parentChatMessage = withContext(Dispatchers.IO) { + chatActivity.chatViewModel.getMessageById( + urlForChatting, + chatActivity.currentConversation!!, + message.parentMessageId!! + ).first() + } + parentChatMessage.activeUser = message.activeUser + parentChatMessage.imageUrl?.let { + binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE + binding.messageQuote.quotedMessageImage.load(it) { + addHeader( + "Authorization", + ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + ) + } + } ?: run { + binding.messageQuote.quotedMessageImage.visibility = View.GONE + } + binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName + ?: context.getText(R.string.nc_nick_guest) + binding.messageQuote.quotedMessage.text = messageUtils + .enrichChatReplyMessageText( + binding.messageQuote.quotedMessage.context, + parentChatMessage, + true, + viewThemeUtils + ) + + binding.messageQuote.quotedMessageAuthor + .setTextColor(ContextCompat.getColor(context, R.color.textColorMaxContrast)) + + if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) { + viewThemeUtils.platform.colorViewBackground( + binding.messageQuote.quoteColoredView, + ColorRole.PRIMARY + ) + } else { + binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast) + } + binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE + } catch (e: Exception) { + Log.d(TAG, "Error when processing parent message in view holder", e) } - } ?: run { - binding.messageQuote.quotedMessageImage.visibility = View.GONE } - binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName - ?: context.getText(R.string.nc_nick_guest) - binding.messageQuote.quotedMessage.text = messageUtils - .enrichChatReplyMessageText( - binding.messageQuote.quotedMessage.context, - parentChatMessage, - true, - viewThemeUtils - ) - - binding.messageQuote.quotedMessageAuthor - .setTextColor(ContextCompat.getColor(context, R.color.textColorMaxContrast)) - - if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) { - viewThemeUtils.platform.colorViewBackground(binding.messageQuote.quoteColoredView, ColorRole.PRIMARY) - } else { - binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast) - } - - binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE } else { binding.messageQuote.quotedChatMessageView.visibility = View.GONE } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java index c73826c4f..fb1f6f85d 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java @@ -18,7 +18,7 @@ import com.google.android.material.card.MaterialCardView; import com.nextcloud.talk.R; import com.nextcloud.talk.databinding.ItemCustomIncomingPreviewMessageBinding; import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding; -import com.nextcloud.talk.models.json.chat.ChatMessage; +import com.nextcloud.talk.chat.data.model.ChatMessage; import com.nextcloud.talk.utils.TextMatchers; import java.util.HashMap; diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt index 52e4f2bbc..e44901a81 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt @@ -11,9 +11,11 @@ package com.nextcloud.talk.adapters.messages import android.content.Context import android.text.TextUtils +import android.util.Log import android.util.TypedValue import android.view.View import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope import autodagger.AutoInjector import coil.load import com.nextcloud.android.common.ui.theme.utils.ColorRole @@ -25,7 +27,7 @@ import com.nextcloud.talk.databinding.ItemCustomIncomingTextMessageBinding import com.nextcloud.talk.extensions.loadBotsAvatar import com.nextcloud.talk.extensions.loadChangelogBotAvatar import com.nextcloud.talk.extensions.loadFederatedUserAvatar -import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.DateUtils @@ -33,6 +35,13 @@ import com.nextcloud.talk.utils.TextMatchers import com.nextcloud.talk.utils.message.MessageUtils import com.nextcloud.talk.utils.preferences.AppPreferences import com.stfalcon.chatkit.messages.MessageHolders +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) @@ -99,14 +108,14 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) : if (message.lastEditTimestamp != 0L && !message.isDeleted) { binding.messageEditIndicator.visibility = View.VISIBLE - binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.lastEditTimestamp) + binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.lastEditTimestamp!!) } else { binding.messageEditIndicator.visibility = View.GONE binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) } // parent message handling - if (!message.isDeleted && message.parentMessage != null) { + if (!message.isDeleted && message.parentMessageId != null) { processParentMessage(message) binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE } else { @@ -176,44 +185,73 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) : } private fun processParentMessage(message: ChatMessage) { - val parentChatMessage = message.parentMessage - parentChatMessage!!.activeUser = message.activeUser - parentChatMessage.imageUrl?.let { - binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE - binding.messageQuote.quotedMessageImage.load(it) { - addHeader( - "Authorization", - ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! - ) + if (message.parentMessageId != null && !message.isDeleted) { + CoroutineScope(Dispatchers.Main).launch { + try { + val chatActivity = commonMessageInterface as ChatActivity + val urlForChatting = ApiUtils.getUrlForChat( + chatActivity.chatApiVersion, + chatActivity.conversationUser?.baseUrl, + chatActivity.roomToken + ) + + val parentChatMessage = withContext(Dispatchers.IO) { + chatActivity.chatViewModel.getMessageById( + urlForChatting, + chatActivity.currentConversation!!, + message.parentMessageId!! + ).first() + } + + parentChatMessage.activeUser = message.activeUser + parentChatMessage.imageUrl?.let { + binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE + binding.messageQuote.quotedMessageImage.load(it) { + addHeader( + "Authorization", + ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + ) + } + } ?: run { + binding.messageQuote.quotedMessageImage.visibility = View.GONE + } + binding.messageQuote.quotedMessageAuthor.text = + if (parentChatMessage.actorDisplayName.isNullOrEmpty()) { + context.getText(R.string.nc_nick_guest) + } else { + parentChatMessage.actorDisplayName + } + + binding.messageQuote.quotedMessage.text = messageUtils + .enrichChatReplyMessageText( + binding.messageQuote.quotedMessage.context, + parentChatMessage, + true, + viewThemeUtils + ) + + if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) { + viewThemeUtils.platform.colorViewBackground( + binding.messageQuote.quoteColoredView, + ColorRole.PRIMARY + ) + } else { + binding.messageQuote.quoteColoredView.setBackgroundColor( + ContextCompat.getColor( + binding.messageQuote.quoteColoredView.context, + R.color.high_emphasis_text + ) + ) + } + + binding.messageQuote.quotedChatMessageView.setOnClickListener { + val chatActivity = commonMessageInterface as ChatActivity + chatActivity.jumpToQuotedMessage(parentChatMessage) + } + } catch (e: Exception) { + Log.d(TAG, "Error when processing parent message in view holder", e) + } } - } ?: run { - binding.messageQuote.quotedMessageImage.visibility = View.GONE - } - binding.messageQuote.quotedMessageAuthor.text = if (parentChatMessage.actorDisplayName.isNullOrEmpty()) { - context.getText(R.string.nc_nick_guest) - } else { - parentChatMessage.actorDisplayName - } - - binding.messageQuote.quotedMessage.text = messageUtils - .enrichChatReplyMessageText( - binding.messageQuote.quotedMessage.context, - parentChatMessage, - true, - viewThemeUtils - ) - - if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) { - viewThemeUtils.platform.colorViewBackground(binding.messageQuote.quoteColoredView, ColorRole.PRIMARY) - } else { - binding.messageQuote.quoteColoredView.setBackgroundColor( - ContextCompat.getColor(binding.messageQuote.quoteColoredView.context, R.color.high_emphasis_text) - ) - } - - binding.messageQuote.quotedChatMessageView.setOnClickListener { - val chatActivity = commonMessageInterface as ChatActivity - chatActivity.jumpToQuotedMessage(parentChatMessage) } } @@ -234,5 +272,6 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) : companion object { const val TEXT_SIZE_MULTIPLIER = 2.5 + private val TAG = IncomingTextMessageViewHolder::class.java.simpleName } } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt index 8c2f321b0..0ba927370 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt @@ -24,17 +24,23 @@ import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.talk.R 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.databinding.ItemCustomIncomingVoiceMessageBinding import com.nextcloud.talk.extensions.loadBotsAvatar import com.nextcloud.talk.extensions.loadChangelogBotAvatar import com.nextcloud.talk.extensions.loadFederatedUserAvatar -import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.DateUtils import com.nextcloud.talk.utils.message.MessageUtils import com.nextcloud.talk.utils.preferences.AppPreferences import com.stfalcon.chatkit.messages.MessageHolders +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.util.concurrent.ExecutionException import javax.inject.Inject @@ -203,14 +209,17 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : Log.d(TAG, "WorkInfo.State.RUNNING in ViewHolder") showVoiceMessageLoading() } + WorkInfo.State.SUCCEEDED -> { Log.d(TAG, "WorkInfo.State.SUCCEEDED in ViewHolder") showPlayButton() } + WorkInfo.State.FAILED -> { Log.d(TAG, "WorkInfo.State.FAILED in ViewHolder") showPlayButton() } + else -> { } } @@ -269,40 +278,62 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : } private fun setParentMessageDataOnMessageItem(message: ChatMessage) { - if (!message.isDeleted && message.parentMessage != null) { - val parentChatMessage = message.parentMessage - parentChatMessage!!.activeUser = message.activeUser - parentChatMessage.imageUrl?.let { - binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE - binding.messageQuote.quotedMessageImage.load(it) { - addHeader( - "Authorization", - ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + if (message.parentMessageId != null && !message.isDeleted) { + CoroutineScope(Dispatchers.Main).launch { + try { + val chatActivity = commonMessageInterface as ChatActivity + val urlForChatting = ApiUtils.getUrlForChat( + chatActivity.chatApiVersion, + chatActivity.conversationUser?.baseUrl, + chatActivity.roomToken ) + + val parentChatMessage = withContext(Dispatchers.IO) { + chatActivity.chatViewModel.getMessageById( + urlForChatting, + chatActivity.currentConversation!!, + message.parentMessageId!! + ).first() + } + parentChatMessage.activeUser = message.activeUser + parentChatMessage.imageUrl?.let { + binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE + binding.messageQuote.quotedMessageImage.load(it) { + addHeader( + "Authorization", + ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + ) + } + } ?: run { + binding.messageQuote.quotedMessageImage.visibility = View.GONE + } + binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName + ?: context!!.getText(R.string.nc_nick_guest) + binding.messageQuote.quotedMessage.text = messageUtils + .enrichChatReplyMessageText( + binding.messageQuote.quotedMessage.context, + parentChatMessage, + true, + viewThemeUtils + ) + + binding.messageQuote.quotedMessageAuthor + .setTextColor(ContextCompat.getColor(context!!, R.color.textColorMaxContrast)) + + if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) { + viewThemeUtils.platform.colorViewBackground( + binding.messageQuote.quoteColoredView, + ColorRole.PRIMARY + ) + } else { + binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast) + } + + binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE + } catch (e: Exception) { + Log.d(TAG, "Error when processing parent message in view holder", e) } - } ?: run { - binding.messageQuote.quotedMessageImage.visibility = View.GONE } - binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName - ?: context!!.getText(R.string.nc_nick_guest) - binding.messageQuote.quotedMessage.text = messageUtils - .enrichChatReplyMessageText( - binding.messageQuote.quotedMessage.context, - parentChatMessage, - true, - viewThemeUtils - ) - - binding.messageQuote.quotedMessageAuthor - .setTextColor(ContextCompat.getColor(context!!, R.color.textColorMaxContrast)) - - if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) { - viewThemeUtils.platform.colorViewBackground(binding.messageQuote.quoteColoredView, ColorRole.PRIMARY) - } else { - binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast) - } - - binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE } else { binding.messageQuote.quotedChatMessageView.visibility = View.GONE } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/LinkPreview.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/LinkPreview.kt index 4c9f72927..b9e791219 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/LinkPreview.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/LinkPreview.kt @@ -14,7 +14,7 @@ import android.view.View import coil.load import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.databinding.ReferenceInsideMessageBinding -import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.models.json.opengraph.OpenGraphOverall import com.nextcloud.talk.utils.ApiUtils import io.reactivex.Observer diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLinkPreviewMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLinkPreviewMessageViewHolder.kt index 567f3936c..94386c11a 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLinkPreviewMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLinkPreviewMessageViewHolder.kt @@ -9,17 +9,21 @@ package com.nextcloud.talk.adapters.messages import android.annotation.SuppressLint import android.content.Context +import android.util.Log import android.view.View import androidx.appcompat.content.res.AppCompatResources +import androidx.lifecycle.lifecycleScope import autodagger.AutoInjector import coil.load import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.talk.R +import com.nextcloud.talk.adapters.messages.OutcomingPollMessageViewHolder.Companion import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.databinding.ItemCustomOutcomingLinkPreviewMessageBinding -import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.models.json.chat.ReadStatus import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.ApiUtils @@ -27,6 +31,11 @@ import com.nextcloud.talk.utils.DateUtils import com.nextcloud.talk.utils.message.MessageUtils import com.nextcloud.talk.utils.preferences.AppPreferences import com.stfalcon.chatkit.messages.MessageHolders +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) @@ -138,34 +147,53 @@ class OutcomingLinkPreviewMessageViewHolder(outcomingView: View, payload: Any) : } private fun setParentMessageDataOnMessageItem(message: ChatMessage) { - if (!message.isDeleted && message.parentMessage != null) { - val parentChatMessage = message.parentMessage - parentChatMessage!!.activeUser = message.activeUser - parentChatMessage.imageUrl?.let { - binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE - binding.messageQuote.quotedMessageImage.load(it) { - addHeader( - "Authorization", - ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + if (message.parentMessageId != null && !message.isDeleted) { + CoroutineScope(Dispatchers.Main).launch { + try { + val chatActivity = commonMessageInterface as ChatActivity + val urlForChatting = ApiUtils.getUrlForChat( + chatActivity.chatApiVersion, + chatActivity.conversationUser?.baseUrl, + chatActivity.roomToken ) - } - } ?: run { - binding.messageQuote.quotedMessageImage.visibility = View.GONE - } - binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName - ?: context.getText(R.string.nc_nick_guest) - binding.messageQuote.quotedMessage.text = messageUtils - .enrichChatReplyMessageText( - binding.messageQuote.quotedMessage.context, - parentChatMessage, - false, - viewThemeUtils - ) - viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage) - viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor) - viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView) - binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE + val parentChatMessage = withContext(Dispatchers.IO) { + chatActivity.chatViewModel.getMessageById( + urlForChatting, + chatActivity.currentConversation!!, + message.parentMessageId!! + ).first() + } + parentChatMessage.activeUser = message.activeUser + parentChatMessage.imageUrl?.let { + binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE + binding.messageQuote.quotedMessageImage.load(it) { + addHeader( + "Authorization", + ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + ) + } + } ?: run { + binding.messageQuote.quotedMessageImage.visibility = View.GONE + } + binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName + ?: context.getText(R.string.nc_nick_guest) + binding.messageQuote.quotedMessage.text = messageUtils + .enrichChatReplyMessageText( + binding.messageQuote.quotedMessage.context, + parentChatMessage, + false, + viewThemeUtils + ) + viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage) + viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor) + viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView) + + binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE + } catch (e: Exception) { + Log.d(TAG, "Error when processing parent message in view holder", e) + } + } } else { binding.messageQuote.quotedChatMessageView.visibility = View.GONE } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt index 2a50ecb02..f9208eb2e 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt @@ -18,16 +18,19 @@ import android.view.View import android.webkit.WebView import android.webkit.WebViewClient import androidx.appcompat.content.res.AppCompatResources +import androidx.lifecycle.lifecycleScope import autodagger.AutoInjector import coil.load import com.google.android.flexbox.FlexboxLayout import com.google.android.material.snackbar.Snackbar import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.talk.R +import com.nextcloud.talk.adapters.messages.IncomingPollMessageViewHolder.Companion import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.databinding.ItemCustomOutcomingLocationMessageBinding -import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.models.json.chat.ReadStatus import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.ApiUtils @@ -35,6 +38,11 @@ import com.nextcloud.talk.utils.DateUtils import com.nextcloud.talk.utils.UriUtils import com.nextcloud.talk.utils.message.MessageUtils import com.stfalcon.chatkit.messages.MessageHolders +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.net.URLEncoder import javax.inject.Inject import kotlin.math.roundToInt @@ -190,34 +198,53 @@ class OutcomingLocationMessageViewHolder(incomingView: View) : } private fun setParentMessageDataOnMessageItem(message: ChatMessage) { - if (!message.isDeleted && message.parentMessage != null) { - val parentChatMessage = message.parentMessage - parentChatMessage!!.activeUser = message.activeUser - parentChatMessage.imageUrl?.let { - binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE - binding.messageQuote.quotedMessageImage.load(it) { - addHeader( - "Authorization", - ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + if (message.parentMessageId != null && !message.isDeleted) { + CoroutineScope(Dispatchers.Main).launch { + try { + val chatActivity = commonMessageInterface as ChatActivity + val urlForChatting = ApiUtils.getUrlForChat( + chatActivity.chatApiVersion, + chatActivity.conversationUser?.baseUrl, + chatActivity.roomToken ) - } - } ?: run { - binding.messageQuote.quotedMessageImage.visibility = View.GONE - } - binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName - ?: context.getText(R.string.nc_nick_guest) - binding.messageQuote.quotedMessage.text = messageUtils - .enrichChatReplyMessageText( - binding.messageQuote.quotedMessage.context, - parentChatMessage, - false, - viewThemeUtils - ) - viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage) - viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor) - viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView) - binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE + val parentChatMessage = withContext(Dispatchers.IO) { + chatActivity.chatViewModel.getMessageById( + urlForChatting, + chatActivity.currentConversation!!, + message.parentMessageId!! + ).first() + } + parentChatMessage.activeUser = message.activeUser + parentChatMessage.imageUrl?.let { + binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE + binding.messageQuote.quotedMessageImage.load(it) { + addHeader( + "Authorization", + ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + ) + } + } ?: run { + binding.messageQuote.quotedMessageImage.visibility = View.GONE + } + binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName + ?: context.getText(R.string.nc_nick_guest) + binding.messageQuote.quotedMessage.text = messageUtils + .enrichChatReplyMessageText( + binding.messageQuote.quotedMessage.context, + parentChatMessage, + false, + viewThemeUtils + ) + viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage) + viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor) + viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView) + + binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE + } catch (e: Exception) { + Log.d(TAG, "Error when processing parent message in view holder", e) + } + } } else { binding.messageQuote.quotedChatMessageView.visibility = View.GONE } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt index 72fe4d986..18473daea 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt @@ -9,18 +9,21 @@ package com.nextcloud.talk.adapters.messages import android.annotation.SuppressLint import android.content.Context +import android.util.Log import android.view.View import androidx.appcompat.content.res.AppCompatResources +import androidx.lifecycle.lifecycleScope import autodagger.AutoInjector import coil.load import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.talk.R +import com.nextcloud.talk.adapters.messages.IncomingPollMessageViewHolder.Companion import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.databinding.ItemCustomOutcomingPollMessageBinding -import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.models.json.chat.ReadStatus import com.nextcloud.talk.polls.ui.PollMainDialogFragment import com.nextcloud.talk.ui.theme.ViewThemeUtils @@ -29,6 +32,11 @@ import com.nextcloud.talk.utils.DateUtils import com.nextcloud.talk.utils.message.MessageUtils import com.nextcloud.talk.utils.preferences.AppPreferences import com.stfalcon.chatkit.messages.MessageHolders +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) @@ -153,34 +161,53 @@ class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) : } private fun setParentMessageDataOnMessageItem(message: ChatMessage) { - if (!message.isDeleted && message.parentMessage != null) { - val parentChatMessage = message.parentMessage - parentChatMessage!!.activeUser = message.activeUser - parentChatMessage.imageUrl?.let { - binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE - binding.messageQuote.quotedMessageImage.load(it) { - addHeader( - "Authorization", - ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + if (message.parentMessageId != null && !message.isDeleted) { + CoroutineScope(Dispatchers.Main).launch { + try { + val chatActivity = commonMessageInterface as ChatActivity + val urlForChatting = ApiUtils.getUrlForChat( + chatActivity.chatApiVersion, + chatActivity.conversationUser?.baseUrl, + chatActivity.roomToken ) - } - } ?: run { - binding.messageQuote.quotedMessageImage.visibility = View.GONE - } - binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName - ?: context.getText(R.string.nc_nick_guest) - binding.messageQuote.quotedMessage.text = messageUtils - .enrichChatReplyMessageText( - binding.messageQuote.quotedMessage.context, - parentChatMessage, - false, - viewThemeUtils - ) - viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage) - viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor) - viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView) - binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE + val parentChatMessage = withContext(Dispatchers.IO) { + chatActivity.chatViewModel.getMessageById( + urlForChatting, + chatActivity.currentConversation!!, + message.parentMessageId!! + ).first() + } + parentChatMessage.activeUser = message.activeUser + parentChatMessage.imageUrl?.let { + binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE + binding.messageQuote.quotedMessageImage.load(it) { + addHeader( + "Authorization", + ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + ) + } + } ?: run { + binding.messageQuote.quotedMessageImage.visibility = View.GONE + } + binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName + ?: context.getText(R.string.nc_nick_guest) + binding.messageQuote.quotedMessage.text = messageUtils + .enrichChatReplyMessageText( + binding.messageQuote.quotedMessage.context, + parentChatMessage, + false, + viewThemeUtils + ) + viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage) + viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor) + viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView) + + binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE + } catch (e: Exception) { + Log.d(TAG, "Error when processing parent message in view holder", e) + } + } } else { binding.messageQuote.quotedChatMessageView.visibility = View.GONE } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java index 81f80a582..21509d502 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java @@ -17,7 +17,7 @@ import com.google.android.material.card.MaterialCardView; import com.nextcloud.talk.R; import com.nextcloud.talk.databinding.ItemCustomOutcomingPreviewMessageBinding; import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding; -import com.nextcloud.talk.models.json.chat.ChatMessage; +import com.nextcloud.talk.chat.data.model.ChatMessage; import com.nextcloud.talk.utils.TextMatchers; import java.util.HashMap; 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 eaffe1312..ba0b4fe1a 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 @@ -9,6 +9,7 @@ package com.nextcloud.talk.adapters.messages import android.content.Context +import android.util.Log import android.util.TypedValue import android.view.View import androidx.core.content.res.ResourcesCompat @@ -20,8 +21,8 @@ import com.nextcloud.talk.R 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.databinding.ItemCustomOutcomingTextMessageBinding -import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.models.json.chat.ReadStatus import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.ApiUtils @@ -29,6 +30,11 @@ import com.nextcloud.talk.utils.DateUtils import com.nextcloud.talk.utils.TextMatchers import com.nextcloud.talk.utils.message.MessageUtils import com.stfalcon.chatkit.messages.MessageHolders.OutcomingTextMessageViewHolder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) @@ -91,14 +97,14 @@ class OutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessageViewH if (message.lastEditTimestamp != 0L && !message.isDeleted) { binding.messageEditIndicator.visibility = View.VISIBLE - binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.lastEditTimestamp) + binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.lastEditTimestamp!!) } else { binding.messageEditIndicator.visibility = View.GONE binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) } // parent message handling - if (!message.isDeleted && message.parentMessage != null) { + if (!message.isDeleted && message.parentMessageId != null) { processParentMessage(message) binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE } else { @@ -148,36 +154,58 @@ class OutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessageViewH } private fun processParentMessage(message: ChatMessage) { - val parentChatMessage = message.parentMessage - parentChatMessage!!.activeUser = message.activeUser - parentChatMessage.imageUrl?.let { - binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE - binding.messageQuote.quotedMessageImage.load(it) { - addHeader( - "Authorization", - ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! - ) + if (message.parentMessageId != null && !message.isDeleted) { + CoroutineScope(Dispatchers.Main).launch { + try { + val chatActivity = commonMessageInterface as ChatActivity + val urlForChatting = ApiUtils.getUrlForChat( + chatActivity.chatApiVersion, + chatActivity.conversationUser?.baseUrl, + chatActivity.roomToken + ) + + val parentChatMessage = withContext(Dispatchers.IO) { + chatActivity.chatViewModel.getMessageById( + urlForChatting, + chatActivity.currentConversation!!, + message.parentMessageId!! + ).first() + } + + parentChatMessage!!.activeUser = message.activeUser + parentChatMessage.imageUrl?.let { + binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE + binding.messageQuote.quotedMessageImage.load(it) { + addHeader( + "Authorization", + ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + ) + } + } ?: run { + binding.messageQuote.quotedMessageImage.visibility = View.GONE + } + binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName + ?: context.getText(R.string.nc_nick_guest) + binding.messageQuote.quotedMessage.text = messageUtils + .enrichChatReplyMessageText( + binding.messageQuote.quotedMessage.context, + parentChatMessage, + false, + viewThemeUtils + ) + + viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage) + viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor) + viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView) + + binding.messageQuote.quotedChatMessageView.setOnClickListener { + val chatActivity = commonMessageInterface as ChatActivity + chatActivity.jumpToQuotedMessage(parentChatMessage) + } + } catch (e: Exception) { + Log.d(TAG, "Error when processing parent message in view holder", e) + } } - } ?: run { - binding.messageQuote.quotedMessageImage.visibility = View.GONE - } - binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName - ?: context.getText(R.string.nc_nick_guest) - binding.messageQuote.quotedMessage.text = messageUtils - .enrichChatReplyMessageText( - binding.messageQuote.quotedMessage.context, - parentChatMessage, - false, - viewThemeUtils - ) - - viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage) - viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor) - viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView) - - binding.messageQuote.quotedChatMessageView.setOnClickListener { - val chatActivity = commonMessageInterface as ChatActivity - chatActivity.jumpToQuotedMessage(parentChatMessage) } } @@ -191,5 +219,6 @@ class OutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessageViewH companion object { const val TEXT_SIZE_MULTIPLIER = 2.5 + private val TAG = OutcomingTextMessageViewHolder::class.java.simpleName } } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt index ccd32b0ef..474999af0 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt @@ -17,16 +17,19 @@ import android.view.View import android.widget.SeekBar import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope import androidx.work.WorkInfo import androidx.work.WorkManager import autodagger.AutoInjector import coil.load import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.talk.R +import com.nextcloud.talk.adapters.messages.IncomingPollMessageViewHolder.Companion import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.databinding.ItemCustomOutcomingVoiceMessageBinding -import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.models.json.chat.ReadStatus import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.ApiUtils @@ -34,6 +37,11 @@ import com.nextcloud.talk.utils.DateUtils import com.nextcloud.talk.utils.message.MessageUtils import com.nextcloud.talk.utils.preferences.AppPreferences import com.stfalcon.chatkit.messages.MessageHolders +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.util.concurrent.ExecutionException import javax.inject.Inject @@ -238,14 +246,17 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : Log.d(TAG, "WorkInfo.State.RUNNING in ViewHolder") showVoiceMessageLoading() } + WorkInfo.State.SUCCEEDED -> { Log.d(TAG, "WorkInfo.State.SUCCEEDED in ViewHolder") showPlayButton() } + WorkInfo.State.FAILED -> { Log.d(TAG, "WorkInfo.State.FAILED in ViewHolder") showPlayButton() } + else -> { Log.d(TAG, "WorkInfo.State unused in ViewHolder") } @@ -264,34 +275,53 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : } private fun setParentMessageDataOnMessageItem(message: ChatMessage) { - if (!message.isDeleted && message.parentMessage != null) { - val parentChatMessage = message.parentMessage - parentChatMessage!!.activeUser = message.activeUser - parentChatMessage.imageUrl?.let { - binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE - binding.messageQuote.quotedMessageImage.load(it) { - addHeader( - "Authorization", - ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + if (message.parentMessageId != null && !message.isDeleted) { + CoroutineScope(Dispatchers.Main).launch { + try { + val chatActivity = commonMessageInterface as ChatActivity + val urlForChatting = ApiUtils.getUrlForChat( + chatActivity.chatApiVersion, + chatActivity.conversationUser?.baseUrl, + chatActivity.roomToken ) - } - } ?: run { - binding.messageQuote.quotedMessageImage.visibility = View.GONE - } - binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName - ?: context!!.getText(R.string.nc_nick_guest) - binding.messageQuote.quotedMessage.text = messageUtils - .enrichChatReplyMessageText( - binding.messageQuote.quotedMessage.context, - parentChatMessage, - false, - viewThemeUtils - ) - viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage) - viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor) - viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView) - binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE + val parentChatMessage = withContext(Dispatchers.IO) { + chatActivity.chatViewModel.getMessageById( + urlForChatting, + chatActivity.currentConversation!!, + message.parentMessageId!! + ).first() + } + parentChatMessage.activeUser = message.activeUser + parentChatMessage.imageUrl?.let { + binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE + binding.messageQuote.quotedMessageImage.load(it) { + addHeader( + "Authorization", + ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + ) + } + } ?: run { + binding.messageQuote.quotedMessageImage.visibility = View.GONE + } + binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName + ?: context!!.getText(R.string.nc_nick_guest) + binding.messageQuote.quotedMessage.text = messageUtils + .enrichChatReplyMessageText( + binding.messageQuote.quotedMessage.context, + parentChatMessage, + false, + viewThemeUtils + ) + viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage) + viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor) + viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView) + + binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE + } catch (e: Exception) { + Log.d(TAG, "Error when processing parent message in view holder", e) + } + } } else { binding.messageQuote.quotedChatMessageView.visibility = View.GONE } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageInterface.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageInterface.kt index 4833a50b0..c7358b9be 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageInterface.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageInterface.kt @@ -6,7 +6,7 @@ */ package com.nextcloud.talk.adapters.messages -import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.chat.data.model.ChatMessage interface PreviewMessageInterface { fun onPreviewMessageLongClick(chatMessage: ChatMessage) diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt index 7ff68e951..02159ceab 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt @@ -34,7 +34,7 @@ import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding import com.nextcloud.talk.extensions.loadChangelogBotAvatar import com.nextcloud.talk.extensions.loadFederatedUserAvatar -import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.utils.DateUtils diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/Reaction.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/Reaction.kt index 4d00f445d..801c2220b 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/Reaction.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/Reaction.kt @@ -12,7 +12,7 @@ import android.view.ViewGroup import android.widget.LinearLayout import android.widget.TextView import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding -import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.DisplayUtils import com.vanniktech.emoji.EmojiTextView diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageInterface.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageInterface.kt index 8258921d8..18199b759 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageInterface.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageInterface.kt @@ -6,7 +6,7 @@ */ package com.nextcloud.talk.adapters.messages -import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.chat.data.model.ChatMessage interface SystemMessageInterface { fun expandSystemMessage(chatMessage: ChatMessage) diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageViewHolder.kt index e442e2a4f..a9332636a 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageViewHolder.kt @@ -19,7 +19,7 @@ import com.nextcloud.talk.R import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.databinding.ItemSystemMessageBinding -import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.utils.DateUtils import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.preferences.AppPreferences diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/TalkMessagesListAdapter.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/TalkMessagesListAdapter.java index f5121a6e5..b9185c7aa 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/TalkMessagesListAdapter.java +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/TalkMessagesListAdapter.java @@ -33,7 +33,7 @@ public class TalkMessagesListAdapter<M extends IMessage> extends MessagesListAda @Override public void onBindViewHolder(ViewHolder holder, int position) { - super.onBindViewHolder(holder, position); + if (holder instanceof IncomingTextMessageViewHolder) { ((IncomingTextMessageViewHolder) holder).assignCommonMessageInterface(chatActivity); @@ -66,5 +66,7 @@ public class TalkMessagesListAdapter<M extends IMessage> extends MessagesListAda } else if (holder instanceof CallStartedViewHolder) { ((CallStartedViewHolder) holder).assignCallStartedMessageInterface(chatActivity); } + + super.onBindViewHolder(holder, position); } } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/UnreadNoticeMessageViewHolder.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/UnreadNoticeMessageViewHolder.java index 888b62c94..6615b4f0d 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/UnreadNoticeMessageViewHolder.java +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/UnreadNoticeMessageViewHolder.java @@ -9,7 +9,7 @@ package com.nextcloud.talk.adapters.messages; import android.view.View; -import com.nextcloud.talk.models.json.chat.ChatMessage; +import com.nextcloud.talk.chat.data.model.ChatMessage; import com.stfalcon.chatkit.messages.MessageHolders; public class UnreadNoticeMessageViewHolder extends MessageHolders.SystemMessageViewHolder<ChatMessage> { diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/VoiceMessageInterface.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/VoiceMessageInterface.kt index df0457752..aada2ade2 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/VoiceMessageInterface.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/VoiceMessageInterface.kt @@ -6,7 +6,7 @@ */ package com.nextcloud.talk.adapters.messages -import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.chat.data.model.ChatMessage interface VoiceMessageInterface { fun updateMediaPlayerProgressBySlider(message: ChatMessage, progress: Int) diff --git a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt b/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt index 95a6ce254..261e869f6 100644 --- a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt +++ b/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt @@ -36,6 +36,7 @@ import com.nextcloud.talk.BuildConfig import com.nextcloud.talk.components.filebrowser.webdav.DavUtils import com.nextcloud.talk.dagger.modules.BusModule import com.nextcloud.talk.dagger.modules.ContextModule +import com.nextcloud.talk.dagger.modules.DaosModule import com.nextcloud.talk.dagger.modules.DatabaseModule import com.nextcloud.talk.dagger.modules.ManagerModule import com.nextcloud.talk.dagger.modules.RepositoryModule @@ -79,7 +80,8 @@ import javax.inject.Singleton RepositoryModule::class, UtilsModule::class, ThemeModule::class, - ManagerModule::class + ManagerModule::class, + DaosModule::class ] ) @Singleton 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 0eada3e66..efc04b6b2 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -59,6 +59,7 @@ import androidx.emoji2.text.EmojiCompat import androidx.fragment.app.DialogFragment import androidx.fragment.app.commit import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -104,6 +105,7 @@ import com.nextcloud.talk.adapters.messages.UnreadNoticeMessageViewHolder import com.nextcloud.talk.adapters.messages.VoiceMessageInterface import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.chat.viewmodels.ChatViewModel import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel import com.nextcloud.talk.conversationinfo.ConversationInfoActivity @@ -119,14 +121,9 @@ import com.nextcloud.talk.jobs.UploadAndShareFilesWorker import com.nextcloud.talk.location.LocationPickerActivity import com.nextcloud.talk.messagesearch.MessageSearchActivity import com.nextcloud.talk.models.domain.ConversationModel -import com.nextcloud.talk.models.domain.ConversationReadOnlyState -import com.nextcloud.talk.models.domain.ConversationType -import com.nextcloud.talk.models.domain.LobbyState -import com.nextcloud.talk.models.domain.ObjectType import com.nextcloud.talk.models.json.capabilities.SpreedCapability -import com.nextcloud.talk.models.json.chat.ChatMessage -import com.nextcloud.talk.models.json.chat.ChatOverall import com.nextcloud.talk.models.json.chat.ReadStatus +import com.nextcloud.talk.models.json.conversations.ConversationEnums import com.nextcloud.talk.polls.ui.PollCreateDialogFragment import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity import com.nextcloud.talk.shareditems.activities.SharedItemsActivity @@ -183,6 +180,8 @@ import com.stfalcon.chatkit.messages.MessagesListAdapter import com.stfalcon.chatkit.utils.DateFormatter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.greenrobot.eventbus.Subscribe @@ -408,6 +407,7 @@ class ChatActivity : handleIntent(intent) chatViewModel = ViewModelProvider(this, viewModelFactory)[ChatViewModel::class.java] + messageInputViewModel = ViewModelProvider(this, viewModelFactory)[MessageInputViewModel::class.java] binding.progressBar.visibility = View.VISIBLE @@ -521,12 +521,37 @@ class ChatActivity : @Suppress("LongMethod") private fun initObservers() { Log.d(TAG, "initObservers Called") + + this.lifecycleScope.launch { + chatViewModel.getConversationFlow + .onEach { conversationModel -> + currentConversation = conversationModel + + val urlForChatting = ApiUtils.getUrlForChat(chatApiVersion, conversationUser?.baseUrl, roomToken) + val credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser!!.token) + + chatViewModel.setData( + currentConversation!!, + credentials!!, + urlForChatting + ) + + logConversationInfos("GetRoomSuccessState") + + if (adapter == null) { + initAdapter() + binding.messagesListView.setAdapter(adapter) + layoutManager = binding.messagesListView.layoutManager as LinearLayoutManager? + } + + chatViewModel.getCapabilities(conversationUser!!, roomToken, currentConversation!!) + }.collect() + } + chatViewModel.getRoomViewState.observe(this) { state -> when (state) { is ChatViewModel.GetRoomSuccessState -> { - currentConversation = state.conversationModel - logConversationInfos("GetRoomSuccessState") - chatViewModel.getCapabilities(conversationUser!!, roomToken, currentConversation!!) + // unused atm } is ChatViewModel.GetRoomErrorState -> { @@ -569,24 +594,29 @@ class ChatActivity : binding.chatToolbar.setOnClickListener { _ -> showConversationInfoScreen() } } - if (adapter == null) { - initAdapter() - binding.messagesListView.setAdapter(adapter) - layoutManager = binding.messagesListView.layoutManager as LinearLayoutManager? - } + // if (adapter == null) { + // initAdapter() + // binding.messagesListView.setAdapter(adapter) + // layoutManager = binding.messagesListView.layoutManager as LinearLayoutManager? + // } loadAvatarForStatusBar() setupSwipeToReply() setActionBarTitle() updateRoomTimerHandler() - chatViewModel.refreshChatParams( - setupFieldsForPullChatMessages( - false, - 0, - false - ) + val urlForChatting = ApiUtils.getUrlForChat(chatApiVersion, conversationUser?.baseUrl, roomToken) + + chatViewModel.loadMessages( + withCredentials = credentials!!, + withUrl = urlForChatting, ) + + // chatViewModel.initMessagePolling( + // withCredentials = credentials!!, + // withUrl = urlForChatting, + // roomToken = currentConversation!!.token!! + // ) } is ChatViewModel.GetCapabilitiesErrorState -> { @@ -705,6 +735,11 @@ class ChatActivity : Snackbar.LENGTH_LONG ).show() } + + val id = state.msg.ocs!!.data!!.parentMessage!!.id.toString() + val index = adapter?.getMessagePositionById(id) ?: 0 + val message = adapter?.items?.get(index)?.item as ChatMessage + setMessageAsDeleted(message) } is ChatViewModel.DeleteChatMessageErrorState -> { @@ -738,130 +773,72 @@ class ChatActivity : } } - chatViewModel.getFieldMapForChat.observe(this) { fieldMap -> - if (fieldMap.isNotEmpty()) { - chatViewModel.pullChatMessages( - credentials!!, - ApiUtils.getUrlForChat(chatApiVersion, conversationUser?.baseUrl, roomToken) - ) - } - } - - chatViewModel.pullChatMessageViewState.observe(this) { state -> + chatViewModel.chatMessageViewState.observe(this) { state -> when (state) { - is ChatViewModel.PullChatMessageSuccessState -> { - Log.d(TAG, "PullChatMessageSuccess: Code: ${state.response.code()}") - when (state.response.code()) { - HTTP_CODE_OK -> { - Log.d(TAG, "lookIntoFuture: ${state.lookIntoFuture}") - val chatOverall = state.response.body() as ChatOverall? - var chatMessageList = chatOverall?.ocs!!.data!! - - val newXChatLastCommonRead = state.response.headers()["X-Chat-Last-Common-Read"]?.let { - Integer.parseInt(it) - } - - processHeaderChatLastGiven(state.response, state.lookIntoFuture) - - chatMessageList = handleSystemMessages(chatMessageList) - - if (chatMessageList.isEmpty()) { - chatViewModel.refreshChatParams( - setupFieldsForPullChatMessages( - true, - newXChatLastCommonRead, - true - ) - ) - return@observe - } - - determinePreviousMessageIds(chatMessageList) - - handleExpandableSystemMessages(chatMessageList) - - if (chatMessageList.isNotEmpty() && - ChatMessage.SystemMessageType.CLEARED_CHAT == chatMessageList[0].systemMessageType - ) { - adapter?.clear() - adapter?.notifyDataSetChanged() - } - - var lastAdapterId = getLastAdapterId() - val oneNewMessage = (lastAdapterId != 0 || chatMessageList.size == 1) - - if ( - state.lookIntoFuture && - oneNewMessage && - chatMessageList[0].jsonMessageId > lastAdapterId - ) { - processMessagesFromTheFuture(chatMessageList) - } else if (!state.lookIntoFuture) { - processMessagesNotFromTheFuture(chatMessageList) - collapseSystemMessages() - } - - updateReadStatusOfAllMessages(newXChatLastCommonRead) - - processCallStartedMessages(chatMessageList) - - adapter?.notifyDataSetChanged() - - chatViewModel.refreshChatParams( - setupFieldsForPullChatMessages( - true, - newXChatLastCommonRead, - true - ) - ) - } - - HTTP_CODE_NOT_MODIFIED -> { - chatViewModel.refreshChatParams( - setupFieldsForPullChatMessages( - true, - globalLastKnownPastMessageId, - true - ) - ) - } - - HTTP_CODE_PRECONDITION_FAILED -> { - chatViewModel.refreshChatParams( - setupFieldsForPullChatMessages( - true, - globalLastKnownPastMessageId, - true - ) - ) - } - - else -> {} - } - - processExpiredMessages() - if (isFirstMessagesProcessing) { - cancelNotificationsForCurrentConversation() - isFirstMessagesProcessing = false - binding.progressBar.visibility = View.GONE - binding.messagesListView.visibility = View.VISIBLE - - collapseSystemMessages() - } + is ChatViewModel.ChatMessageStartState -> { + // Handle UI on first load + cancelNotificationsForCurrentConversation() + binding.progressBar.visibility = View.GONE + binding.messagesListView.visibility = View.VISIBLE + collapseSystemMessages() } - is ChatViewModel.PullChatMessageCompleteState -> { - Log.d(TAG, "PullChatMessageCompleted") + is ChatViewModel.ChatMessageUpdateState -> { + // unused atm } - is ChatViewModel.PullChatMessageErrorState -> { - Log.d(TAG, "PullChatMessageError") + is ChatViewModel.ChatMessageErrorState -> { + // unused atm } else -> {} } } + this.lifecycleScope.launch { + chatViewModel.getMessageFlow + .onEach { pair -> + val lookIntoFuture = pair.first + var chatMessageList = pair.second + + chatMessageList = handleSystemMessages(chatMessageList) + + determinePreviousMessageIds(chatMessageList) + + handleExpandableSystemMessages(chatMessageList) + + if (chatMessageList.isNotEmpty() && + ChatMessage.SystemMessageType.CLEARED_CHAT == chatMessageList[0].systemMessageType + ) { + adapter?.clear() + adapter?.notifyDataSetChanged() + // TODO: remove messages from DB, Should be handled beforehand (in viewModel?) + } + + if (lookIntoFuture) { + processMessagesFromTheFuture(chatMessageList) + } else { + processMessagesNotFromTheFuture(chatMessageList) + collapseSystemMessages() + } + + processCallStartedMessages(chatMessageList) + + adapter?.notifyDataSetChanged() + } + .collect() + + + chatViewModel.getUpdateMessageFlow + .onEach { pair -> + val lookIntoFuture = pair.first + var chatMessageList = pair.second + + adapter!!.update(chatMessageList[0]) + } + .collect() + } + chatViewModel.reactionDeletedViewState.observe(this) { state -> when (state) { is ChatViewModel.ReactionDeletedSuccessState -> { @@ -916,6 +893,11 @@ class ChatActivity : ).show() } } + val newString = state.messageEdited.ocs?.data?.parentMessage?.message ?: "(null)" + val id = state.messageEdited.ocs?.data?.parentMessage?.id.toString() + val index = adapter?.getMessagePositionById(id) ?: 0 + val message = adapter?.items?.get(index)?.item as ChatMessage + setMessageAsEdited(message, newString) } is MessageInputViewModel.EditMessageErrorState -> { @@ -1412,15 +1394,15 @@ class ChatActivity : fun isOneToOneConversation() = currentConversation != null && currentConversation?.type != null && - currentConversation?.type == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL + currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL private fun isGroupConversation() = currentConversation != null && currentConversation?.type != null && - currentConversation?.type == ConversationType.ROOM_GROUP_CALL + currentConversation?.type == ConversationEnums.ConversationType.ROOM_GROUP_CALL private fun isPublicConversation() = currentConversation != null && currentConversation?.type != null && - currentConversation?.type == ConversationType.ROOM_PUBLIC_CALL + currentConversation?.type == ConversationEnums.ConversationType.ROOM_PUBLIC_CALL private fun updateRoomTimerHandler() { val delayForRecursiveCall = if (shouldShowLobby()) { @@ -1443,7 +1425,7 @@ class ChatActivity : private fun switchToRoom(token: String, startCallAfterRoomSwitch: Boolean, isVoiceOnlyCall: Boolean) { if (conversationUser != null) { runOnUiThread { - if (currentConversation?.objectType == ObjectType.ROOM) { + if (currentConversation?.objectType == ConversationEnums.ObjectType.ROOM) { Snackbar.make( binding.root, context.resources.getString(R.string.switch_to_main_room), @@ -1826,7 +1808,7 @@ class ChatActivity : private fun shouldShowLobby(): Boolean { if (currentConversation != null) { return CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.WEBINARY_LOBBY) && - currentConversation?.lobbyState == LobbyState.LOBBY_STATE_MODERATORS_ONLY && + currentConversation?.lobbyState == ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY && !ConversationUtils.canModerate(currentConversation!!, spreedCapabilities) && !participantPermissions.canIgnoreLobby() } @@ -1862,7 +1844,7 @@ class ChatActivity : private fun isReadOnlyConversation(): Boolean { return currentConversation?.conversationReadOnlyState != null && currentConversation?.conversationReadOnlyState == - ConversationReadOnlyState.CONVERSATION_READ_ONLY + ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_ONLY } private fun checkLobbyState() { @@ -2327,7 +2309,7 @@ class ChatActivity : "" } - if (currentConversation?.type == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) { + if (currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) { var statusMessage = "" if (currentConversation?.statusIcon != null) { statusMessage += currentConversation?.statusIcon @@ -2337,8 +2319,8 @@ class ChatActivity : } statusMessageViewContents(statusMessage) } else { - if (currentConversation?.type == ConversationType.ROOM_GROUP_CALL || - currentConversation?.type == ConversationType.ROOM_PUBLIC_CALL + if (currentConversation?.type == ConversationEnums.ConversationType.ROOM_GROUP_CALL || + currentConversation?.type == ConversationEnums.ConversationType.ROOM_PUBLIC_CALL ) { var descriptionMessage = "" descriptionMessage += currentConversation?.description @@ -2610,9 +2592,9 @@ class ChatActivity : GROUPED_MESSAGES_SAME_AUTHOR_THRESHOLD > 0 ) chatMessage.isOneToOneConversation = - (currentConversation?.type == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) + (currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) chatMessage.isFormerOneToOneConversation = - (currentConversation?.type == ConversationType.FORMER_ONE_TO_ONE) + (currentConversation?.type == ConversationEnums.ConversationType.FORMER_ONE_TO_ONE) it.addToStart(chatMessage, scrollToEndOnUpdate) } } @@ -2640,9 +2622,9 @@ class ChatActivity : val chatMessage = chatMessageList[i] chatMessage.isOneToOneConversation = - currentConversation?.type == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL + currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL chatMessage.isFormerOneToOneConversation = - (currentConversation?.type == ConversationType.FORMER_ONE_TO_ONE) + (currentConversation?.type == ConversationEnums.ConversationType.FORMER_ONE_TO_ONE) chatMessage.activeUser = conversationUser chatMessage.token = roomToken } @@ -2721,6 +2703,7 @@ class ChatActivity : if (!voiceMessageToRestoreId.equals("")) { Log.d(RESUME_AUDIO_TAG, "begin method to resume audio playback") + // TODO: replace this logic by calling getItemFromAdapter(messageId) if (adapter != null) { Log.d(RESUME_AUDIO_TAG, "adapter is not null, proceeding") val voiceMessagePosition = adapter!!.items!!.indexOfFirst { @@ -2748,7 +2731,7 @@ class ChatActivity : ) } } else { - Log.d(RESUME_AUDIO_TAG, "TalkMessagesListAdapater is null") + Log.d(RESUME_AUDIO_TAG, "TalkMessagesListAdapter is null") } } else { Log.d(RESUME_AUDIO_TAG, "No voice message to restore") @@ -2758,6 +2741,29 @@ class ChatActivity : voiceMessageToRestoreWasPlaying = false } + private fun getItemFromAdapter(messageId: String): ChatMessage? { + if (adapter != null) { + val messagePosition = adapter!!.items!!.indexOfFirst { + it.item is ChatMessage && (it.item as ChatMessage).id == messageId + } + if (messagePosition >= 0) { + val currentItem = adapter?.items?.get(messagePosition)?.item + if (currentItem is ChatMessage && currentItem.id == messageId) { + return currentItem + } else { + Log.d(TAG, "currentItem retrieved was not chatmessage or its id was not correct") + } + } else { + Log.d( + TAG, "messagePosition is -1, adapter # of items: " + adapter!!.itemCount + ) + } + } else { + Log.d(TAG, "TalkMessagesListAdapter is null") + } + return null + } + private fun scrollToRequestedMessageIfNeeded() { intent.getStringExtra(BundleKeys.KEY_MESSAGE_ID)?.let { scrollToMessageWithId(it) @@ -2771,16 +2777,21 @@ class ChatActivity : } override fun onLoadMore(page: Int, totalItemsCount: Int) { - val calculatedPage = totalItemsCount / PAGE_SIZE - if (calculatedPage > 0) { - chatViewModel.refreshChatParams( - setupFieldsForPullChatMessages( - false, - null, - true - ) - ) - } + val id = ( + adapter?.items?.last { + it.item is ChatMessage + }?.item as ChatMessage + ).jsonMessageId + + val urlForChatting = ApiUtils.getUrlForChat(chatApiVersion, conversationUser?.baseUrl, roomToken) + + chatViewModel.loadMoreMessages( + beforeMessageId = id.toLong(), + withUrl = urlForChatting, + withCredentials = credentials!!, + withMessageLimit = MESSAGE_PULL_LIMIT, + roomToken = currentConversation!!.token!! + ) } override fun format(date: Date): String { @@ -2923,18 +2934,25 @@ class ChatActivity : // setDeletionFlagsAndRemoveInfomessages if (isInfoMessageAboutDeletion(currentMessage)) { - if (!chatMessageMap.containsKey(currentMessage.value.parentMessage!!.id)) { + if (!chatMessageMap.containsKey(currentMessage.value.parentMessageId.toString())) { // if chatMessageMap doesn't contain message to delete (this happens when lookingIntoFuture), // the message to delete has to be modified directly inside the adapter - setMessageAsDeleted(currentMessage.value.parentMessage) + + val id = currentMessage.value.parentMessageId.toString() + val index = adapter?.getMessagePositionById(id) ?: 0 + + if (index > 0) { + val message = adapter?.items?.get(index)?.item as ChatMessage + setMessageAsDeleted(message) + } } else { - chatMessageMap[currentMessage.value.parentMessage!!.id]!!.isDeleted = true + chatMessageMap[currentMessage.value.parentMessageId.toString()]!!.isDeleted = true } chatMessageIterator.remove() } else if (isReactionsMessage(currentMessage)) { // delete reactions system messages - if (!chatMessageMap.containsKey(currentMessage.value.parentMessage!!.id)) { - updateAdapterForReaction(currentMessage.value.parentMessage) + if (!chatMessageMap.containsKey(currentMessage.value.parentMessageId.toString())) { + // updateAdapterForReaction(currentMessage.value.parentMessage) TODO } chatMessageIterator.remove() @@ -2942,8 +2960,8 @@ class ChatActivity : // delete poll system messages chatMessageIterator.remove() } else if (isEditMessage(currentMessage)) { - if (!chatMessageMap.containsKey(currentMessage.value.parentMessage!!.id)) { - setMessageAsEdited(currentMessage.value.parentMessage) + if (!chatMessageMap.containsKey(currentMessage.value.parentMessageId.toString())) { + // setMessageAsEdited(currentMessage.value.parentMessage) TODO } chatMessageIterator.remove() @@ -2977,7 +2995,7 @@ class ChatActivity : } private fun isInfoMessageAboutDeletion(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean { - return currentMessage.value.parentMessage != null && currentMessage.value.systemMessageType == ChatMessage + return currentMessage.value.parentMessageId != null && currentMessage.value.systemMessageType == ChatMessage .SystemMessageType.MESSAGE_DELETED } @@ -2988,7 +3006,7 @@ class ChatActivity : } private fun isEditMessage(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean { - return currentMessage.value.parentMessage != null && currentMessage.value.systemMessageType == ChatMessage + return currentMessage.value.parentMessageId != null && currentMessage.value.systemMessageType == ChatMessage .SystemMessageType.MESSAGE_EDITED } @@ -3039,7 +3057,7 @@ class ChatActivity : bundle.putBoolean(BundleKeys.KEY_CALL_WITHOUT_NOTIFICATION, true) } - if (it.objectType == ObjectType.ROOM) { + if (it.objectType == ConversationEnums.ObjectType.ROOM) { bundle.putBoolean(KEY_IS_BREAKOUT_ROOM, true) } @@ -3285,7 +3303,7 @@ class ChatActivity : val lon = data["longitude"]!! metaData = "{\"type\":\"geo-location\",\"id\":\"geo:$lat,$lon\",\"latitude\":\"$lat\"," + - "\"longitude\":\"$lon\",\"name\":\"$name\"}" + "\"longitude\":\"$lon\",\"name\":\"$name\"}" } when (type) { @@ -3350,7 +3368,7 @@ class ChatActivity : conversationUser?.userId?.isNotEmpty() == true && conversationUser!!.userId != "?" && message.user.id.startsWith("users/") && message.user.id.substring(ACTOR_LENGTH) != currentConversation?.actorId && - currentConversation?.type != ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL || + currentConversation?.type != ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL || isShowMessageDeletionButton(message) || // delete ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() || // forward message.previousMessageId > NO_PREVIOUS_MESSAGE_ID && // mark as unread @@ -3361,39 +3379,43 @@ class ChatActivity : private fun setMessageAsDeleted(message: IMessage?) { val messageTemp = message as ChatMessage messageTemp.isDeleted = true + messageTemp.message = getString(R.string.message_deleted_by_you) messageTemp.isOneToOneConversation = - currentConversation?.type == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL + currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL messageTemp.activeUser = conversationUser adapter?.update(messageTemp) } - private fun setMessageAsEdited(message: IMessage?) { + private fun setMessageAsEdited(message: IMessage?, newString: String) { val messageTemp = message as ChatMessage messageTemp.lastEditTimestamp = message.lastEditTimestamp + messageTemp.message = newString val index = adapter?.getMessagePositionById(messageTemp.id)!! if (index > 0) { val adapterMsg = adapter?.items?.get(index)?.item as ChatMessage - messageTemp.parentMessage = adapterMsg.parentMessage + messageTemp.parentMessageId = adapterMsg.parentMessageId } messageTemp.isOneToOneConversation = - currentConversation?.type == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL + currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL messageTemp.activeUser = conversationUser adapter?.update(messageTemp) } private fun updateAdapterForReaction(message: IMessage?) { - val messageTemp = message as ChatMessage + message?.let { + val messageTemp = message as ChatMessage - messageTemp.isOneToOneConversation = - currentConversation?.type == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL - messageTemp.activeUser = conversationUser + messageTemp.isOneToOneConversation = + currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL + messageTemp.activeUser = conversationUser - adapter?.update(messageTemp) + adapter?.update(messageTemp) + } } fun updateUiToAddReaction(message: ChatMessage, emoji: String) { @@ -3428,6 +3450,9 @@ class ChatActivity : amount = 0 } message.reactions!![emoji] = amount - 1 + if (message.reactions!![emoji]!! <= 0) { + message.reactions!!.remove(emoji) + } message.reactionsSelf!!.remove(emoji) adapter?.update(message) } @@ -3529,7 +3554,7 @@ class ChatActivity : @Subscribe(threadMode = ThreadMode.BACKGROUND) fun onMessageEvent(userMentionClickEvent: UserMentionClickEvent) { - if (currentConversation?.type != ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL || + if (currentConversation?.type != ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL || currentConversation?.name != userMentionClickEvent.userId ) { var apiVersion = 1 @@ -3602,13 +3627,21 @@ class ChatActivity : } fun jumpToQuotedMessage(parentMessage: ChatMessage) { + var foundMessage = false for (position in 0 until (adapter!!.items.size)) { val currentItem = adapter?.items?.get(position)?.item if (currentItem is ChatMessage && currentItem.id == parentMessage.id) { layoutManager!!.scrollToPosition(position) + foundMessage = true break } } + if (!foundMessage) { + Log.d(TAG, "quoted message with id " + parentMessage.id + " was not found in adapter") + // TODO: show better info + // TODO: improve handling how this can be avoided. E.g. loading chat until message is reached... + Snackbar.make(binding.root, "Message was not found", Snackbar.LENGTH_LONG).show() + } } override fun joinAudioCall() { @@ -3688,6 +3721,7 @@ class ChatActivity : private const val QUOTED_MESSAGE_IMAGE_MAX_HEIGHT = 96f private const val MENTION_AUTO_COMPLETE_ELEVATION = 6f private const val MESSAGE_PULL_LIMIT = 100 + private const val PAGE_SIZE = 100 private const val INVITE_LENGTH = 6 private const val ACTOR_LENGTH = 6 private const val ANIMATION_DURATION: Long = 750 @@ -3715,6 +3749,5 @@ class ChatActivity : private const val CURRENT_AUDIO_POSITION_KEY = "CURRENT_AUDIO_POSITION" private const val CURRENT_AUDIO_WAS_PLAYING_KEY = "CURRENT_AUDIO_PLAYING" private const val RESUME_AUDIO_TAG = "RESUME_AUDIO_TAG" - private const val PAGE_SIZE = 50 } } 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 ad21845f2..90fbc915d 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt @@ -27,6 +27,7 @@ import android.view.View import android.view.ViewGroup import android.view.animation.AlphaAnimation import android.view.animation.Animation +import android.view.animation.Animation.AnimationListener import android.view.animation.LinearInterpolator import android.widget.ImageButton import android.widget.ImageView @@ -40,6 +41,7 @@ import androidx.core.content.ContextCompat import androidx.core.widget.doAfterTextChanged import androidx.emoji2.widget.EmojiTextView import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import autodagger.AutoInjector import coil.load import com.google.android.flexbox.FlexboxLayout @@ -50,10 +52,11 @@ import com.nextcloud.talk.R import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.callbacks.MentionAutocompleteCallback +import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.chat.viewmodels.ChatViewModel +import com.nextcloud.talk.data.network.NetworkMonitor import com.nextcloud.talk.databinding.FragmentMessageInputBinding import com.nextcloud.talk.jobs.UploadAndShareFilesWorker -import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.models.json.mention.Mention import com.nextcloud.talk.models.json.signaling.NCSignalingMessage import com.nextcloud.talk.presenters.MentionAutocompletePresenter @@ -70,6 +73,9 @@ import com.nextcloud.talk.utils.text.Spans import com.otaliastudios.autocomplete.Autocomplete import com.stfalcon.chatkit.commons.models.IMessage import com.vanniktech.emoji.EmojiPopup +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import java.util.Objects import javax.inject.Inject @@ -101,6 +107,9 @@ class MessageInputFragment : Fragment() { @Inject lateinit var userManager: UserManager + @Inject + lateinit var networkMonitor: NetworkMonitor + lateinit var binding: FragmentMessageInputBinding private var typedWhileTypingTimerIsRunning: Boolean = false private var typingTimer: CountDownTimer? = null @@ -158,6 +167,76 @@ class MessageInputFragment : Fragment() { else -> {} } } + + viewLifecycleOwner.lifecycleScope.launch { + var wasOnline = true + networkMonitor.isOnline.onEach { isOnline -> + val connectionGained = (!wasOnline && isOnline) + wasOnline = !binding.fragmentMessageInputView.isShown + Log.d(TAG, "isOnline: $isOnline\nwasOnline: $wasOnline\nconnectionGained: $connectionGained") + + handleMessageQueue(isOnline) + handleUI(isOnline, connectionGained) + }.collect() + } + } + + private fun handleUI(isOnline: Boolean, connectionGained: Boolean) { + if (isOnline) { + if (connectionGained) { + val animation: Animation = AlphaAnimation(1.0f, 0.0f) + animation.duration = 3000 + animation.interpolator = LinearInterpolator() + binding.fragmentConnectionLost.setBackgroundColor(resources.getColor(R.color.hwSecurityGreen)) + binding.fragmentConnectionLost.text = getString(R.string.connection_gained) + binding.fragmentConnectionLost.startAnimation(animation) + binding.fragmentConnectionLost.animation.setAnimationListener(object : AnimationListener { + override fun onAnimationStart(animation: Animation?) { + // unused atm + } + + override fun onAnimationEnd(animation: Animation?) { + binding.fragmentConnectionLost.visibility = View.GONE + binding.fragmentConnectionLost.setBackgroundColor(resources.getColor(R.color.hwSecurityRed)) + binding.fragmentConnectionLost.text = + getString(R.string.connection_lost_sent_messages_are_queued) + } + + override fun onAnimationRepeat(animation: Animation?) { + // unused atm + } + }) + } + + binding.fragmentMessageInputView.attachmentButton.isEnabled = true + binding.fragmentMessageInputView.recordAudioButton.isEnabled = true + } else { + binding.fragmentConnectionLost.clearAnimation() + binding.fragmentConnectionLost.visibility = View.GONE + binding.fragmentConnectionLost.setBackgroundColor(resources.getColor(R.color.hwSecurityRed)) + binding.fragmentConnectionLost.text = + getString(R.string.connection_lost_sent_messages_are_queued) + binding.fragmentConnectionLost.visibility = View.VISIBLE + binding.fragmentMessageInputView.attachmentButton.isEnabled = false + binding.fragmentMessageInputView.recordAudioButton.isEnabled = false + } + } + + private fun handleMessageQueue(isOnline: Boolean) { + if (isOnline) { + chatActivity.messageInputViewModel.switchToMessageQueue(false) + chatActivity.messageInputViewModel.sendAndEmptyMessageQueue( + chatActivity.roomToken, + chatActivity.conversationUser!!.getCredentials(), + ApiUtils.getUrlForChat( + chatActivity.chatApiVersion, + chatActivity.conversationUser!!.baseUrl!!, + chatActivity.roomToken + ) + ) + } else { + chatActivity.messageInputViewModel.switchToMessageQueue(true) + } } private fun restoreState() { @@ -694,6 +773,7 @@ class MessageInputFragment : Fragment() { private fun sendMessage(message: CharSequence, replyTo: Int?, sendWithoutNotification: Boolean) { chatActivity.messageInputViewModel.sendChatMessage( + chatActivity.roomToken, chatActivity.conversationUser!!.getCredentials(), ApiUtils.getUrlForChat( chatActivity.chatApiVersion, diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt b/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt new file mode 100644 index 000000000..529af495a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt @@ -0,0 +1,76 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Your Name <your@email.com> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.chat.data + +import android.os.Bundle +import com.nextcloud.talk.chat.data.io.LifecycleAwareManager +import com.nextcloud.talk.models.json.chat.ChatMessageJson +import com.nextcloud.talk.data.sync.Syncable +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.models.domain.ConversationModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow + +interface ChatMessageRepository : LifecycleAwareManager { + + /** + * Stream of a list of messages to be handled using the associated boolean + * false for past messages, true for future messages. + */ + val messageFlow: + Flow< + Pair< + Boolean, + List<ChatMessage> + > + > + + val updateMessageFlow: + Flow< + Pair< + Boolean, + List<ChatMessage> + > + > + + fun setData( + conversationModel: ConversationModel, + credentials: String, + urlForChatting: String + ) + + fun loadInitialMessages(withNetworkParams: Bundle): Job + + /** + * Loads messages from local storage. If the messages are not found, then it + * synchronizes the database with the server, before retrying exactly once. Only + * emits to [messageFlow] if the message list is not empty. + * + * [withNetworkParams] credentials and url + */ + fun loadMoreMessages( + beforeMessageId: Long, + roomToken: String, + withMessageLimit: Int, + withNetworkParams: Bundle + ): Job + + /** + * Long polls the server for any updates to the chat, if found, it synchronizes + * the database with the server and emits the new messages to [messageFlow], + * else it simply retries after timeout. + * + * [withNetworkParams] credentials and url. + */ + fun initMessagePolling(): Job + + /** + * Gets a individual message. + */ + suspend fun getMessage(messageId: Long, bundle: Bundle): Flow<ChatMessage> +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.kt b/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt similarity index 68% rename from app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.kt rename to app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt index ed16ad788..1e228ba5b 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt @@ -2,120 +2,88 @@ * Nextcloud Talk - Android Client * * SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de> - * SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de> + * SPDX-FileCopyrightText: 2022-2024 Marcel Hibbe <dev@mhibbe.de> * SPDX-FileCopyrightText: 2021 Tim Krüger <t@timkrueger.me> * SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com> * SPDX-License-Identifier: GPL-3.0-or-later */ -package com.nextcloud.talk.models.json.chat +package com.nextcloud.talk.chat.data.model -import android.os.Parcelable import android.text.TextUtils import android.util.Log -import com.bluelinelabs.logansquare.annotation.JsonField import com.bluelinelabs.logansquare.annotation.JsonIgnore -import com.bluelinelabs.logansquare.annotation.JsonObject import com.nextcloud.talk.R import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.json.chat.ChatUtils.Companion.getParsedMessage +import com.nextcloud.talk.models.json.chat.ReadStatus import com.nextcloud.talk.models.json.converters.EnumSystemMessageTypeConverter import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.CapabilitiesUtil import com.stfalcon.chatkit.commons.models.IUser import com.stfalcon.chatkit.commons.models.MessageContentType -import kotlinx.parcelize.Parcelize import java.security.MessageDigest import java.util.Date -@Parcelize -@JsonObject data class ChatMessage( - @JsonIgnore var isGrouped: Boolean = false, - @JsonIgnore var isOneToOneConversation: Boolean = false, - @JsonIgnore var isFormerOneToOneConversation: Boolean = false, - @JsonIgnore var activeUser: User? = null, - @JsonIgnore var selectedIndividualHashMap: Map<String?, String?>? = null, - @JsonIgnore var isDeleted: Boolean = false, - @JsonField(name = ["id"]) var jsonMessageId: Int = 0, - @JsonIgnore var previousMessageId: Int = -1, - @JsonField(name = ["token"]) var token: String? = null, // guests or users - @JsonField(name = ["actorType"]) var actorType: String? = null, - @JsonField(name = ["actorId"]) var actorId: String? = null, // send when crafting a message - @JsonField(name = ["actorDisplayName"]) var actorDisplayName: String? = null, - @JsonField(name = ["timestamp"]) var timestamp: Long = 0, // send when crafting a message, max 1000 lines - @JsonField(name = ["message"]) var message: String? = null, - @JsonField(name = ["messageParameters"]) var messageParameters: HashMap<String?, HashMap<String?, String?>>? = null, - @JsonField(name = ["systemMessage"], typeConverter = EnumSystemMessageTypeConverter::class) var systemMessageType: SystemMessageType? = null, - @JsonField(name = ["isReplyable"]) var replyable: Boolean = false, - @JsonField(name = ["parent"]) - var parentMessage: ChatMessage? = null, + var parentMessageId: Long? = null, var readStatus: Enum<ReadStatus> = ReadStatus.NONE, - @JsonField(name = ["messageType"]) var messageType: String? = null, - @JsonField(name = ["reactions"]) var reactions: LinkedHashMap<String, Int>? = null, - @JsonField(name = ["reactionsSelf"]) var reactionsSelf: ArrayList<String>? = null, - @JsonField(name = ["expirationTimestamp"]) var expirationTimestamp: Int = 0, - @JsonField(name = ["markdown"]) var renderMarkdown: Boolean? = null, - @JsonField(name = ["lastEditActorDisplayName"]) var lastEditActorDisplayName: String? = null, - @JsonField(name = ["lastEditActorId"]) var lastEditActorId: String? = null, - @JsonField(name = ["lastEditActorType"]) var lastEditActorType: String? = null, - @JsonField(name = ["lastEditTimestamp"]) - var lastEditTimestamp: Long = 0, + var lastEditTimestamp: Long? = 0, var isDownloadingVoiceMessage: Boolean = false, @@ -145,7 +113,7 @@ data class ChatMessage( var openWhenDownloaded: Boolean = true -) : Parcelable, MessageContentType, MessageContentType.Image { +) : MessageContentType, MessageContentType.Image { var extractedUrlToPreview: String? = null @@ -282,95 +250,7 @@ data class ChatMessage( } } - val lastMessageDisplayText: String - get() { - if (getCalculateMessageType() == MessageType.REGULAR_TEXT_MESSAGE || - getCalculateMessageType() == MessageType.SYSTEM_MESSAGE || - getCalculateMessageType() == MessageType.SINGLE_LINK_MESSAGE - ) { - return text - } else { - if (MessageType.SINGLE_LINK_GIPHY_MESSAGE == getCalculateMessageType() || - MessageType.SINGLE_LINK_TENOR_MESSAGE == getCalculateMessageType() || - MessageType.SINGLE_LINK_GIF_MESSAGE == getCalculateMessageType() - ) { - return if (actorId == activeUser!!.userId) { - sharedApplication!!.getString(R.string.nc_sent_a_gif_you) - } else { - String.format( - sharedApplication!!.resources.getString(R.string.nc_sent_a_gif), - getNullsafeActorDisplayName() - ) - } - } else if (MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == getCalculateMessageType()) { - return if (actorId == activeUser!!.userId) { - sharedApplication!!.getString(R.string.nc_sent_an_attachment_you) - } else { - String.format( - sharedApplication!!.resources.getString(R.string.nc_sent_an_attachment), - getNullsafeActorDisplayName() - ) - } - } else if (MessageType.SINGLE_NC_GEOLOCATION_MESSAGE == getCalculateMessageType()) { - return if (actorId == activeUser!!.userId) { - sharedApplication!!.getString(R.string.nc_sent_location_you) - } else { - String.format( - sharedApplication!!.resources.getString(R.string.nc_sent_location), - getNullsafeActorDisplayName() - ) - } - } else if (MessageType.VOICE_MESSAGE == getCalculateMessageType()) { - return if (actorId == activeUser!!.userId) { - sharedApplication!!.getString(R.string.nc_sent_voice_you) - } else { - String.format( - sharedApplication!!.resources.getString(R.string.nc_sent_voice), - getNullsafeActorDisplayName() - ) - } - } else if (MessageType.SINGLE_LINK_AUDIO_MESSAGE == getCalculateMessageType()) { - return if (actorId == activeUser!!.userId) { - sharedApplication!!.getString(R.string.nc_sent_an_audio_you) - } else { - String.format( - sharedApplication!!.resources.getString(R.string.nc_sent_an_audio), - getNullsafeActorDisplayName() - ) - } - } else if (MessageType.SINGLE_LINK_VIDEO_MESSAGE == getCalculateMessageType()) { - return if (actorId == activeUser!!.userId) { - sharedApplication!!.getString(R.string.nc_sent_a_video_you) - } else { - String.format( - sharedApplication!!.resources.getString(R.string.nc_sent_a_video), - getNullsafeActorDisplayName() - ) - } - } else if (MessageType.SINGLE_LINK_IMAGE_MESSAGE == getCalculateMessageType()) { - return if (actorId == activeUser!!.userId) { - sharedApplication!!.getString(R.string.nc_sent_an_image_you) - } else { - String.format( - sharedApplication!!.resources.getString(R.string.nc_sent_an_image), - getNullsafeActorDisplayName() - ) - } - } else if (MessageType.POLL_MESSAGE == getCalculateMessageType()) { - return if (actorId == activeUser!!.userId) { - sharedApplication!!.getString(R.string.nc_sent_poll_you) - } else { - String.format( - sharedApplication!!.resources.getString(R.string.nc_sent_poll), - getNullsafeActorDisplayName() - ) - } - } - } - return "" - } - - private fun getNullsafeActorDisplayName() = + fun getNullsafeActorDisplayName() = if (!TextUtils.isEmpty(actorDisplayName)) { actorDisplayName } else { diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/ChatRepository.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt similarity index 97% rename from app/src/main/java/com/nextcloud/talk/chat/data/ChatRepository.kt rename to app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt index bd85f5fb0..f18292917 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/ChatRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt @@ -4,7 +4,7 @@ * SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de> * SPDX-License-Identifier: GPL-3.0-or-later */ -package com.nextcloud.talk.chat.data +package com.nextcloud.talk.chat.data.network import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.domain.ConversationModel @@ -19,7 +19,7 @@ import io.reactivex.Observable import retrofit2.Response @Suppress("LongParameterList", "TooManyFunctions") -interface ChatRepository { +interface ChatNetworkDataSource { fun getRoom(user: User, roomToken: String): Observable<ConversationModel> fun getCapabilities(user: User, roomToken: String): Observable<SpreedCapability> fun joinRoom(user: User, roomToken: String, roomPassword: String): Observable<ConversationModel> 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 new file mode 100644 index 000000000..62fa68758 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt @@ -0,0 +1,667 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.chat.data.network + +import android.os.Bundle +import android.util.Log +import com.nextcloud.talk.chat.data.ChatMessageRepository +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.data.database.dao.ChatBlocksDao +import com.nextcloud.talk.data.database.dao.ChatMessagesDao +import com.nextcloud.talk.data.database.mappers.asEntity +import com.nextcloud.talk.data.database.mappers.asModel +import com.nextcloud.talk.data.database.model.ChatBlockEntity +import com.nextcloud.talk.data.database.model.ChatMessageEntity +import com.nextcloud.talk.data.network.NetworkMonitor +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.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import com.nextcloud.talk.utils.preferences.AppPreferences +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import javax.inject.Inject + +class OfflineFirstChatRepository @Inject constructor( + private val chatDao: ChatMessagesDao, + private val chatBlocksDao: ChatBlocksDao, + private val network: ChatNetworkDataSource, + private val datastore: AppPreferences, + private val monitor: NetworkMonitor, + private val userProvider: CurrentUserProviderNew +) : ChatMessageRepository { + + val currentUser: User = userProvider.currentUser.blockingGet() + + override val messageFlow: + Flow< + Pair< + Boolean, + List<ChatMessage> + > + > + get() = _messageFlow + + private val _messageFlow: + MutableSharedFlow< + Pair< + Boolean, + List<ChatMessage> + > + > = MutableSharedFlow() + + override val updateMessageFlow: + Flow< + Pair< + Boolean, + List<ChatMessage> + > + > + get() = _updateMessageFlow + + private val _updateMessageFlow: + MutableSharedFlow< + Pair< + Boolean, + List<ChatMessage> + > + > = MutableSharedFlow() + + private var newXChatLastCommonRead: Int? = null + private var itIsPaused = false + private val scope = CoroutineScope(Dispatchers.IO) + + lateinit var internalConversationId: String + private lateinit var conversationModel: ConversationModel + private lateinit var credentials: String + private lateinit var urlForChatting: String + + override fun setData( + conversationModel: ConversationModel, + credentials: String, + urlForChatting: String + ) { + this.conversationModel = conversationModel + this.credentials = credentials + this.urlForChatting = urlForChatting + // internalConversationId = userProvider.currentUser.blockingGet().id!!.toString() + "@" + conversationModel.token + internalConversationId = conversationModel.internalId + } + + override fun loadInitialMessages(withNetworkParams: Bundle): Job = + scope.launch { + Log.d(TAG, "---- loadInitialMessages ------------") + + val fieldMap = getFieldMap( + lookIntoFuture = false, + includeLastKnown = true, + setReadMarker = true, + lastKnown = null + ) + withNetworkParams.putSerializable(BundleKeys.KEY_FIELD_MAP, fieldMap) + withNetworkParams.putString(BundleKeys.KEY_ROOM_TOKEN, conversationModel.token) + + sync(withNetworkParams) + + Log.d(TAG, "newestMessageId after sync: " + chatDao.getNewestMessageId(internalConversationId)) + + showLast100MessagesBeforeAndEqual( + internalConversationId, + chatDao.getNewestMessageId(internalConversationId) + ) + + initMessagePolling() + } + + override fun loadMoreMessages( + beforeMessageId: Long, + roomToken: String, + withMessageLimit: Int, + withNetworkParams: Bundle + ): Job = + scope.launch { + Log.d(TAG, "---- loadMoreMessages for $beforeMessageId ------------") + + val fieldMap = getFieldMap( + lookIntoFuture = false, + includeLastKnown = false, + setReadMarker = true, + lastKnown = beforeMessageId.toInt() + ) + withNetworkParams.putSerializable(BundleKeys.KEY_FIELD_MAP, fieldMap) + // withNetworkParams.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken) + + val loadFromServer = hasToLoadPreviousMessagesFromServer(beforeMessageId) + + if (loadFromServer) { + if (monitor.isOnline.first()) { + sync(withNetworkParams) + } else { + // TODO: handle how user is informed about gaps when being offline. Something like: + // val offlineChatMessage = ChatMessage( + // message = "you are offline. Some messages might be missing here." + // ) + // val list = mutableListOf<ChatMessage>() + // list.add(offlineChatMessage) + // + // if (list.isNotEmpty()) { + // val pair = Pair(false, list) + // _messageFlow.emit(pair) + // } + } + } + + showLast100MessagesBefore(internalConversationId, beforeMessageId) + } + + override fun initMessagePolling(): Job = + scope.launch { + // monitor.isOnline.onEach { online -> + + Log.d(TAG, "---- initMessagePolling ------------") + + val initialMessageId = chatDao.getNewestMessageId(internalConversationId).toInt() + Log.d(TAG, "newestMessage: $initialMessageId") + + var fieldMap = getFieldMap( + lookIntoFuture = true, + includeLastKnown = false, + setReadMarker = true, + lastKnown = initialMessageId + ) + + val networkParams = Bundle() + + while (!itIsPaused) { + if (!monitor.isOnline.first()) Thread.sleep(500) + + // sync database with server ( This is a long blocking call b/c long polling is set ) + networkParams.putSerializable(BundleKeys.KEY_FIELD_MAP, fieldMap) + // withNetworkParams.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken) + + // this@OfflineFirstChatRepository.sync(withNetworkParams) + // sync(withNetworkParams) + + val resultsFromSync = sync(networkParams) + // TODO: load from DB?! at least make sure no changes are made here that are not saved to DB then! + + Log.d(TAG, "got result from longpolling") + if (!resultsFromSync.isNullOrEmpty()) { + val chatMessages = resultsFromSync.map(ChatMessageEntity::asModel) + val pair = Pair(true, chatMessages) + _messageFlow.emit(pair) + } + + // Process read status if not null + // val lastKnown = datastore.getLastKnownId(internalConversationId, 0) + // list = list.map { chatMessage -> + // chatMessage.readStatus = if (chatMessage.jsonMessageId <= lastKnown) { + // ReadStatus.READ + // } else { + // ReadStatus.SENT + // } + // + // return@map chatMessage + // } + + val newestMessage2 = chatDao.getNewestMessageId(internalConversationId).toInt() + Log.d(TAG, "newestMessage in loop: $newestMessage2") + + // update field map vars for next cycle + fieldMap = getFieldMap( + lookIntoFuture = true, + includeLastKnown = false, + setReadMarker = true, + lastKnown = newestMessage2 + ) + } + // }.flowOn(Dispatchers.IO).collect() + } + + private suspend fun hasToLoadPreviousMessagesFromServer( + beforeMessageId: Long + ): Boolean { + val loadFromServer: Boolean + + val blockForMessage = getBlockOfMessage(beforeMessageId.toInt()) + + if (blockForMessage == null) { + Log.d(TAG, "No blocks for this message were found so we have to ask server") + loadFromServer = true + } else if (!blockForMessage.hasHistory) { + Log.d(TAG, "The last chatBlock is reached so we won't request server for older messages") + loadFromServer = false + } else { + // we know that beforeMessageId and blockForMessage.oldestMessageId are in the same block. + // As we want the last 100 entries before beforeMessageId, we calculate if these messages are 100 + // entries apart from each other + + val amountBetween = chatDao.getCountBetweenMessageIds( + internalConversationId, + beforeMessageId, + blockForMessage.oldestMessageId + ) + loadFromServer = amountBetween < 100 + + Log.d( + TAG, "Amount between messageId " + beforeMessageId + " and " + blockForMessage.oldestMessageId + + " is: " + amountBetween + " so 'loadFromServer' is " + loadFromServer + ) + } + return loadFromServer + } + + private fun getFieldMap( + lookIntoFuture: Boolean, + includeLastKnown: Boolean, + setReadMarker: Boolean, + lastKnown: Int? + ): HashMap<String, Int> { + val fieldMap = HashMap<String, Int>() + + fieldMap["includeLastKnown"] = if (includeLastKnown) 1 else 0 + + if (lastKnown != null) { + fieldMap["lastKnownMessageId"] = lastKnown + } + + // newXChatLastCommonRead?.let { + // fieldMap["lastCommonReadId"] = if (it > 0) it else lastKnown + // } + + fieldMap["timeout"] = if (lookIntoFuture) 30 else 0 + fieldMap["limit"] = 100 + fieldMap["lookIntoFuture"] = if (lookIntoFuture) 1 else 0 + fieldMap["setReadMarker"] = if (setReadMarker) 1 else 0 + + return fieldMap + } + + private suspend fun getMessagesFrom(messageIds: List<Long>): List<ChatMessage> = + chatDao.getMessagesFromIds(messageIds).map { + it.map(ChatMessageEntity::asModel) + }.first() + + override suspend fun getMessage(messageId: Long, bundle: Bundle): + Flow<ChatMessage> { + + Log.d(TAG, "Get message with id $messageId") + val loadFromServer = hasToLoadPreviousMessagesFromServer(messageId) + + if (loadFromServer) { + + val fieldMap = getFieldMap( + lookIntoFuture = false, + includeLastKnown = true, + setReadMarker = false, + lastKnown = messageId.toInt() + ) + bundle.putSerializable(BundleKeys.KEY_FIELD_MAP, fieldMap) + + // Although only the single message will be returned, a server request will load 100 messages. + // If this turns out to be confusion for debugging we could load set the limit to 1 for this request. + sync(bundle) + } + return chatDao.getChatMessageForConversation(internalConversationId, messageId) + .map(ChatMessageEntity::asModel) + } + + @Suppress("UNCHECKED_CAST") + private fun getMessagesFromServer(bundle: Bundle): Pair<Int, List<ChatMessageJson>>? { + Log.d(TAG, "An online request is made!!!!!!!!!!!!!!!!!!!!") + // val credentials = bundle.getString(BundleKeys.KEY_CREDENTIALS) + // val url = bundle.getString(BundleKeys.KEY_CHAT_URL) + val fieldMap = bundle.getSerializable(BundleKeys.KEY_FIELD_MAP) as HashMap<String, Int> + + try { + val result = network.pullChatMessages(credentials, urlForChatting, fieldMap) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .map { + when (it.code()) { + HTTP_CODE_OK -> { + Log.d(TAG, "getMessagesFromServer HTTP_CODE_OK") + // newXChatLastCommonRead = it.headers()["X-Chat-Last-Common-Read"]?.let { + // Integer.parseInt(it) + // } + // + // val xChatLastGivenHeader: String? = it.headers()["X-Chat-Last-Given"] + // val lastKnownId = if (it.headers().size > 0 && + // xChatLastGivenHeader?.isNotEmpty() == true + // ) { + // xChatLastGivenHeader.toInt() + // } else { + // + // } + // + // // if (lastKnownId > 0) { + // datastore.saveLastKnownId(internalConversationId, lastKnownId) + // // } + + return@map Pair( + HTTP_CODE_OK, + (it.body() as ChatOverall).ocs!!.data!! + ) + } + + HTTP_CODE_NOT_MODIFIED -> { + Log.d(TAG, "getMessagesFromServer HTTP_CODE_NOT_MODIFIED") + + return@map Pair( + HTTP_CODE_NOT_MODIFIED, + listOf<ChatMessageJson>() + ) + } + + HTTP_CODE_PRECONDITION_FAILED -> { + Log.d(TAG, "getMessagesFromServer HTTP_CODE_PRECONDITION_FAILED") + + return@map Pair( + HTTP_CODE_PRECONDITION_FAILED, + listOf<ChatMessageJson>() + ) + } + + else -> { + return@map Pair( + HTTP_CODE_PRECONDITION_FAILED, + listOf<ChatMessageJson>() + ) + } + } + } + .blockingSingle() + return result + } catch (e: Exception) { + Log.e(TAG, "some exception", e) + } + return null + } + + private suspend fun sync(bundle: Bundle): List<ChatMessageEntity>? { + val result = getMessagesFromServer(bundle) ?: return listOf() + var chatMessagesFromSync: List<ChatMessageEntity>? = null + + val fieldMap = bundle.getSerializable(BundleKeys.KEY_FIELD_MAP) as HashMap<String, Int> + val queriedMessageId = fieldMap["lastKnownMessageId"] + val lookIntoFuture = fieldMap["lookIntoFuture"] == 1 + + val statusCode = result.first + // val statusCode = result.first + + val hasHistory = getHasHistory(statusCode, lookIntoFuture) + + Log.d( + TAG, + "internalConv=$internalConversationId statusCode=$statusCode lookIntoFuture=$lookIntoFuture " + + "hasHistory=$hasHistory " + + "queriedMessageId=$queriedMessageId" + ) + + val blockContainingQueriedMessage: ChatBlockEntity? = getBlockOfMessage(queriedMessageId) + + if (blockContainingQueriedMessage != null && !hasHistory) { + blockContainingQueriedMessage.hasHistory = false + chatBlocksDao.upsertChatBlock(blockContainingQueriedMessage) + Log.d(TAG, "End of chat was reached so hasHistory=false is set") + } + + if (result.second.isNotEmpty()) { + val chatMessagesJson = result.second + + if (lookIntoFuture) { + handleUpdateMessages(chatMessagesJson) + } + + chatMessagesFromSync = chatMessagesJson.map { + it.asEntity(currentUser.id!!) + } + + chatDao.upsertChatMessages(chatMessagesFromSync) + + val oldestIdFromSync = chatMessagesFromSync.minByOrNull { it.id }!!.id + val newestIdFromSync = chatMessagesFromSync.maxByOrNull { it.id }!!.id + Log.d(TAG, "oldestIdFromSync: $oldestIdFromSync") + Log.d(TAG, "newestIdFromSync: $newestIdFromSync") + + var oldestMessageIdForNewChatBlock = oldestIdFromSync + var newestMessageIdForNewChatBlock = newestIdFromSync + + if (blockContainingQueriedMessage != null) { + if (lookIntoFuture) { + val oldestMessageIdFromBlockOfQueriedMessage = blockContainingQueriedMessage.oldestMessageId + Log.d(TAG, "oldestMessageIdFromBlockOfQueriedMessage: $oldestMessageIdFromBlockOfQueriedMessage") + oldestMessageIdForNewChatBlock = oldestMessageIdFromBlockOfQueriedMessage + } else { + val newestMessageIdFromBlockOfQueriedMessage = blockContainingQueriedMessage.newestMessageId + Log.d(TAG, "newestMessageIdFromBlockOfQueriedMessage: $newestMessageIdFromBlockOfQueriedMessage") + newestMessageIdForNewChatBlock = newestMessageIdFromBlockOfQueriedMessage + } + } + + Log.d(TAG, "oldestMessageIdForNewChatBlock: $oldestMessageIdForNewChatBlock") + Log.d(TAG, "newestMessageIdForNewChatBlock: $newestMessageIdForNewChatBlock") + + val newChatBlock = ChatBlockEntity( + internalConversationId = internalConversationId, + oldestMessageId = oldestMessageIdForNewChatBlock, + newestMessageId = newestMessageIdForNewChatBlock, + hasHistory = hasHistory + ) + chatBlocksDao.upsertChatBlock(newChatBlock) + + updateBlocks(newChatBlock) + } else { + Log.d(TAG, "no data is updated...") + } + + return chatMessagesFromSync + } + + private suspend fun handleUpdateMessages(messagesJson: List<ChatMessageJson>) { + messagesJson.forEach { messageJson -> + when (messageJson.systemMessageType) { + ChatMessage.SystemMessageType.REACTION -> { + messageJson.parentMessage?.let { parentMessageJson -> + val parentMessageEntity = parentMessageJson.asEntity(currentUser.id!!) + chatDao.upsertChatMessage(parentMessageEntity) + // TODO: inform UI to update this message!! + + val pair = Pair(true, listOf(parentMessageEntity.asModel())) + _updateMessageFlow.emit(pair) + } + } + + ChatMessage.SystemMessageType.REACTION_REVOKED -> { + // TODO + } + + ChatMessage.SystemMessageType.REACTION_DELETED -> { + // TODO + } + + ChatMessage.SystemMessageType.MESSAGE_DELETED -> { + // TODO + } + + ChatMessage.SystemMessageType.POLL_VOTED -> { + // TODO + } + + ChatMessage.SystemMessageType.MESSAGE_EDITED -> { + // TODO + } + + ChatMessage.SystemMessageType.CLEARED_CHAT -> { + val pattern = "$internalConversationId%" // LIKE "<accountId>@<conversationId>@%" + chatDao.clearAllMessagesForUser(pattern) + } + + else -> {} + } + } + } + + /** + * 304 is returned when oldest message of chat was queried or when long polling request returned with no + * modification. hasHistory is only set to false, when 304 was returned for the the oldest message + */ + private fun getHasHistory(statusCode: Int, lookIntoFuture: Boolean): Boolean { + return if (statusCode == HTTP_CODE_NOT_MODIFIED) { + lookIntoFuture + } else { + true + } + } + + private suspend fun getBlockOfMessage(queriedMessageId: Int?): ChatBlockEntity? { + var blockContainingQueriedMessage: ChatBlockEntity? = null + if (queriedMessageId != null) { + val blocksContainingQueriedMessage = + chatBlocksDao.getChatBlocksContainingMessageId(internalConversationId, queriedMessageId.toLong()) + + val chatBlocks = blocksContainingQueriedMessage.first() + if (chatBlocks.size > 1) { + Log.w(TAG, "multiple chat blocks with messageId $queriedMessageId were found") + } + + blockContainingQueriedMessage = if (chatBlocks.isNotEmpty()) { + chatBlocks.first() + } else { + null + } + } + return blockContainingQueriedMessage + } + + private suspend fun updateBlocks(chatBlock: ChatBlockEntity): ChatBlockEntity? { + val connectedChatBlocks = + chatBlocksDao.getConnectedChatBlocks( + internalConversationId, + chatBlock.oldestMessageId, + chatBlock.newestMessageId + ).first() + + if (connectedChatBlocks.size == 1) { + Log.d(TAG, "This chatBlock is not connected to others") + val chatBlockFromDb = connectedChatBlocks[0] + Log.d(TAG, "chatBlockFromDb.oldestMessageId: " + chatBlockFromDb.oldestMessageId) + Log.d(TAG, "chatBlockFromDb.newestMessageId: " + chatBlockFromDb.newestMessageId) + return chatBlockFromDb + } else if (connectedChatBlocks.size > 1) { + Log.d(TAG, "Found " + connectedChatBlocks.size + " chat blocks that are connected") + val oldestIdFromDbChatBlocks = + connectedChatBlocks.minByOrNull { it.oldestMessageId }!!.oldestMessageId + val newestIdFromDbChatBlocks = + connectedChatBlocks.maxByOrNull { it.newestMessageId }!!.newestMessageId + + val hasNoHistory = connectedChatBlocks.any { !it.hasHistory } + val hasHistory = !hasNoHistory + Log.d(TAG, "hasHistory = $hasHistory") + + chatBlocksDao.deleteChatBlocks(connectedChatBlocks) + Log.d(TAG, "These chat blocks were deleted") + + val newChatBlock = ChatBlockEntity( + internalConversationId = internalConversationId, + oldestMessageId = oldestIdFromDbChatBlocks, + newestMessageId = newestIdFromDbChatBlocks, + hasHistory = hasHistory + ) + chatBlocksDao.upsertChatBlock(newChatBlock) + Log.d(TAG, "A new chat block was created that covers all the range of the found chatblocks") + Log.d(TAG, "new chatBlock - oldest MessageId: $oldestIdFromDbChatBlocks") + Log.d(TAG, "new chatBlock - newest MessageId: $newestIdFromDbChatBlocks") + return newChatBlock + } else { + Log.d(TAG, "No chat block found ....") + return null + } + } + + private suspend fun showLast100MessagesBeforeAndEqual(internalConversationId: String, messageId: Long) { + suspend fun getMessagesBeforeAndEqual( + messageId: Long, + internalConversationId: String, + messageLimit: Int + ): List<ChatMessage> = + chatDao.getMessagesForConversationBeforeAndEqual( + internalConversationId, + messageId, + messageLimit + ).map { + it.map(ChatMessageEntity::asModel) + }.first() + + val list = getMessagesBeforeAndEqual( + messageId, + internalConversationId, + 100 + ) + + if (list.isNotEmpty()) { + val pair = Pair(false, list) + _messageFlow.emit(pair) + } + } + + private suspend fun showLast100MessagesBefore(internalConversationId: String, messageId: Long) { + suspend fun getMessagesBefore( + messageId: Long, + internalConversationId: String, + messageLimit: Int + ): List<ChatMessage> = + chatDao.getMessagesForConversationBefore( + internalConversationId, + messageId, + messageLimit + ).map { + it.map(ChatMessageEntity::asModel) + }.first() + + val list = getMessagesBefore( + messageId, + internalConversationId, + 100 + ) + + if (list.isNotEmpty()) { + val pair = Pair(false, list) + _messageFlow.emit(pair) + } + } + + override fun handleOnPause() { + itIsPaused = true + } + + override fun handleOnResume() { + itIsPaused = false + } + + override fun handleOnStop() { + // unused atm + } + + companion object { + val TAG = OfflineFirstChatRepository::class.simpleName + private const val HTTP_CODE_OK: Int = 200 + private const val HTTP_CODE_NOT_MODIFIED = 304 + private const val HTTP_CODE_PRECONDITION_FAILED = 412 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/network/NetworkChatRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt similarity index 97% rename from app/src/main/java/com/nextcloud/talk/chat/data/network/NetworkChatRepositoryImpl.kt rename to app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt index b922f6c2c..774a6d423 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/network/NetworkChatRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt @@ -7,7 +7,6 @@ package com.nextcloud.talk.chat.data.network import com.nextcloud.talk.api.NcApi -import com.nextcloud.talk.chat.data.ChatRepository import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.json.capabilities.SpreedCapability @@ -21,7 +20,7 @@ import com.nextcloud.talk.utils.ApiUtils import io.reactivex.Observable import retrofit2.Response -class NetworkChatRepositoryImpl(private val ncApi: NcApi) : ChatRepository { +class RetrofitChatNetwork(private val ncApi: NcApi) : ChatNetworkDataSource { override fun getRoom(user: User, roomToken: String): Observable<ConversationModel> { val credentials: String = ApiUtils.getCredentials(user.username, user.token)!! val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1)) @@ -29,7 +28,7 @@ class NetworkChatRepositoryImpl(private val ncApi: NcApi) : ChatRepository { return ncApi.getRoom( credentials, ApiUtils.getUrlForRoom(apiVersion, user.baseUrl!!, roomToken) - ).map { ConversationModel.mapToConversationModel(it.ocs?.data!!) } + ).map { ConversationModel.mapToConversationModel(it.ocs?.data!!, user) } } override fun getCapabilities(user: User, roomToken: String): Observable<SpreedCapability> { @@ -50,7 +49,7 @@ class NetworkChatRepositoryImpl(private val ncApi: NcApi) : ChatRepository { credentials, ApiUtils.getUrlForParticipantsActive(apiVersion, user.baseUrl!!, roomToken), roomPassword - ).map { ConversationModel.mapToConversationModel(it.ocs?.data!!) } + ).map { ConversationModel.mapToConversationModel(it.ocs?.data!!, user) } } override fun setReminder( 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 58eaa463a..f97d27196 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 @@ -8,22 +8,25 @@ package com.nextcloud.talk.chat.viewmodels import android.content.Context import android.net.Uri +import android.os.Bundle import android.util.Log import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import com.nextcloud.talk.chat.data.ChatRepository +import com.nextcloud.talk.chat.data.ChatMessageRepository import com.nextcloud.talk.chat.data.io.AudioFocusRequestManager import com.nextcloud.talk.chat.data.io.MediaRecorderManager +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource +import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.jobs.UploadAndShareFilesWorker import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.domain.ReactionAddedModel import com.nextcloud.talk.models.domain.ReactionDeletedModel import com.nextcloud.talk.models.json.capabilities.SpreedCapability -import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage import com.nextcloud.talk.models.json.conversations.RoomOverall import com.nextcloud.talk.models.json.conversations.RoomsOverall @@ -31,20 +34,30 @@ import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.reminder.Reminder import com.nextcloud.talk.repositories.reactions.ReactionsRepository import com.nextcloud.talk.utils.ConversationUtils +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew import io.reactivex.Observer import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers -import retrofit2.Response +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.onEach import java.io.File import javax.inject.Inject @Suppress("TooManyFunctions", "LongParameterList") class ChatViewModel @Inject constructor( - private val chatRepository: ChatRepository, + // should be removed here. Use it via RetrofitChatNetwork + private val chatNetworkDataSource: ChatNetworkDataSource, + private val chatRepository: ChatMessageRepository, + private val conversationRepository: OfflineConversationsRepository, private val reactionsRepository: ReactionsRepository, private val mediaRecorderManager: MediaRecorderManager, - private val audioFocusRequestManager: AudioFocusRequestManager + private val audioFocusRequestManager: AudioFocusRequestManager, + private val userProvider: CurrentUserProviderNew ) : ViewModel(), DefaultLifecycleObserver { enum class LifeCycleFlag { @@ -52,6 +65,7 @@ class ChatViewModel @Inject constructor( RESUMED, STOPPED } + lateinit var currentLifeCycleFlag: LifeCycleFlag val disposableSet = mutableSetOf<Disposable>() @@ -59,6 +73,7 @@ class ChatViewModel @Inject constructor( super.onResume(owner) currentLifeCycleFlag = LifeCycleFlag.RESUMED mediaRecorderManager.handleOnResume() + chatRepository.handleOnResume() } override fun onPause(owner: LifecycleOwner) { @@ -67,13 +82,16 @@ class ChatViewModel @Inject constructor( disposableSet.forEach { disposable -> disposable.dispose() } disposableSet.clear() mediaRecorderManager.handleOnPause() + chatRepository.handleOnPause() } override fun onStop(owner: LifecycleOwner) { super.onStop(owner) currentLifeCycleFlag = LifeCycleFlag.STOPPED mediaRecorderManager.handleOnStop() + chatRepository.handleOnStop() } + val getAudioFocusChange: LiveData<AudioFocusRequestManager.ManagerState> get() = audioFocusRequestManager.getManagerState @@ -89,9 +107,26 @@ class ChatViewModel @Inject constructor( val getVoiceRecordingLocked: LiveData<Boolean> get() = _getVoiceRecordingLocked - private val _getFieldMapForChat: MutableLiveData<HashMap<String, Int>> = MutableLiveData() - val getFieldMapForChat: LiveData<HashMap<String, Int>> - get() = _getFieldMapForChat + val getMessageFlow = chatRepository.messageFlow + .onEach { + _chatMessageViewState.value = if (_chatMessageViewState.value == ChatMessageInitialState) { + ChatMessageStartState + } else { + ChatMessageUpdateState + } + }.catch { + _chatMessageViewState.value = ChatMessageErrorState + } + + val getUpdateMessageFlow = chatRepository.updateMessageFlow + + val getConversationFlow = conversationRepository.conversationFlow + .onEach { + _getRoomViewState.value = GetRoomSuccessState + }.catch { + _getRoomViewState.value = GetRoomErrorState + } + sealed interface ViewState object GetReminderStartState : ViewState @@ -111,7 +146,7 @@ class ChatViewModel @Inject constructor( object GetRoomStartState : ViewState object GetRoomErrorState : ViewState - open class GetRoomSuccessState(val conversationModel: ConversationModel) : ViewState + object GetRoomSuccessState : ViewState private val _getRoomViewState: MutableLiveData<ViewState> = MutableLiveData(GetRoomStartState) val getRoomViewState: LiveData<ViewState> @@ -136,28 +171,24 @@ class ChatViewModel @Inject constructor( object LeaveRoomStartState : ViewState class LeaveRoomSuccessState(val funToCallWhenLeaveSuccessful: (() -> Unit)?) : ViewState + private val _leaveRoomViewState: MutableLiveData<ViewState> = MutableLiveData(LeaveRoomStartState) val leaveRoomViewState: LiveData<ViewState> get() = _leaveRoomViewState - object SendChatMessageStartState : ViewState - class SendChatMessageSuccessState(val message: CharSequence) : ViewState - class SendChatMessageErrorState(val e: Throwable, val message: CharSequence) : ViewState - private val _sendChatMessageViewState: MutableLiveData<ViewState> = MutableLiveData(SendChatMessageStartState) - val sendChatMessageViewState: LiveData<ViewState> - get() = _sendChatMessageViewState + object ChatMessageInitialState : ViewState + object ChatMessageStartState : ViewState + object ChatMessageUpdateState : ViewState + object ChatMessageErrorState : ViewState - object PullChatMessageStartState : ViewState - class PullChatMessageSuccessState(val response: Response<*>, val lookIntoFuture: Boolean) : ViewState - object PullChatMessageErrorState : ViewState - object PullChatMessageCompleteState : ViewState - private val _pullChatMessageViewState: MutableLiveData<ViewState> = MutableLiveData(PullChatMessageStartState) - val pullChatMessageViewState: LiveData<ViewState> - get() = _pullChatMessageViewState + private val _chatMessageViewState: MutableLiveData<ViewState> = MutableLiveData(ChatMessageInitialState) + val chatMessageViewState: LiveData<ViewState> + get() = _chatMessageViewState object DeleteChatMessageStartState : ViewState class DeleteChatMessageSuccessState(val msg: ChatOverallSingleMessage) : ViewState object DeleteChatMessageErrorState : ViewState + private val _deleteChatMessageViewState: MutableLiveData<ViewState> = MutableLiveData(DeleteChatMessageStartState) val deleteChatMessageViewState: LiveData<ViewState> get() = _deleteChatMessageViewState @@ -172,29 +203,38 @@ class ChatViewModel @Inject constructor( object ReactionAddedStartState : ViewState class ReactionAddedSuccessState(val reactionAddedModel: ReactionAddedModel) : ViewState + private val _reactionAddedViewState: MutableLiveData<ViewState> = MutableLiveData(ReactionAddedStartState) val reactionAddedViewState: LiveData<ViewState> get() = _reactionAddedViewState object ReactionDeletedStartState : ViewState class ReactionDeletedSuccessState(val reactionDeletedModel: ReactionDeletedModel) : ViewState + private val _reactionDeletedViewState: MutableLiveData<ViewState> = MutableLiveData(ReactionDeletedStartState) val reactionDeletedViewState: LiveData<ViewState> get() = _reactionDeletedViewState - fun refreshChatParams(pullChatMessagesFieldMap: HashMap<String, Int>, overrideRefresh: Boolean = false) { - if (pullChatMessagesFieldMap != _getFieldMapForChat.value || overrideRefresh) { - _getFieldMapForChat.postValue(pullChatMessagesFieldMap) - Log.d(TAG, "FieldMap Refreshed with $pullChatMessagesFieldMap vs ${_getFieldMapForChat.value}") - } + fun setData( + conversationModel: ConversationModel, + credentials: String, + urlForChatting: String + ) { + chatRepository.setData( + conversationModel, + credentials, + urlForChatting + ) } fun getRoom(user: User, token: String) { _getRoomViewState.value = GetRoomStartState - chatRepository.getRoom(user, token) - .subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.subscribe(GetRoomObserver()) + conversationRepository.getConversationSettings(token) + + // chatNetworkDataSource.getRoom(user, token) + // .subscribeOn(Schedulers.io()) + // ?.observeOn(AndroidSchedulers.mainThread()) + // ?.subscribe(GetRoomObserver()) } fun getCapabilities(user: User, token: String, conversationModel: ConversationModel) { @@ -208,7 +248,7 @@ class ChatViewModel @Inject constructor( _getCapabilitiesViewState.value = GetCapabilitiesUpdateState(user.capabilities!!.spreedCapability!!) } } else { - chatRepository.getCapabilities(user, token) + chatNetworkDataSource.getCapabilities(user, token) .subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread()) ?.subscribe(object : Observer<SpreedCapability> { @@ -238,7 +278,7 @@ class ChatViewModel @Inject constructor( fun joinRoom(user: User, token: String, roomPassword: String) { _joinRoomViewState.value = JoinRoomStartState - chatRepository.joinRoom(user, token, roomPassword) + chatNetworkDataSource.joinRoom(user, token, roomPassword) .subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread()) ?.retry(JOIN_ROOM_RETRY_COUNT) @@ -246,21 +286,21 @@ class ChatViewModel @Inject constructor( } fun setReminder(user: User, roomToken: String, messageId: String, timestamp: Int, chatApiVersion: Int) { - chatRepository.setReminder(user, roomToken, messageId, timestamp, chatApiVersion) + chatNetworkDataSource.setReminder(user, roomToken, messageId, timestamp, chatApiVersion) .subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread()) ?.subscribe(SetReminderObserver()) } fun getReminder(user: User, roomToken: String, messageId: String, chatApiVersion: Int) { - chatRepository.getReminder(user, roomToken, messageId, chatApiVersion) + chatNetworkDataSource.getReminder(user, roomToken, messageId, chatApiVersion) .subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread()) ?.subscribe(GetReminderObserver()) } fun deleteReminder(user: User, roomToken: String, messageId: String, chatApiVersion: Int) { - chatRepository.deleteReminder(user, roomToken, messageId, chatApiVersion) + chatNetworkDataSource.deleteReminder(user, roomToken, messageId, chatApiVersion) .subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread()) ?.subscribe(object : Observer<GenericOverall> { @@ -284,7 +324,7 @@ class ChatViewModel @Inject constructor( fun leaveRoom(credentials: String, url: String, funToCallWhenLeaveSuccessful: (() -> Unit)?) { val startNanoTime = System.nanoTime() - chatRepository.leaveRoom(credentials, url) + chatNetworkDataSource.leaveRoom(credentials, url) .subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread()) ?.subscribe(object : Observer<GenericOverall> { @@ -309,7 +349,7 @@ class ChatViewModel @Inject constructor( } fun createRoom(credentials: String, url: String, queryMap: Map<String, String>) { - chatRepository.createRoom(credentials, url, queryMap) + chatNetworkDataSource.createRoom(credentials, url, queryMap) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(object : Observer<RoomOverall> { @@ -332,72 +372,42 @@ class ChatViewModel @Inject constructor( }) } - fun sendChatMessage( - credentials: String, - url: String, - message: CharSequence, - displayName: String, - replyTo: Int, - sendWithoutNotification: Boolean + fun loadMessages(withCredentials: String, withUrl: String) { + val bundle = Bundle() + bundle.putString(BundleKeys.KEY_CHAT_URL, withUrl) + bundle.putString(BundleKeys.KEY_CREDENTIALS, withCredentials) + chatRepository.loadInitialMessages( + withNetworkParams = bundle + ) + } + + fun loadMoreMessages( + beforeMessageId: Long, + roomToken: String, + withMessageLimit: Int, + withCredentials: String, + withUrl: String ) { - chatRepository.sendChatMessage( - credentials, - url, - message, - displayName, - replyTo, - sendWithoutNotification - ).subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.subscribe(object : Observer<GenericOverall> { - override fun onSubscribe(d: Disposable) { - disposableSet.add(d) - } - - override fun onError(e: Throwable) { - _sendChatMessageViewState.value = SendChatMessageErrorState(e, message) - } - - override fun onComplete() { - // unused atm - } - - override fun onNext(t: GenericOverall) { - _sendChatMessageViewState.value = SendChatMessageSuccessState(message) - } - }) + val bundle = Bundle() + bundle.putString(BundleKeys.KEY_CHAT_URL, withUrl) + bundle.putString(BundleKeys.KEY_CREDENTIALS, withCredentials) + chatRepository.loadMoreMessages( + beforeMessageId, + roomToken, + withMessageLimit, + withNetworkParams = bundle + ) } - fun pullChatMessages(credentials: String, url: String) { - chatRepository.pullChatMessages(credentials, url, _getFieldMapForChat.value!!) - .subscribeOn(Schedulers.io()) - .takeUntil { (currentLifeCycleFlag == LifeCycleFlag.PAUSED) } - ?.observeOn(AndroidSchedulers.mainThread()) - ?.subscribe(object : Observer<Response<*>> { - override fun onSubscribe(d: Disposable) { - Log.d(TAG, "pullChatMessages - pullChatMessages SUBSCRIBE") - disposableSet.add(d) - } - - override fun onError(e: Throwable) { - Log.e(TAG, "pullChatMessages - pullChatMessages ERROR", e) - _pullChatMessageViewState.value = PullChatMessageErrorState - } - - override fun onComplete() { - Log.d(TAG, "pullChatMessages - pullChatMessages COMPLETE") - _pullChatMessageViewState.value = PullChatMessageCompleteState - } - - override fun onNext(response: Response<*>) { - val lookIntoFuture = getFieldMapForChat.value?.get("lookIntoFuture") == 1 - _pullChatMessageViewState.value = PullChatMessageSuccessState(response, lookIntoFuture) - } - }) - } + // fun initMessagePolling(withCredentials: String, withUrl: String, roomToken: String) { + // val bundle = Bundle() + // bundle.putString(BundleKeys.KEY_CHAT_URL, withUrl) + // bundle.putString(BundleKeys.KEY_CREDENTIALS, withCredentials) + // chatRepository.initMessagePolling(roomToken, withNetworkParams = bundle) + // } fun deleteChatMessages(credentials: String, url: String, messageId: String) { - chatRepository.deleteChatMessage(credentials, url) + chatNetworkDataSource.deleteChatMessage(credentials, url) .subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread()) ?.subscribe(object : Observer<ChatOverallSingleMessage> { @@ -426,7 +436,7 @@ class ChatViewModel @Inject constructor( } fun setChatReadMarker(credentials: String, url: String, previousMessageId: Int) { - chatRepository.setChatReadMarker(credentials, url, previousMessageId) + chatNetworkDataSource.setChatReadMarker(credentials, url, previousMessageId) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(object : Observer<GenericOverall> { @@ -449,7 +459,7 @@ class ChatViewModel @Inject constructor( } fun shareToNotes(credentials: String, url: String, message: String, displayName: String) { - chatRepository.shareToNotes(credentials, url, message, displayName) + chatNetworkDataSource.shareToNotes(credentials, url, message, displayName) .subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread()) ?.subscribe(object : Observer<GenericOverall> { @@ -472,13 +482,13 @@ class ChatViewModel @Inject constructor( } fun checkForNoteToSelf(credentials: String, baseUrl: String, includeStatus: Boolean) { - chatRepository.checkForNoteToSelf(credentials, baseUrl, includeStatus).subscribeOn(Schedulers.io()) + chatNetworkDataSource.checkForNoteToSelf(credentials, baseUrl, includeStatus).subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread()) ?.subscribe(CheckForNoteToSelfObserver()) } fun shareLocationToNotes(credentials: String, url: String, objectType: String, objectId: String, metadata: String) { - chatRepository.shareLocationToNotes(credentials, url, objectType, objectId, metadata) + chatNetworkDataSource.shareLocationToNotes(credentials, url, objectType, objectId, metadata) .subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread()) ?.subscribe(object : Observer<GenericOverall> { @@ -575,6 +585,7 @@ class ChatViewModel @Inject constructor( uploadFile(uri.toString(), room, displayName, metaData) } } + fun stopAndDiscardAudioRecording() { stopAudioRecording() Log.d(TAG, "File discarded") @@ -619,24 +630,38 @@ class ChatViewModel @Inject constructor( _getCapabilitiesViewState.value = GetCapabilitiesStartState } - inner class GetRoomObserver : Observer<ConversationModel> { - override fun onSubscribe(d: Disposable) { - // unused atm + suspend fun getMessageById(url: String, conversationModel: ConversationModel, messageId: Long): Flow<ChatMessage> = + flow { + val bundle = Bundle() + bundle.putString(BundleKeys.KEY_CHAT_URL, url) + bundle.putString( + BundleKeys.KEY_CREDENTIALS, + userProvider.currentUser.blockingGet().getCredentials() + ) + bundle.putString(BundleKeys.KEY_ROOM_TOKEN, conversationModel.token!!) + + val message = chatRepository.getMessage(messageId, bundle) + emit(message.first()) } - override fun onNext(conversationModel: ConversationModel) { - _getRoomViewState.value = GetRoomSuccessState(conversationModel) - } - - override fun onError(e: Throwable) { - Log.e(TAG, "Error when fetching room") - _getRoomViewState.value = GetRoomErrorState - } - - override fun onComplete() { - // unused atm - } - } +// inner class GetRoomObserver : Observer<ConversationModel> { +// override fun onSubscribe(d: Disposable) { +// // unused atm +// } +// +// override fun onNext(conversationModel: ConversationModel) { +// _getRoomViewState.value = GetRoomSuccessState(conversationModel) +// } +// +// override fun onError(e: Throwable) { +// Log.e(TAG, "Error when fetching room") +// _getRoomViewState.value = GetRoomErrorState +// } +// +// override fun onComplete() { +// // unused atm +// } +// } inner class JoinRoomObserver : Observer<ConversationModel> { override fun onSubscribe(d: Disposable) { @@ -704,7 +729,7 @@ class ChatViewModel @Inject constructor( rooms?.let { try { val noteToSelf = rooms.first { - val model = ConversationModel.mapToConversationModel(it) + val model = ConversationModel.mapToConversationModel(it, userProvider.currentUser.blockingGet()) ConversationUtils.isNoteToSelfConversation(model) } _getNoteToSelfAvaliability.value = NoteToSelfAvaliableState(noteToSelf.token!!) 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 509268ac8..4489ca830 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 @@ -14,12 +14,13 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import com.nextcloud.talk.chat.data.ChatRepository 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.network.ChatNetworkDataSource import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.utils.preferences.AppPreferences import com.stfalcon.chatkit.commons.models.IMessage import io.reactivex.Observer import io.reactivex.android.schedulers.AndroidSchedulers @@ -28,10 +29,11 @@ import io.reactivex.schedulers.Schedulers import javax.inject.Inject class MessageInputViewModel @Inject constructor( - private val chatRepository: ChatRepository, + private val chatNetworkDataSource: ChatNetworkDataSource, private val audioRecorderManager: AudioRecorderManager, private val mediaPlayerManager: MediaPlayerManager, - private val audioFocusRequestManager: AudioFocusRequestManager + private val audioFocusRequestManager: AudioFocusRequestManager, + private val dataStore: AppPreferences ) : ViewModel(), DefaultLifecycleObserver { enum class LifeCycleFlag { PAUSED, @@ -41,6 +43,16 @@ class MessageInputViewModel @Inject constructor( lateinit var currentLifeCycleFlag: LifeCycleFlag val disposableSet = mutableSetOf<Disposable>() + data class QueuedMessage( + val message: CharSequence? = null, + val displayName: String? = null, + val replyTo: Int? = null, + val sendWithoutNotification: Boolean? = null + ) + + private var isQueueing: Boolean = false + private val messageQueue: MutableList<QueuedMessage> = mutableListOf() + override fun onResume(owner: LifecycleOwner) { super.onResume(owner) currentLifeCycleFlag = LifeCycleFlag.RESUMED @@ -109,6 +121,7 @@ class MessageInputViewModel @Inject constructor( @Suppress("LongParameterList") fun sendChatMessage( + roomToken: String, credentials: String, url: String, message: CharSequence, @@ -116,7 +129,13 @@ class MessageInputViewModel @Inject constructor( replyTo: Int, sendWithoutNotification: Boolean ) { - chatRepository.sendChatMessage( + if (isQueueing) { + messageQueue.add(QueuedMessage(message, displayName, replyTo, sendWithoutNotification)) + dataStore.saveMessageQueue(roomToken, messageQueue) + return + } + + chatNetworkDataSource.sendChatMessage( credentials, url, message, @@ -145,7 +164,7 @@ class MessageInputViewModel @Inject constructor( } fun editChatMessage(credentials: String, url: String, text: String) { - chatRepository.editChatMessage(credentials, url, text) + chatNetworkDataSource.editChatMessage(credentials, url, text) .subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread()) ?.subscribe(object : Observer<ChatOverallSingleMessage> { @@ -216,4 +235,28 @@ class MessageInputViewModel @Inject constructor( fun setRecordingTime(time: Long) { _getRecordingTime.postValue(time) } + + fun sendAndEmptyMessageQueue(roomToken: String, credentials: String, url: String) { + if (isQueueing) return + messageQueue.clear() + + val queue = dataStore.getMessageQueue(roomToken) + dataStore.saveMessageQueue(roomToken, null) // empties the queue + while (queue.size > 0) { + val msg = queue.removeFirst() + sendChatMessage( + roomToken, + credentials, + url, + msg.message!!, + msg.displayName!!, + msg.replyTo!!, + msg.sendWithoutNotification!! + ) + } + } + + fun switchToMessageQueue(shouldQueue: Boolean) { + isQueueing = shouldQueue + } } diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt index ffcb08c4f..ae9ac6c2e 100644 --- a/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt @@ -46,7 +46,7 @@ import com.nextcloud.talk.jobs.AddParticipantsToConversation import com.nextcloud.talk.models.RetrofitBucket import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser -import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.models.json.conversations.ConversationEnums import com.nextcloud.talk.models.json.conversations.RoomOverall import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter import com.nextcloud.talk.models.json.participants.Participant @@ -288,10 +288,10 @@ class ContactsActivity : // if there are more participants to add, ask for roomName and add them one after another } else { - val roomType: Conversation.ConversationType = if (isPublicCall) { - Conversation.ConversationType.ROOM_PUBLIC_CALL + val roomType: ConversationEnums.ConversationType = if (isPublicCall) { + ConversationEnums.ConversationType.ROOM_PUBLIC_CALL } else { - Conversation.ConversationType.ROOM_GROUP_CALL + ConversationEnums.ConversationType.ROOM_GROUP_CALL } val userIdsArray = ArrayList(selectedUserIds) val groupIdsArray = ArrayList(selectedGroupIds) @@ -415,7 +415,7 @@ class ContactsActivity : searchView!!.inputType = InputType.TYPE_TEXT_VARIATION_FILTER var imeOptions: Int = EditorInfo.IME_ACTION_DONE or EditorInfo.IME_FLAG_NO_FULLSCREEN if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && - appPreferences?.isKeyboardIncognito == true + appPreferences.isKeyboardIncognito == true ) { imeOptions = imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING } diff --git a/app/src/main/java/com/nextcloud/talk/conversation/CreateConversationDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/conversation/CreateConversationDialogFragment.kt index aa439c11f..94f12e5b2 100644 --- a/app/src/main/java/com/nextcloud/talk/conversation/CreateConversationDialogFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/conversation/CreateConversationDialogFragment.kt @@ -37,7 +37,7 @@ import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.conversation.viewmodel.ConversationViewModel import com.nextcloud.talk.databinding.DialogCreateConversationBinding import com.nextcloud.talk.jobs.AddParticipantsToConversation -import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.models.json.conversations.ConversationEnums import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew @@ -66,7 +66,7 @@ class CreateConversationDialogFragment : DialogFragment() { private var emojiPopup: EmojiPopup? = null - private var conversationType: Conversation.ConversationType? = null + private var conversationType: ConversationEnums.ConversationType? = null private var usersToInvite: ArrayList<String> = ArrayList() private var groupsToInvite: ArrayList<String> = ArrayList() private var emailsToInvite: ArrayList<String> = ArrayList() diff --git a/app/src/main/java/com/nextcloud/talk/conversation/repository/ConversationRepository.kt b/app/src/main/java/com/nextcloud/talk/conversation/repository/ConversationRepository.kt index f85409e1f..70c50aa64 100644 --- a/app/src/main/java/com/nextcloud/talk/conversation/repository/ConversationRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/conversation/repository/ConversationRepository.kt @@ -6,7 +6,7 @@ */ package com.nextcloud.talk.conversation.repository -import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.models.json.conversations.ConversationEnums import com.nextcloud.talk.models.json.conversations.RoomOverall import com.nextcloud.talk.models.json.generic.GenericOverall import io.reactivex.Observable @@ -15,5 +15,8 @@ interface ConversationRepository { fun renameConversation(roomToken: String, roomNameNew: String): Observable<GenericOverall> - fun createConversation(roomName: String, conversationType: Conversation.ConversationType?): Observable<RoomOverall> + fun createConversation( + roomName: String, + conversationType: ConversationEnums.ConversationType? + ): Observable<RoomOverall> } diff --git a/app/src/main/java/com/nextcloud/talk/conversation/repository/ConversationRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/conversation/repository/ConversationRepositoryImpl.kt index f18db9578..f5dae7059 100644 --- a/app/src/main/java/com/nextcloud/talk/conversation/repository/ConversationRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/conversation/repository/ConversationRepositoryImpl.kt @@ -9,7 +9,7 @@ package com.nextcloud.talk.conversation.repository import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.RetrofitBucket -import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.models.json.conversations.ConversationEnums import com.nextcloud.talk.models.json.conversations.RoomOverall import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.utils.ApiUtils @@ -43,29 +43,30 @@ class ConversationRepositoryImpl(private val ncApi: NcApi, currentUserProvider: override fun createConversation( roomName: String, - conversationType: Conversation.ConversationType? + conversationType: ConversationEnums.ConversationType? ): Observable<RoomOverall> { val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1)) - val retrofitBucket: RetrofitBucket = if (conversationType == Conversation.ConversationType.ROOM_PUBLIC_CALL) { - ApiUtils.getRetrofitBucketForCreateRoom( - apiVersion, - currentUser.baseUrl!!, - ROOM_TYPE_PUBLIC, - null, - null, - roomName - ) - } else { - ApiUtils.getRetrofitBucketForCreateRoom( - apiVersion, - currentUser.baseUrl!!, - ROOM_TYPE_GROUP, - null, - null, - roomName - ) - } + val retrofitBucket: RetrofitBucket = + if (conversationType == ConversationEnums.ConversationType.ROOM_PUBLIC_CALL) { + ApiUtils.getRetrofitBucketForCreateRoom( + apiVersion, + currentUser.baseUrl!!, + ROOM_TYPE_PUBLIC, + null, + null, + roomName + ) + } else { + ApiUtils.getRetrofitBucketForCreateRoom( + apiVersion, + currentUser.baseUrl!!, + ROOM_TYPE_GROUP, + null, + null, + roomName + ) + } return ncApi.createRoom(credentials, retrofitBucket.url, retrofitBucket.queryMap) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) diff --git a/app/src/main/java/com/nextcloud/talk/conversation/viewmodel/ConversationViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversation/viewmodel/ConversationViewModel.kt index cdbe3f0c9..7783199f1 100644 --- a/app/src/main/java/com/nextcloud/talk/conversation/viewmodel/ConversationViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/conversation/viewmodel/ConversationViewModel.kt @@ -10,7 +10,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.nextcloud.talk.conversation.repository.ConversationRepository -import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.models.json.conversations.ConversationEnums import com.nextcloud.talk.models.json.conversations.RoomOverall import io.reactivex.Observer import io.reactivex.android.schedulers.AndroidSchedulers @@ -40,7 +40,7 @@ class ConversationViewModel @Inject constructor(private val repository: Conversa disposable?.dispose() } - fun createConversation(roomName: String, conversationType: Conversation.ConversationType?) { + fun createConversation(roomName: String, conversationType: ConversationEnums.ConversationType?) { _viewState.value = CreatingState repository.createConversation( diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt index b4ee2aeb0..8ab356bef 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt @@ -57,11 +57,9 @@ import com.nextcloud.talk.extensions.loadUserAvatar import com.nextcloud.talk.jobs.DeleteConversationWorker import com.nextcloud.talk.jobs.LeaveConversationWorker import com.nextcloud.talk.models.domain.ConversationModel -import com.nextcloud.talk.models.domain.ConversationType -import com.nextcloud.talk.models.domain.LobbyState -import com.nextcloud.talk.models.domain.NotificationLevel import com.nextcloud.talk.models.domain.converters.DomainEnumNotificationLevelConverter import com.nextcloud.talk.models.json.capabilities.SpreedCapability +import com.nextcloud.talk.models.json.conversations.ConversationEnums import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.participants.Participant @@ -350,7 +348,7 @@ class ConversationInfoActivity : binding.webinarInfoView.webinarSettings.visibility = VISIBLE val isLobbyOpenToModeratorsOnly = - conversation!!.lobbyState == LobbyState.LOBBY_STATE_MODERATORS_ONLY + conversation!!.lobbyState == ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY binding.webinarInfoView.lobbySwitch.isChecked = isLobbyOpenToModeratorsOnly reconfigureLobbyTimerView() @@ -386,8 +384,8 @@ class ConversationInfoActivity : } private fun webinaryRoomType(conversation: ConversationModel): Boolean { - return conversation.type == ConversationType.ROOM_GROUP_CALL || - conversation.type == ConversationType.ROOM_PUBLIC_CALL + return conversation.type == ConversationEnums.ConversationType.ROOM_GROUP_CALL || + conversation.type == ConversationEnums.ConversationType.ROOM_PUBLIC_CALL } private fun reconfigureLobbyTimerView(dateTime: Calendar? = null) { @@ -402,9 +400,9 @@ class ConversationInfoActivity : } conversation!!.lobbyState = if (isChecked) { - LobbyState.LOBBY_STATE_MODERATORS_ONLY + ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY } else { - LobbyState.LOBBY_STATE_ALL_PARTICIPANTS + ConversationEnums.LobbyState.LOBBY_STATE_ALL_PARTICIPANTS } if ( @@ -760,13 +758,13 @@ class ConversationInfoActivity : binding.deleteConversationAction.visibility = VISIBLE } - if (ConversationType.ROOM_SYSTEM == conversation!!.type) { + if (ConversationEnums.ConversationType.ROOM_SYSTEM == conversation!!.type) { binding.notificationSettingsView.callNotificationsSwitch.visibility = GONE } binding.listBansButton.visibility = if (ConversationUtils.canModerate(conversationCopy, spreedCapabilities) && - ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL != conversation!!.type + ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL != conversation!!.type ) { VISIBLE } else { @@ -922,7 +920,7 @@ class ConversationInfoActivity : binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown.isEnabled = true binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown.alpha = 1.0f - if (conversation!!.notificationLevel != NotificationLevel.DEFAULT) { + if (conversation!!.notificationLevel != ConversationEnums.NotificationLevel.DEFAULT) { val stringValue: String = when ( DomainEnumNotificationLevelConverter() @@ -952,7 +950,7 @@ class ConversationInfoActivity : } private fun setProperNotificationValue(conversation: ConversationModel?) { - if (conversation!!.type == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) { + if (conversation!!.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) { if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MENTION_FLAG)) { binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown.setText( resources.getString(R.string.nc_notify_me_always) @@ -971,7 +969,10 @@ class ConversationInfoActivity : private fun loadConversationAvatar() { when (conversation!!.type) { - ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> if (!TextUtils.isEmpty(conversation!!.name)) { + ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> if (!TextUtils.isEmpty( + conversation!!.name + ) + ) { conversation!!.name?.let { binding.avatarImage.loadUserAvatar( conversationUser, @@ -982,7 +983,7 @@ class ConversationInfoActivity : } } - ConversationType.ROOM_GROUP_CALL, ConversationType.ROOM_PUBLIC_CALL -> { + ConversationEnums.ConversationType.ROOM_GROUP_CALL, ConversationEnums.ConversationType.ROOM_PUBLIC_CALL -> { binding.avatarImage.loadConversationAvatar( conversationUser, conversation!!, @@ -991,11 +992,11 @@ class ConversationInfoActivity : ) } - ConversationType.ROOM_SYSTEM -> { + ConversationEnums.ConversationType.ROOM_SYSTEM -> { binding.avatarImage.loadSystemAvatar() } - ConversationType.NOTE_TO_SELF -> { + ConversationEnums.ConversationType.NOTE_TO_SELF -> { binding.avatarImage.loadNoteToSelfAvatar() } diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/GuestAccessHelper.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/GuestAccessHelper.kt index 9aae36e00..43b1a8817 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfo/GuestAccessHelper.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/GuestAccessHelper.kt @@ -19,8 +19,8 @@ import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.ActivityConversationInfoBinding import com.nextcloud.talk.databinding.DialogPasswordBinding import com.nextcloud.talk.models.domain.ConversationModel -import com.nextcloud.talk.models.domain.ConversationType import com.nextcloud.talk.models.json.capabilities.SpreedCapability +import com.nextcloud.talk.models.json.conversations.ConversationEnums import com.nextcloud.talk.repositories.conversations.ConversationsRepository import com.nextcloud.talk.utils.ConversationUtils import io.reactivex.Observer @@ -47,7 +47,7 @@ class GuestAccessHelper( binding.guestAccessView.guestAccessSettings.visibility = View.GONE } - if (conversation.type == ConversationType.ROOM_PUBLIC_CALL) { + if (conversation.type == ConversationEnums.ConversationType.ROOM_PUBLIC_CALL) { binding.guestAccessView.allowGuestsSwitch.isChecked = true showAllOptions() if (conversation.hasPassword) { diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt index d8b4ec63a..3a145beeb 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt @@ -12,7 +12,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import com.nextcloud.talk.chat.data.ChatRepository +import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.json.capabilities.SpreedCapability @@ -26,7 +26,7 @@ import io.reactivex.schedulers.Schedulers import javax.inject.Inject class ConversationInfoViewModel @Inject constructor( - private val chatRepository: ChatRepository + private val chatNetworkDataSource: ChatNetworkDataSource ) : ViewModel() { object LifeCycleObserver : DefaultLifecycleObserver { @@ -92,7 +92,7 @@ class ConversationInfoViewModel @Inject constructor( fun getRoom(user: User, token: String) { _viewState.value = GetRoomStartState - chatRepository.getRoom(user, token) + chatNetworkDataSource.getRoom(user, token) .subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread()) ?.subscribe(GetRoomObserver()) @@ -104,7 +104,7 @@ class ConversationInfoViewModel @Inject constructor( if (conversationModel.remoteServer.isNullOrEmpty()) { _getCapabilitiesViewState.value = GetCapabilitiesSuccessState(user.capabilities!!.spreedCapability!!) } else { - chatRepository.getCapabilities(user, token) + chatNetworkDataSource.getCapabilities(user, token) .subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread()) ?.subscribe(object : Observer<SpreedCapability> { @@ -130,7 +130,7 @@ class ConversationInfoViewModel @Inject constructor( fun listBans(user: User, token: String) { val url = ApiUtils.getUrlForBans(user.baseUrl!!, token) - chatRepository.listBans(user.getCredentials(), url) + chatNetworkDataSource.listBans(user.getCredentials(), url) .subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread()) ?.subscribe(object : Observer<List<TalkBan>> { @@ -154,7 +154,7 @@ class ConversationInfoViewModel @Inject constructor( fun banActor(user: User, token: String, actorType: String, actorId: String, internalNote: String) { val url = ApiUtils.getUrlForBans(user.baseUrl!!, token) - chatRepository.banActor(user.getCredentials(), url, actorType, actorId, internalNote) + chatNetworkDataSource.banActor(user.getCredentials(), url, actorType, actorId, internalNote) .subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread()) ?.subscribe(object : Observer<TalkBan> { @@ -178,7 +178,7 @@ class ConversationInfoViewModel @Inject constructor( fun unbanActor(user: User, token: String, banId: Int) { val url = ApiUtils.getUrlForUnban(user.baseUrl!!, token, banId) - chatRepository.unbanActor(user.getCredentials(), url) + chatNetworkDataSource.unbanActor(user.getCredentials(), url) .subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread()) ?.subscribe(object : Observer<GenericOverall> { diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt index c030ea7e2..24dba4f13 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt @@ -34,8 +34,8 @@ import com.nextcloud.talk.extensions.loadConversationAvatar import com.nextcloud.talk.extensions.loadSystemAvatar import com.nextcloud.talk.extensions.loadUserAvatar import com.nextcloud.talk.models.domain.ConversationModel -import com.nextcloud.talk.models.domain.ConversationType import com.nextcloud.talk.models.json.capabilities.SpreedCapability +import com.nextcloud.talk.models.json.conversations.ConversationEnums import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.CapabilitiesUtil @@ -126,10 +126,6 @@ class ConversationInfoEditActivity : BaseActivity() { initObservers() } - override fun onResume() { - super.onResume() - } - private fun initObservers() { conversationInfoEditViewModel.viewState.observe(this) { state -> when (state) { @@ -349,15 +345,18 @@ class ConversationInfoEditActivity : BaseActivity() { setupAvatarOptions() when (conversation!!.type) { - ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> if (!TextUtils.isEmpty(conversation!!.name)) { + ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> if (!TextUtils.isEmpty( + conversation!!.name + ) + ) { conversation!!.name?.let { binding.avatarImage.loadUserAvatar(conversationUser, it, true, false) } } - ConversationType.ROOM_GROUP_CALL, ConversationType.ROOM_PUBLIC_CALL -> { + ConversationEnums.ConversationType.ROOM_GROUP_CALL, ConversationEnums.ConversationType.ROOM_PUBLIC_CALL -> { binding.avatarImage.loadConversationAvatar(conversationUser, conversation!!, false, viewThemeUtils) } - ConversationType.ROOM_SYSTEM -> { + ConversationEnums.ConversationType.ROOM_SYSTEM -> { binding.avatarImage.loadSystemAvatar() } diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfoedit/data/ConversationInfoEditRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/data/ConversationInfoEditRepositoryImpl.kt index c84e17c65..8ed719a4c 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfoedit/data/ConversationInfoEditRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/data/ConversationInfoEditRepositoryImpl.kt @@ -31,7 +31,7 @@ class ConversationInfoEditRepositoryImpl(private val ncApi: NcApi, currentUserPr builder.setType(MultipartBody.FORM) builder.addFormDataPart( "file", - file!!.name, + file.name, file.asRequestBody(Mimetype.IMAGE_PREFIX_GENERIC.toMediaTypeOrNull()) ) val filePart: MultipartBody.Part = MultipartBody.Part.createFormData( @@ -44,13 +44,13 @@ class ConversationInfoEditRepositoryImpl(private val ncApi: NcApi, currentUserPr credentials, ApiUtils.getUrlForConversationAvatar(1, user.baseUrl!!, roomToken), filePart - ).map { ConversationModel.mapToConversationModel(it.ocs?.data!!) } + ).map { ConversationModel.mapToConversationModel(it.ocs?.data!!, user) } } override fun deleteConversationAvatar(user: User, roomToken: String): Observable<ConversationModel> { return ncApi.deleteConversationAvatar( credentials, ApiUtils.getUrlForConversationAvatar(1, user.baseUrl!!, roomToken) - ).map { ConversationModel.mapToConversationModel(it.ocs?.data!!) } + ).map { ConversationModel.mapToConversationModel(it.ocs?.data!!, user) } } } diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfoedit/viewmodel/ConversationInfoEditViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/viewmodel/ConversationInfoEditViewModel.kt index cc9af97fe..ff8af5955 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfoedit/viewmodel/ConversationInfoEditViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/viewmodel/ConversationInfoEditViewModel.kt @@ -10,7 +10,7 @@ import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import com.nextcloud.talk.chat.data.ChatRepository +import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource import com.nextcloud.talk.conversationinfoedit.data.ConversationInfoEditRepository import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.domain.ConversationModel @@ -22,7 +22,7 @@ import java.io.File import javax.inject.Inject class ConversationInfoEditViewModel @Inject constructor( - private val repository: ChatRepository, + private val repository: ChatNetworkDataSource, private val conversationInfoEditRepository: ConversationInfoEditRepository ) : ViewModel() { diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt index bda2f14ca..8d9be6505 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt @@ -45,6 +45,7 @@ import androidx.appcompat.widget.SearchView import androidx.core.view.MenuItemCompat import androidx.fragment.app.DialogFragment import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import androidx.work.Data import androidx.work.OneTimeWorkRequest @@ -91,8 +92,8 @@ import com.nextcloud.talk.jobs.DeleteConversationWorker import com.nextcloud.talk.jobs.UploadAndShareFilesWorker import com.nextcloud.talk.messagesearch.MessageSearchHelper import com.nextcloud.talk.messagesearch.MessageSearchHelper.MessageSearchResults -import com.nextcloud.talk.models.json.conversations.Conversation -import com.nextcloud.talk.models.json.conversations.RoomsOverall +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.conversations.ConversationEnums import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository import com.nextcloud.talk.settings.SettingsActivity import com.nextcloud.talk.ui.dialog.ChooseAccountDialogFragment @@ -107,6 +108,7 @@ import com.nextcloud.talk.utils.CapabilitiesUtil.isServerEOL import com.nextcloud.talk.utils.CapabilitiesUtil.isUnifiedSearchAvailable import com.nextcloud.talk.utils.CapabilitiesUtil.isUserStatusAvailable import com.nextcloud.talk.utils.ClosedInterfaceImpl +import com.nextcloud.talk.utils.ConversationUtils import com.nextcloud.talk.utils.FileUtils import com.nextcloud.talk.utils.Mimetype import com.nextcloud.talk.utils.ParticipantPermissions @@ -134,6 +136,9 @@ import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers import io.reactivex.subjects.BehaviorSubject +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import org.apache.commons.lang3.builder.CompareToBuilder import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode @@ -190,7 +195,7 @@ class ConversationsListActivity : private var isRefreshing = false private var showShareToScreen = false private var filesToShare: ArrayList<String>? = null - private var selectedConversation: Conversation? = null + private var selectedConversation: ConversationModel? = null private var textToPaste: String? = "" private var selectedMessageId: String? = null private var forwardMessage: Boolean = false @@ -259,7 +264,7 @@ class ConversationsListActivity : if (adapter == null) { adapter = FlexibleAdapter(conversationItems, this, true) } else { - binding?.loadingContent?.visibility = View.GONE + binding.loadingContent?.visibility = View.GONE } adapter!!.addListener(this) prepareViews() @@ -334,6 +339,51 @@ class ConversationsListActivity : else -> {} } } + + conversationsListViewModel.getRoomsViewState.observe(this) { state -> + when (state) { + is ConversationsListViewModel.GetRoomsSuccessState -> { + if (adapterWasNull) { + adapterWasNull = false + binding.loadingContent.visibility = View.GONE + } + initOverallLayout(state.listIsNotEmpty) + binding.swipeRefreshLayoutView.isRefreshing = false + } + + is ConversationsListViewModel.GetRoomsErrorState -> { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_SHORT).show() + } + + else -> {} + } + } + + lifecycleScope.launch { + conversationsListViewModel.getRoomsFlow + .onEach { list -> + // Update Conversations + conversationItems.clear() + for (conversation in list) { + addToConversationItems(conversation) + } + sortConversations(conversationItems) + sortConversations(conversationItemsWithHeader) + + // Filter Conversations + if (!filterState.containsValue(true)) filterableConversationItems = conversationItems + filterConversation() + adapter!!.updateDataSet(filterableConversationItems, false) + Handler().postDelayed({ checkToShowUnreadBubble() }, UNREAD_BUBBLE_DELAY.toLong()) + + // Fetch Open Conversations + val apiVersion = ApiUtils.getConversationApiVersion( + currentUser!!, + intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1) + ) + fetchOpenConversations(apiVersion) + }.collect() + } } fun filterConversation() { @@ -374,7 +424,7 @@ class ConversationsListActivity : updateFilterConversationButtonColor() } - private fun filter(conversation: Conversation): Boolean { + private fun filter(conversation: ConversationModel): Boolean { var result = true for ((k, v) in filterState) { if (v) { @@ -383,8 +433,8 @@ class ConversationsListActivity : ( result && ( - conversation.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL || - conversation.type == Conversation.ConversationType.FORMER_ONE_TO_ONE + conversation.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL || + conversation.type == ConversationEnums.ConversationType.FORMER_ONE_TO_ONE ) && (conversation.unreadMessages > 0) ) @@ -573,7 +623,7 @@ class ConversationsListActivity : if (!filterState.containsValue(true)) filterableConversationItems = searchableConversationItems adapter!!.updateDataSet(filterableConversationItems, false) adapter!!.showAllHeaders() - binding?.swipeRefreshLayoutView?.isEnabled = false + binding.swipeRefreshLayoutView?.isEnabled = false searchBehaviorSubject.onNext(true) return true } @@ -586,10 +636,10 @@ class ConversationsListActivity : if (searchHelper != null) { // cancel any pending searches searchHelper!!.cancelSearch() - binding?.swipeRefreshLayoutView?.isRefreshing = false + binding.swipeRefreshLayoutView?.isRefreshing = false searchBehaviorSubject.onNext(false) } - binding?.swipeRefreshLayoutView?.isEnabled = true + binding.swipeRefreshLayoutView?.isEnabled = true searchView!!.onActionViewCollapsed() binding.conversationListAppbar.stateListAnimator = AnimatorInflater.loadStateListAnimator( @@ -602,7 +652,7 @@ class ConversationsListActivity : viewThemeUtils.platform.resetStatusBar(this@ConversationsListActivity) } - val layoutManager = binding?.recyclerView?.layoutManager as SmoothScrollLinearLayoutManager? + val layoutManager = binding.recyclerView?.layoutManager as SmoothScrollLinearLayoutManager? layoutManager?.scrollToPositionWithOffset(0, 0) return true } @@ -681,67 +731,68 @@ class ConversationsListActivity : } fun fetchRooms() { - val includeStatus = isUserStatusAvailable(userManager.currentUser.blockingGet()) + val includeStatus = isUserStatusAvailable(currentUser!!) + conversationsListViewModel.getRooms() // checks internet connection before fetching rooms if (isNetworkAvailable(context)) { - Log.d(TAG, "Internet connection available") - dispose(null) - isRefreshing = true - conversationItems = ArrayList() - conversationItemsWithHeader = ArrayList() - val apiVersion = ApiUtils.getConversationApiVersion( - currentUser!!, - intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1) - ) - val startNanoTime = System.nanoTime() - Log.d(TAG, "fetchData - getRooms - calling: $startNanoTime") - roomsQueryDisposable = ncApi.getRooms( - credentials, - ApiUtils.getUrlForRooms( - apiVersion, - currentUser!!.baseUrl - ), - includeStatus - ) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ (ocs): RoomsOverall -> - Log.d(TAG, "fetchData - getRooms - got response: $startNanoTime") - - // This is invoked asynchronously, when server returns a response the view might have been - // unbound in the meantime. Check if the view is still there. - // FIXME - does it make sense to update internal data structures even when view has been unbound? - // if (view == null) { - // Log.d(TAG, "fetchData - getRooms - view is not bound: $startNanoTime") - // return@subscribe - // } - - if (adapterWasNull) { - adapterWasNull = false - binding?.loadingContent?.visibility = View.GONE - } - initOverallLayout(ocs!!.data!!.isNotEmpty()) - for (conversation in ocs.data!!) { - addToConversationItems(conversation) - } - sortConversations(conversationItems) - sortConversations(conversationItemsWithHeader) - if (!filterState.containsValue(true)) filterableConversationItems = conversationItems - filterConversation() - adapter!!.updateDataSet(filterableConversationItems, false) - Handler().postDelayed({ checkToShowUnreadBubble() }, UNREAD_BUBBLE_DELAY.toLong()) - fetchOpenConversations(apiVersion) - binding?.swipeRefreshLayoutView?.isRefreshing = false - }, { throwable: Throwable -> - handleHttpExceptions(throwable) - binding?.swipeRefreshLayoutView?.isRefreshing = false - dispose(roomsQueryDisposable) - }) { - dispose(roomsQueryDisposable) - binding?.swipeRefreshLayoutView?.isRefreshing = false - isRefreshing = false - } + // Log.d(TAG, "Internet connection available") + // dispose(null) + // isRefreshing = true + // conversationItems = ArrayList() + // conversationItemsWithHeader = ArrayList() + // val apiVersion = ApiUtils.getConversationApiVersion( + // currentUser!!, + // intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1) + // ) + // val startNanoTime = System.nanoTime() + // Log.d(TAG, "fetchData - getRooms - calling: $startNanoTime") + // roomsQueryDisposable = ncApi.getRooms( + // credentials, + // ApiUtils.getUrlForRooms( + // apiVersion, + // currentUser!!.baseUrl + // ), + // includeStatus + // ) + // .subscribeOn(Schedulers.io()) + // .observeOn(AndroidSchedulers.mainThread()) + // .subscribe({ (ocs): RoomsOverall -> + // Log.d(TAG, "fetchData - getRooms - got response: $startNanoTime") + // + // // This is invoked asynchronously, when server returns a response the view might have been + // // unbound in the meantime. Check if the view is still there. + // // FIXME - does it make sense to update internal data structures even when view has been unbound? + // // if (view == null) { + // // Log.d(TAG, "fetchData - getRooms - view is not bound: $startNanoTime") + // // return@subscribe + // // } + // + // if (adapterWasNull) { + // adapterWasNull = false + // binding?.loadingContent?.visibility = View.GONE + // } + // initOverallLayout(ocs!!.data!!.isNotEmpty()) + // for (conversation in ocs.data!!) { + // addToConversationItems(conversation) + // } + // sortConversations(conversationItems) + // sortConversations(conversationItemsWithHeader) + // if (!filterState.containsValue(true)) filterableConversationItems = conversationItems + // filterConversation() + // adapter!!.updateDataSet(filterableConversationItems, false) + // Handler().postDelayed({ checkToShowUnreadBubble() }, UNREAD_BUBBLE_DELAY.toLong()) + // fetchOpenConversations(apiVersion) + // binding?.swipeRefreshLayoutView?.isRefreshing = false + // }, { throwable: Throwable -> + // handleHttpExceptions(throwable) + // binding?.swipeRefreshLayoutView?.isRefreshing = false + // dispose(roomsQueryDisposable) + // }) { + // dispose(roomsQueryDisposable) + // binding?.swipeRefreshLayoutView?.isRefreshing = false + // isRefreshing = false + // } } else { Log.d(TAG, "No internet connection detected") showNetworkErrorDialog() @@ -760,31 +811,31 @@ class ConversationsListActivity : private fun initOverallLayout(isConversationListNotEmpty: Boolean) { if (isConversationListNotEmpty) { - if (binding?.emptyLayout?.visibility != View.GONE) { - binding?.emptyLayout?.visibility = View.GONE + if (binding.emptyLayout?.visibility != View.GONE) { + binding.emptyLayout?.visibility = View.GONE } - if (binding?.swipeRefreshLayoutView?.visibility != View.VISIBLE) { - binding?.swipeRefreshLayoutView?.visibility = View.VISIBLE + if (binding.swipeRefreshLayoutView?.visibility != View.VISIBLE) { + binding.swipeRefreshLayoutView?.visibility = View.VISIBLE } } else { - if (binding?.emptyLayout?.visibility != View.VISIBLE) { - binding?.emptyLayout?.visibility = View.VISIBLE + if (binding.emptyLayout?.visibility != View.VISIBLE) { + binding.emptyLayout?.visibility = View.VISIBLE } - if (binding?.swipeRefreshLayoutView?.visibility != View.GONE) { - binding?.swipeRefreshLayoutView?.visibility = View.GONE + if (binding.swipeRefreshLayoutView?.visibility != View.GONE) { + binding.swipeRefreshLayoutView?.visibility = View.GONE } } } - private fun addToConversationItems(conversation: Conversation) { + private fun addToConversationItems(conversation: ConversationModel) { if (intent.getStringExtra(KEY_FORWARD_HIDE_SOURCE_ROOM) != null && intent.getStringExtra(KEY_FORWARD_HIDE_SOURCE_ROOM) == conversation.roomId ) { return } - if (conversation.objectType == Conversation.ObjectType.ROOM && - conversation.lobbyState == Conversation.LobbyState.LOBBY_STATE_MODERATORS_ONLY + if (conversation.objectType == ConversationEnums.ObjectType.ROOM && + conversation.lobbyState == ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY ) { return } @@ -909,35 +960,35 @@ class ConversationsListActivity : ) ) { val openConversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList() - openConversationsQueryDisposable = ncApi.getOpenConversations( - credentials, - ApiUtils.getUrlForOpenConversations(apiVersion, currentUser!!.baseUrl!!) - ) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ (ocs): RoomsOverall -> - for (conversation in ocs!!.data!!) { - val headerTitle = resources!!.getString(R.string.openConversations) - var genericTextHeaderItem: GenericTextHeaderItem - if (!callHeaderItems.containsKey(headerTitle)) { - genericTextHeaderItem = GenericTextHeaderItem(headerTitle, viewThemeUtils) - callHeaderItems[headerTitle] = genericTextHeaderItem - } - val conversationItem = ConversationItem( - conversation, - currentUser!!, - this, - callHeaderItems[headerTitle], - viewThemeUtils - ) - openConversationItems.add(conversationItem) - } - searchableConversationItems.addAll(openConversationItems) - }, { throwable: Throwable -> - Log.e(TAG, "fetchData - getRooms - ERROR", throwable) - handleHttpExceptions(throwable) - dispose(openConversationsQueryDisposable) - }) { dispose(openConversationsQueryDisposable) } + // openConversationsQueryDisposable = ncApi.getOpenConversations( + // credentials, + // ApiUtils.getUrlForOpenConversations(apiVersion, currentUser!!.baseUrl!!) + // ) + // .subscribeOn(Schedulers.io()) + // .observeOn(AndroidSchedulers.mainThread()) + // .subscribe({ (ocs): RoomsOverall -> + // for (conversation in ocs!!.data!!) { + // val headerTitle = resources!!.getString(R.string.openConversations) + // var genericTextHeaderItem: GenericTextHeaderItem + // if (!callHeaderItems.containsKey(headerTitle)) { + // genericTextHeaderItem = GenericTextHeaderItem(headerTitle, viewThemeUtils) + // callHeaderItems[headerTitle] = genericTextHeaderItem + // } + // val conversationItem = ConversationItem( + // conversation, + // currentUser!!, + // this, + // callHeaderItems[headerTitle], + // viewThemeUtils + // ) + // openConversationItems.add(conversationItem) + // } + // searchableConversationItems.addAll(openConversationItems) + // }, { throwable: Throwable -> + // Log.e(TAG, "fetchData - getRooms - ERROR", throwable) + // handleHttpExceptions(throwable) + // dispose(openConversationsQueryDisposable) + // }) { dispose(openConversationsQueryDisposable) } } else { Log.d(TAG, "no open conversations fetched because of missing capability") } @@ -979,24 +1030,24 @@ class ConversationsListActivity : } } }) - binding?.recyclerView?.setOnTouchListener { v: View, _: MotionEvent? -> + binding.recyclerView?.setOnTouchListener { v: View, _: MotionEvent? -> if (!isDestroyed) { val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager imm.hideSoftInputFromWindow(v.windowToken, 0) } false } - binding?.swipeRefreshLayoutView?.setOnRefreshListener { + binding.swipeRefreshLayoutView?.setOnRefreshListener { fetchRooms() fetchPendingInvitations() } - binding?.swipeRefreshLayoutView?.let { viewThemeUtils.androidx.themeSwipeRefreshLayout(it) } - binding?.emptyLayout?.setOnClickListener { showNewConversationsScreen() } - binding?.floatingActionButton?.setOnClickListener { + binding.swipeRefreshLayoutView?.let { viewThemeUtils.androidx.themeSwipeRefreshLayout(it) } + binding.emptyLayout?.setOnClickListener { showNewConversationsScreen() } + binding.floatingActionButton?.setOnClickListener { run(context) showNewConversationsScreen() } - binding?.floatingActionButton?.let { viewThemeUtils.material.themeFAB(it) } + binding.floatingActionButton?.let { viewThemeUtils.material.themeFAB(it) } binding.switchAccountButton.setOnClickListener { if (resources != null && resources!!.getBoolean(R.bool.multiaccount_support)) { @@ -1015,13 +1066,13 @@ class ConversationsListActivity : newFragment.show(supportFragmentManager, FilterConversationFragment.TAG) } - binding?.newMentionPopupBubble?.hide() - binding?.newMentionPopupBubble?.setPopupBubbleListener { - binding?.recyclerView?.smoothScrollToPosition( + binding.newMentionPopupBubble?.hide() + binding.newMentionPopupBubble?.setPopupBubbleListener { + binding.recyclerView?.smoothScrollToPosition( nextUnreadConversationScrollPosition ) } - binding?.newMentionPopupBubble?.let { viewThemeUtils.material.colorMaterialButtonPrimaryFilled(it) } + binding.newMentionPopupBubble?.let { viewThemeUtils.material.colorMaterialButtonPrimaryFilled(it) } } private fun hideLogoForBrandedClients() { @@ -1041,17 +1092,17 @@ class ConversationsListActivity : try { val lastVisibleItem = layoutManager!!.findLastCompletelyVisibleItemPosition() for (flexItem in conversationItems) { - val conversation: Conversation = (flexItem as ConversationItem).model + val conversation: ConversationModel = (flexItem as ConversationItem).model val position = adapter!!.getGlobalPositionOf(flexItem) if (hasUnreadItems(conversation) && position > lastVisibleItem) { nextUnreadConversationScrollPosition = position - if (!binding?.newMentionPopupBubble?.isShown!!) { - binding?.newMentionPopupBubble?.show() + if (!binding.newMentionPopupBubble?.isShown!!) { + binding.newMentionPopupBubble?.show() } return@subscribe } nextUnreadConversationScrollPosition = 0 - binding?.newMentionPopupBubble?.hide() + binding.newMentionPopupBubble?.hide() } } catch (e: NullPointerException) { Log.d( @@ -1066,10 +1117,10 @@ class ConversationsListActivity : } } - private fun hasUnreadItems(conversation: Conversation) = + private fun hasUnreadItems(conversation: ConversationModel) = conversation.unreadMention || conversation.unreadMessages > 0 && - conversation.type === Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL + conversation.type === ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL private fun showNewConversationsScreen() { val intent = Intent(context, ContactsActivityCompose::class.java) @@ -1157,7 +1208,7 @@ class ConversationsListActivity : @SuppressLint("CheckResult") // handled by helper private fun startMessageSearch(search: String?) { - binding?.swipeRefreshLayoutView?.isRefreshing = true + binding.swipeRefreshLayoutView?.isRefreshing = true searchHelper?.startMessageSearch(search!!) ?.subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread()) @@ -1214,7 +1265,7 @@ class ConversationsListActivity : } @Suppress("Detekt.ComplexMethod") - private fun handleConversation(conversation: Conversation?) { + private fun handleConversation(conversation: ConversationModel?) { selectedConversation = conversation if (selectedConversation != null) { val hasChatPermission = ParticipantPermissions( @@ -1244,19 +1295,19 @@ class ConversationsListActivity : } } - private fun shouldShowLobby(conversation: Conversation): Boolean { + private fun shouldShowLobby(conversation: ConversationModel): Boolean { val participantPermissions = ParticipantPermissions( currentUser!!.capabilities?.spreedCapability!!, - conversation + selectedConversation!! ) - return conversation.lobbyState == Conversation.LobbyState.LOBBY_STATE_MODERATORS_ONLY && - !conversation.canModerate(currentUser!!) && + return conversation.lobbyState == ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY && + !ConversationUtils.canModerate(conversation, currentUser!!.capabilities!!.spreedCapability!!) && !participantPermissions.canIgnoreLobby() } - private fun isReadOnlyConversation(conversation: Conversation): Boolean { + private fun isReadOnlyConversation(conversation: ConversationModel): Boolean { return conversation.conversationReadOnlyState === - Conversation.ConversationReadOnlyState.CONVERSATION_READ_ONLY + ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_ONLY } private fun handleSharedData() { @@ -1519,7 +1570,7 @@ class ConversationsListActivity : }, BOTTOM_SHEET_DELAY) } - fun showDeleteConversationDialog(conversation: Conversation) { + fun showDeleteConversationDialog(conversation: ConversationModel) { binding.floatingActionButton.let { val dialogBuilder = MaterialAlertDialogBuilder(it.context) .setIcon( @@ -1751,7 +1802,7 @@ class ConversationsListActivity : } } - private fun deleteConversation(conversation: Conversation) { + private fun deleteConversation(conversation: ConversationModel) { val data = Data.Builder() data.putLong( KEY_INTERNAL_USER_ID, @@ -1810,15 +1861,15 @@ class ConversationsListActivity : } // add unified search result at the end of the list adapter!!.addItems(adapter!!.mainItemCount + adapter!!.scrollableHeaders.size, adapterItems) - binding?.recyclerView?.scrollToPosition(0) + binding.recyclerView?.scrollToPosition(0) } } - binding?.swipeRefreshLayoutView?.isRefreshing = false + binding.swipeRefreshLayoutView?.isRefreshing = false } private fun onMessageSearchError(throwable: Throwable) { handleHttpExceptions(throwable) - binding?.swipeRefreshLayoutView?.isRefreshing = false + binding.swipeRefreshLayoutView?.isRefreshing = false showErrorDialog() } diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/data/ConversationsListRepository.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/data/ConversationsListRepository.kt deleted file mode 100644 index 6f7baf1a5..000000000 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/data/ConversationsListRepository.kt +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Nextcloud Talk - Android Client - * - * SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de> - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package com.nextcloud.talk.conversationlist.data - -interface ConversationsListRepository diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/data/ConversationsListRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/data/ConversationsListRepositoryImpl.kt deleted file mode 100644 index 098b27e71..000000000 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/data/ConversationsListRepositoryImpl.kt +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Nextcloud Talk - Android Client - * - * SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de> - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package com.nextcloud.talk.conversationlist.data - -import com.nextcloud.talk.api.NcApi - -class ConversationsListRepositoryImpl(private val ncApi: NcApi) : ConversationsListRepository diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/data/OfflineConversationsRepository.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/data/OfflineConversationsRepository.kt new file mode 100644 index 000000000..d89023365 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/data/OfflineConversationsRepository.kt @@ -0,0 +1,40 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.conversationlist.data + +import com.nextcloud.talk.data.sync.Syncable +import com.nextcloud.talk.models.domain.ConversationModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow + +interface OfflineConversationsRepository : Syncable { + + /** + * Stream of a list of rooms, for use in the conversation list. + */ + val roomListFlow: Flow<List<ConversationModel>> + + /** + * Stream of a single conversation, for use in each conversations settings. + */ + val conversationFlow: Flow<ConversationModel> + + /** + * Loads rooms from local storage. If the rooms are not found, then it + * synchronizes the database with the server, before retrying exactly once. Only + * emits to [roomListFlow] if the rooms list is not empty. + * + */ + fun getRooms(): Job + + /** + * Called once onStart to emit a conversation to [conversationFlow] + * to be handled asynchronously. + */ + fun getConversationSettings(roomToken: String): Job +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/ConversationsNetworkDataSource.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/ConversationsNetworkDataSource.kt new file mode 100644 index 000000000..bf3ae39d1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/ConversationsNetworkDataSource.kt @@ -0,0 +1,16 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.conversationlist.data.network + +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.json.conversations.Conversation +import io.reactivex.Observable + +interface ConversationsNetworkDataSource { + fun getRooms(user: User, url: String, includeStatus: Boolean): Observable<List<Conversation>> +} 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 new file mode 100644 index 000000000..f3b59d15d --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.kt @@ -0,0 +1,111 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Your Name <your@email.com> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.conversationlist.data.network + +import android.os.Bundle +import androidx.core.os.bundleOf +import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository +import com.nextcloud.talk.data.database.dao.ConversationsDao +import com.nextcloud.talk.data.database.mappers.asEntity +import com.nextcloud.talk.data.database.mappers.asModel +import com.nextcloud.talk.data.database.model.ConversationEntity +import com.nextcloud.talk.data.sync.Synchronizer +import com.nextcloud.talk.data.sync.changeListSync +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import javax.inject.Inject + +class OfflineFirstConversationsRepository @Inject constructor( + private val dao: ConversationsDao, + private val network: ConversationsNetworkDataSource, + private val currentUserProviderNew: CurrentUserProviderNew +) : OfflineConversationsRepository, Synchronizer { + + override val roomListFlow: Flow<List<ConversationModel>> + get() = _roomListFlow + private val _roomListFlow: MutableSharedFlow<List<ConversationModel>> = MutableSharedFlow() + + override val conversationFlow: Flow<ConversationModel> + get() = _conversationFlow + private val _conversationFlow: MutableSharedFlow<ConversationModel> = MutableSharedFlow() + + private val scope = CoroutineScope(Dispatchers.IO) + private var user: User = currentUserProviderNew.currentUser.blockingGet() + + override fun getRooms(): Job = + scope.launch { + repeat(2) { + val list = getListOfConversations(user.id!!) + if (list.isNotEmpty()) { + _roomListFlow.emit(list) + } + this@OfflineFirstConversationsRepository.sync(bundleOf()) + } + } + + override fun getConversationSettings(roomToken: String): Job = + scope.launch { + val id = user.id!! + val model = getConversation(id, roomToken) + model?.let { _conversationFlow.emit(model) } + } + + override suspend fun syncWith(bundle: Bundle, synchronizer: Synchronizer): Boolean = + synchronizer.changeListSync( + modelFetcher = { + return@changeListSync getConversationsFromServer() + }, + // not needed + versionUpdater = {}, + modelDeleter = {}, + modelUpdater = { models -> + val list = models.filterIsInstance<Conversation>().map { + it.asEntity(user.id!!) + } + dao.upsertConversations(list) + } + ) + + private fun getConversationsFromServer(): List<Conversation> { + val list = network.getRooms(user, user.baseUrl!!, false) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .map { list -> + return@map list.map { + it.apply { + id = roomId!!.toLong() + } + } + } + .blockingSingle() + + return list ?: listOf() + } + + private suspend fun getListOfConversations(accountId: Long): List<ConversationModel> = + dao.getConversationsForUser(accountId).map { + it.map(ConversationEntity::asModel) + }.first() + + private suspend fun getConversation(accountId: Long, token: String): ConversationModel? { + val entity = dao.getConversationForUser(accountId, token).first() + return entity?.asModel() + } +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/RetrofitConversationsNetwork.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/RetrofitConversationsNetwork.kt new file mode 100644 index 000000000..966968862 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/RetrofitConversationsNetwork.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de> + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.conversationlist.data.network + +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.utils.ApiUtils +import io.reactivex.Observable + +class RetrofitConversationsNetwork(private val ncApi: NcApi) : ConversationsNetworkDataSource { + override fun getRooms(user: User, url: String, includeStatus: Boolean): Observable<List<Conversation>> { + val credentials: String = ApiUtils.getCredentials(user.username, user.token)!! + val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1)) + + return ncApi.getRooms( + credentials, + ApiUtils.getUrlForRooms(apiVersion, user.baseUrl!!), + includeStatus + ).map { it -> + it.ocs?.data?.map { it } ?: listOf() + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt index 50dc68386..f70cf134a 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt @@ -10,7 +10,7 @@ import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import com.nextcloud.talk.conversationlist.data.ConversationsListRepository +import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository import com.nextcloud.talk.invitation.data.InvitationsModel import com.nextcloud.talk.invitation.data.InvitationsRepository import com.nextcloud.talk.users.UserManager @@ -18,21 +18,36 @@ import io.reactivex.Observer import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.onEach import javax.inject.Inject class ConversationsListViewModel @Inject constructor( - private val conversationsListRepository: ConversationsListRepository + private val repository: OfflineConversationsRepository, + var userManager: UserManager ) : ViewModel() { @Inject lateinit var invitationsRepository: InvitationsRepository - @Inject - lateinit var userManager: UserManager - sealed interface ViewState + object GetRoomsStartState : ViewState + object GetRoomsErrorState : ViewState + open class GetRoomsSuccessState(val listIsNotEmpty: Boolean) : ViewState + + private val _getRoomsViewState: MutableLiveData<ViewState> = MutableLiveData(GetRoomsStartState) + val getRoomsViewState: LiveData<ViewState> + get() = _getRoomsViewState + + val getRoomsFlow = repository.roomListFlow + .onEach { list -> + _getRoomsViewState.value = GetRoomsSuccessState(list.isNotEmpty()) + }.catch { + _getRoomsViewState.value = GetRoomsErrorState + } + object GetFederationInvitationsStartState : ViewState object GetFederationInvitationsErrorState : ViewState @@ -63,6 +78,12 @@ class ConversationsListViewModel @Inject constructor( } } + fun getRooms() { + val startNanoTime = System.nanoTime() + Log.d(TAG, "fetchData - getRooms - calling: $startNanoTime") + repository.getRooms() + } + inner class FederatedInvitationsObserver : Observer<InvitationsModel> { override fun onSubscribe(d: Disposable) { // unused atm diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/DaosModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/DaosModule.kt new file mode 100644 index 000000000..27ec540a0 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/DaosModule.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.dagger.modules + +import com.nextcloud.talk.data.database.dao.ChatBlocksDao +import com.nextcloud.talk.data.database.dao.ChatMessagesDao +import com.nextcloud.talk.data.database.dao.ConversationsDao +import com.nextcloud.talk.data.source.local.TalkDatabase +import dagger.Module +import dagger.Provides + +@Module +internal object DaosModule { + @Provides + fun providesConversationsDao(database: TalkDatabase): ConversationsDao = database.conversationsDao() + + @Provides + fun providesChatDao(database: TalkDatabase): ChatMessagesDao = database.chatMessagesDao() + + @Provides + fun providesChatBlocksDao(database: TalkDatabase): ChatBlocksDao = database.chatBlocksDao() +} diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/DatabaseModule.java b/app/src/main/java/com/nextcloud/talk/dagger/modules/DatabaseModule.java index ed2066f9b..253e0c3ce 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/DatabaseModule.java +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/DatabaseModule.java @@ -9,6 +9,8 @@ package com.nextcloud.talk.dagger.modules; import android.content.Context; +import com.nextcloud.talk.data.network.NetworkMonitor; +import com.nextcloud.talk.data.network.NetworkMonitorImpl; import com.nextcloud.talk.data.source.local.TalkDatabase; import com.nextcloud.talk.utils.preferences.AppPreferences; import com.nextcloud.talk.utils.preferences.AppPreferencesImpl; @@ -44,4 +46,10 @@ public class DatabaseModule { @NonNull final AppPreferences appPreferences) { return TalkDatabase.getInstance(context, appPreferences); } + + @Provides + @Singleton + public NetworkMonitor provideNetworkMonitor(@NonNull final Context poContext) { + return new NetworkMonitorImpl(poContext); + } } 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 4c882dba2..3159f7e6b 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 @@ -1,7 +1,7 @@ /* * Nextcloud Talk - Android Client * - * SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de> + * SPDX-FileCopyrightText: 2022-2024 Marcel Hibbe <dev@mhibbe.de> * SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de> * SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com> * SPDX-FileCopyrightText: 2022 Nextcloud GmbH @@ -10,17 +10,25 @@ package com.nextcloud.talk.dagger.modules import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.chat.data.ChatMessageRepository +import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource +import com.nextcloud.talk.chat.data.network.OfflineFirstChatRepository +import com.nextcloud.talk.chat.data.network.RetrofitChatNetwork import com.nextcloud.talk.api.NcApiCoroutines -import com.nextcloud.talk.chat.data.ChatRepository -import com.nextcloud.talk.chat.data.network.NetworkChatRepositoryImpl import com.nextcloud.talk.contacts.ContactsRepository import com.nextcloud.talk.contacts.ContactsRepositoryImpl import com.nextcloud.talk.conversation.repository.ConversationRepository import com.nextcloud.talk.conversation.repository.ConversationRepositoryImpl import com.nextcloud.talk.conversationinfoedit.data.ConversationInfoEditRepository import com.nextcloud.talk.conversationinfoedit.data.ConversationInfoEditRepositoryImpl -import com.nextcloud.talk.conversationlist.data.ConversationsListRepository -import com.nextcloud.talk.conversationlist.data.ConversationsListRepositoryImpl +import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository +import com.nextcloud.talk.conversationlist.data.network.ConversationsNetworkDataSource +import com.nextcloud.talk.conversationlist.data.network.OfflineFirstConversationsRepository +import com.nextcloud.talk.conversationlist.data.network.RetrofitConversationsNetwork +import com.nextcloud.talk.data.database.dao.ChatBlocksDao +import com.nextcloud.talk.data.database.dao.ChatMessagesDao +import com.nextcloud.talk.data.database.dao.ConversationsDao +import com.nextcloud.talk.data.network.NetworkMonitor import com.nextcloud.talk.data.source.local.TalkDatabase import com.nextcloud.talk.data.storage.ArbitraryStoragesRepository import com.nextcloud.talk.data.storage.ArbitraryStoragesRepositoryImpl @@ -51,6 +59,7 @@ import com.nextcloud.talk.translate.repositories.TranslateRepositoryImpl import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.utils.DateUtils import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import com.nextcloud.talk.utils.preferences.AppPreferences import dagger.Module import dagger.Provides import okhttp3.OkHttpClient @@ -97,8 +106,12 @@ class RepositoryModule { } @Provides - fun provideReactionsRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): ReactionsRepository { - return ReactionsRepositoryImpl(ncApi, userProvider) + fun provideReactionsRepository( + ncApi: NcApi, + userProvider: CurrentUserProviderNew, + dao: ChatMessagesDao + ): ReactionsRepository { + return ReactionsRepositoryImpl(ncApi, userProvider, dao) } @Provides @@ -128,13 +141,13 @@ class RepositoryModule { } @Provides - fun provideConversationsListRepository(ncApi: NcApi): ConversationsListRepository { - return ConversationsListRepositoryImpl(ncApi) + fun provideChatNetworkDataSource(ncApi: NcApi): ChatNetworkDataSource { + return RetrofitChatNetwork(ncApi) } @Provides - fun provideChatRepository(ncApi: NcApi): ChatRepository { - return NetworkChatRepositoryImpl(ncApi) + fun provideConversationsNetworkDataSource(ncApi: NcApi): ConversationsNetworkDataSource { + return RetrofitConversationsNetwork(ncApi) } @Provides @@ -155,6 +168,34 @@ class RepositoryModule { return InvitationsRepositoryImpl(ncApi) } + @Provides + fun provideOfflineFirstChatRepository( + chatMessagesDao: ChatMessagesDao, + chatBlocksDao: ChatBlocksDao, + dataSource: ChatNetworkDataSource, + appPreferences: AppPreferences, + networkMonitor: NetworkMonitor, + userProvider: CurrentUserProviderNew + ): ChatMessageRepository { + return OfflineFirstChatRepository( + chatMessagesDao, + chatBlocksDao, + dataSource, + appPreferences, + networkMonitor, + userProvider + ) + } + + @Provides + fun provideOfflineFirstConversationsRepository( + dao: ConversationsDao, + dataSource: ConversationsNetworkDataSource, + currentUserProviderNew: CurrentUserProviderNew + ): OfflineConversationsRepository { + return OfflineFirstConversationsRepository(dao, dataSource, currentUserProviderNew) + } + @Provides fun provideContactsRepository(ncApiCoroutines: NcApiCoroutines, userManager: UserManager): ContactsRepository { return ContactsRepositoryImpl(ncApiCoroutines, userManager) diff --git a/app/src/main/java/com/nextcloud/talk/data/changeListVersion/SyncableModel.kt b/app/src/main/java/com/nextcloud/talk/data/changeListVersion/SyncableModel.kt new file mode 100644 index 000000000..6173d9e9b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/changeListVersion/SyncableModel.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.changeListVersion + +/** + * Models any changes from the network, agnostic to what data is being modeled. + * Implemented by Models that support offline synchronization. + */ +interface SyncableModel { + + /** + * Model identifier. + */ + var id: Long + + /** + * Model deletion checker. + */ + var markedForDeletion: Boolean +} diff --git a/app/src/main/java/com/nextcloud/talk/data/database/dao/ChatBlocksDao.kt b/app/src/main/java/com/nextcloud/talk/data/database/dao/ChatBlocksDao.kt new file mode 100644 index 000000000..f9efffe95 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/database/dao/ChatBlocksDao.kt @@ -0,0 +1,92 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.database.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.nextcloud.talk.data.database.model.ChatBlockEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface ChatBlocksDao { + @Delete + fun deleteChatBlocks(blocks: List<ChatBlockEntity>) + + @Query( + """ + SELECT * + FROM ChatBlocks + WHERE internalConversationId in (:internalConversationId) + ORDER BY newestMessageId ASC + """ + ) + fun getChatBlocks(internalConversationId: String): Flow<List<ChatBlockEntity>> + + // @Query( + // """ + // SELECT * + // FROM ChatBlocks + // WHERE internalConversationId in (:internalConversationId) + // AND newestMessageId >= :messageId + // ORDER BY newestMessageId ASC + // """ + // ) + // fun getChatBlocksThatReachMessageId( + // internalConversationId: String, + // messageId: Long + // ): + // Flow<List<ChatBlockEntity>> + + @Query( + """ + SELECT * + FROM ChatBlocks + WHERE internalConversationId in (:internalConversationId) + AND oldestMessageId <= :messageId + AND newestMessageId >= :messageId + ORDER BY newestMessageId ASC + """ + ) + fun getChatBlocksContainingMessageId(internalConversationId: String, messageId: Long): Flow<List<ChatBlockEntity?>> + + + @Query( + """ + SELECT * + FROM ChatBlocks + WHERE internalConversationId = :internalConversationId + AND( + (oldestMessageId <= :oldestMessageId AND newestMessageId >= :oldestMessageId) + OR + (oldestMessageId <= :newestMessageId AND newestMessageId >= :newestMessageId) + OR + (oldestMessageId >= :oldestMessageId AND newestMessageId <= :newestMessageId) + ) + ORDER BY newestMessageId ASC + """ + ) + fun getConnectedChatBlocks( + internalConversationId: String, + oldestMessageId: Long, + newestMessageId: Long + ): Flow<List<ChatBlockEntity>> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsertChatBlock(chatBlock: ChatBlockEntity) + + @Query( + """ + DELETE FROM ChatBlocks + WHERE internalConversationId LIKE :pattern + """ + ) + fun clearChatBlocksForUser(pattern: String) +} 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 new file mode 100644 index 000000000..bb0ab220b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/database/dao/ChatMessagesDao.kt @@ -0,0 +1,134 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.database.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import com.nextcloud.talk.data.database.model.ChatMessageEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface ChatMessagesDao { + @Query( + """ + SELECT MAX(id) as max_items + FROM ChatMessages + WHERE internalConversationId = :internalConversationId + """ + ) + fun getNewestMessageId(internalConversationId: String): Long + + @Query( + """ + SELECT * + FROM ChatMessages + WHERE internalConversationId = :internalConversationId + ORDER BY timestamp DESC, id DESC + """ + ) + fun getMessagesForConversation(internalConversationId: String): Flow<List<ChatMessageEntity>> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsertChatMessages(chatMessages: List<ChatMessageEntity>) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsertChatMessage(chatMessage: ChatMessageEntity) + + @Query( + """ + SELECT * + FROM ChatMessages + WHERE internalConversationId = :internalConversationId AND id = :messageId + """ + ) + fun getChatMessageForConversation(internalConversationId: String, messageId: Long): Flow<ChatMessageEntity> + + @Query( + value = """ + DELETE FROM ChatMessages + WHERE id in (:messageIds) + """ + ) + fun deleteChatMessages(messageIds: List<Int>) + + @Update + fun updateChatMessage(message: ChatMessageEntity) + + @Query( + """ + SELECT * + FROM ChatMessages + WHERE id in (:messageIds) + ORDER BY timestamp ASC, id ASC + """ + ) + fun getMessagesFromIds(messageIds: List<Long>): Flow<List<ChatMessageEntity>> + + @Query( + """ + SELECT * + FROM ChatMessages + WHERE internalConversationId = :internalConversationId AND id >= :messageId + ORDER BY timestamp ASC, id ASC + """ + ) + fun getMessagesForConversationSince(internalConversationId: String, messageId: Long): Flow<List<ChatMessageEntity>> + + @Query( + """ + SELECT * + FROM ChatMessages + WHERE internalConversationId = :internalConversationId + AND id < :messageId + ORDER BY timestamp DESC, id DESC + LIMIT :limit + """ + ) + fun getMessagesForConversationBefore( + internalConversationId: String, + messageId: Long, + limit: Int + ): Flow<List<ChatMessageEntity>> + + @Query( + """ + SELECT * + FROM ChatMessages + WHERE internalConversationId = :internalConversationId + AND id <= :messageId + ORDER BY timestamp DESC, id DESC + LIMIT :limit + """ + ) + fun getMessagesForConversationBeforeAndEqual( + internalConversationId: String, + messageId: Long, + limit: Int + ): Flow<List<ChatMessageEntity>> + + @Query( + """ + SELECT COUNT(*) + FROM ChatMessages + WHERE internalConversationId = :internalConversationId + AND id BETWEEN :newestMessageId AND :oldestMessageId + """ + ) + fun getCountBetweenMessageIds(internalConversationId: String, oldestMessageId: Long, newestMessageId: Long): Int + + @Query( + """ + DELETE FROM chatmessages + WHERE internalId LIKE :pattern + """ + ) + fun clearAllMessagesForUser(pattern: String) +} diff --git a/app/src/main/java/com/nextcloud/talk/data/database/dao/ConversationsDao.kt b/app/src/main/java/com/nextcloud/talk/data/database/dao/ConversationsDao.kt new file mode 100644 index 000000000..2e16a51c3 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/database/dao/ConversationsDao.kt @@ -0,0 +1,49 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.database.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Update +import androidx.room.Upsert +import com.nextcloud.talk.data.database.model.ConversationEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface ConversationsDao { + @Query("SELECT * FROM Conversations where accountId = :accountId") + fun getConversationsForUser(accountId: Long): Flow<List<ConversationEntity>> + + @Query("SELECT * FROM Conversations where accountId = :accountId AND token = :token") + fun getConversationForUser(accountId: Long, token: String): Flow<ConversationEntity> + + @Upsert + fun upsertConversations(conversationEntities: List<ConversationEntity>) + + /** + * Deletes rows in the db matching the specified [conversationIds] + */ + @Query( + value = """ + DELETE FROM conversations + WHERE internalId in (:conversationIds) + """ + ) + fun deleteConversation(conversationIds: List<Long>) + + @Update + fun updateConversation(conversationEntity: ConversationEntity) + + @Query( + """ + DELETE FROM conversations + WHERE internalId LIKE :pattern + """ + ) + fun clearAllConversationsForUser(pattern: String) +} 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 new file mode 100644 index 000000000..a8cbb4178 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/database/mappers/ChatMessageMapUtils.kt @@ -0,0 +1,90 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +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.data.database.dao.ChatMessagesDao +import kotlinx.coroutines.flow.first + +fun ChatMessageJson.asEntity(accountId: Long) = + ChatMessageEntity( + // accountId@token@messageId + internalId = "$accountId@$token@$id", + accountId = accountId, + id = id, + internalConversationId = "$accountId@$token", + message = message, + token = token, + actorType = actorType, + actorId = actorId, + actorDisplayName = actorDisplayName, + timestamp = timestamp, + messageParameters = messageParameters, + systemMessageType = systemMessageType, + replyable = replyable, + parentMessageId = parentMessage?.id, + messageType = messageType, + reactions = reactions, + reactionsSelf = reactionsSelf, + expirationTimestamp = expirationTimestamp, + renderMarkdown = renderMarkdown, + lastEditActorDisplayName = lastEditActorDisplayName, + lastEditActorId = lastEditActorId, + lastEditActorType = lastEditActorType, + lastEditTimestamp = lastEditTimestamp + ) + +fun ChatMessageEntity.asModel() = + ChatMessage( + jsonMessageId = id.toInt(), + message = message, + token = token, + actorType = actorType, + actorId = actorId, + actorDisplayName = actorDisplayName, + timestamp = timestamp, + messageParameters = messageParameters, + systemMessageType = systemMessageType, + replyable = replyable, + parentMessageId = parentMessageId, + messageType = messageType, + reactions = reactions, + reactionsSelf = reactionsSelf, + expirationTimestamp = expirationTimestamp, + renderMarkdown = renderMarkdown, + lastEditActorDisplayName = lastEditActorDisplayName, + lastEditActorId = lastEditActorId, + lastEditActorType = lastEditActorType, + lastEditTimestamp = lastEditTimestamp + ) + +fun ChatMessageJson.asModel() = + ChatMessage( + jsonMessageId = id.toInt(), + message = message, + token = token, + actorType = actorType, + actorId = actorId, + actorDisplayName = actorDisplayName, + timestamp = timestamp, + messageParameters = messageParameters, + systemMessageType = systemMessageType, + replyable = replyable, + parentMessageId = parentMessage?.id, + messageType = messageType, + reactions = reactions, + reactionsSelf = reactionsSelf, + expirationTimestamp = expirationTimestamp, + renderMarkdown = renderMarkdown, + lastEditActorDisplayName = lastEditActorDisplayName, + lastEditActorId = lastEditActorId, + lastEditActorType = lastEditActorType, + lastEditTimestamp = lastEditTimestamp + ) diff --git a/app/src/main/java/com/nextcloud/talk/data/database/mappers/ConversationMapUtils.kt b/app/src/main/java/com/nextcloud/talk/data/database/mappers/ConversationMapUtils.kt new file mode 100644 index 000000000..576f8178b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/database/mappers/ConversationMapUtils.kt @@ -0,0 +1,157 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Your Name <your@email.com> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.database.mappers + +import com.bluelinelabs.logansquare.LoganSquare +import com.nextcloud.talk.data.database.model.ConversationEntity +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.chat.ChatMessageJson +import com.nextcloud.talk.models.json.conversations.Conversation + +fun ConversationModel.asEntity() = + ConversationEntity( + internalId = internalId, + token = token, + name = name, + displayName = displayName, + description = description, + type = type, + lastPing = lastPing, + participantType = participantType, + hasPassword = hasPassword, + sessionId = sessionId, + actorId = actorId, + actorType = actorType, + favorite = favorite, + lastActivity = lastActivity, + unreadMessages = unreadMessages, + unreadMention = unreadMention, + // lastMessageId = lastMessage?.id?.toLong(), + objectType = objectType, + notificationLevel = notificationLevel, + conversationReadOnlyState = conversationReadOnlyState, + lobbyState = lobbyState, + lobbyTimer = lobbyTimer, + lastReadMessage = lastReadMessage, + hasCall = hasCall, + callFlag = callFlag, + canStartCall = canStartCall, + canLeaveConversation = canLeaveConversation, + canDeleteConversation = canDeleteConversation, + unreadMentionDirect = unreadMentionDirect, + notificationCalls = notificationCalls, + permissions = permissions, + messageExpiration = messageExpiration, + status = status, + statusIcon = statusIcon, + statusMessage = statusMessage, + statusClearAt = statusClearAt, + callRecording = callRecording, + avatarVersion = avatarVersion, + hasCustomAvatar = hasCustomAvatar, + callStartTime = callStartTime, + recordingConsentRequired = recordingConsentRequired, + remoteServer = remoteServer, + remoteToken = remoteToken + ) + +fun ConversationEntity.asModel() = + ConversationModel( + internalId = internalId, + token = token, + name = name, + displayName = displayName, + description = description, + type = type, + lastPing = lastPing, + participantType = participantType, + hasPassword = hasPassword, + sessionId = sessionId, + actorId = actorId, + actorType = actorType, + favorite = favorite, + lastActivity = lastActivity, + unreadMessages = unreadMessages, + unreadMention = unreadMention, + lastMessageViaConversationList = lastMessageJson?.let + { LoganSquare.parse(lastMessageJson, ChatMessageJson::class.java) }, + objectType = objectType, + notificationLevel = notificationLevel, + conversationReadOnlyState = conversationReadOnlyState, + lobbyState = lobbyState, + lobbyTimer = lobbyTimer, + lastReadMessage = lastReadMessage, + hasCall = hasCall, + callFlag = callFlag, + canStartCall = canStartCall, + canLeaveConversation = canLeaveConversation, + canDeleteConversation = canDeleteConversation, + unreadMentionDirect = unreadMentionDirect, + notificationCalls = notificationCalls, + permissions = permissions, + messageExpiration = messageExpiration, + status = status, + statusIcon = statusIcon, + statusMessage = statusMessage, + statusClearAt = statusClearAt, + callRecording = callRecording, + avatarVersion = avatarVersion, + hasCustomAvatar = hasCustomAvatar, + callStartTime = callStartTime, + recordingConsentRequired = recordingConsentRequired, + remoteServer = remoteServer, + remoteToken = remoteToken + ) + +fun Conversation.asEntity(accountId: Long) = + ConversationEntity( + internalId = "$accountId@$token", + accountId = accountId, + token = token, + name = name, + displayName = displayName, + description = description, + type = type, + lastPing = lastPing, + participantType = participantType, + hasPassword = hasPassword, + sessionId = sessionId, + actorId = actorId, + actorType = actorType, + favorite = favorite, + lastActivity = lastActivity, + unreadMessages = unreadMessages, + unreadMention = unreadMention, + lastMessageJson = lastMessage?.let { LoganSquare.serialize(lastMessage) }, + objectType = objectType, + notificationLevel = notificationLevel, + conversationReadOnlyState = conversationReadOnlyState, + lobbyState = lobbyState, + lobbyTimer = lobbyTimer, + lastReadMessage = lastReadMessage, + hasCall = hasCall, + callFlag = callFlag, + canStartCall = canStartCall, + canLeaveConversation = canLeaveConversation, + canDeleteConversation = canDeleteConversation, + unreadMentionDirect = unreadMentionDirect, + notificationCalls = notificationCalls, + permissions = permissions, + messageExpiration = messageExpiration, + status = status, + statusIcon = statusIcon, + statusMessage = statusMessage, + statusClearAt = statusClearAt, + callRecording = callRecording, + avatarVersion = avatarVersion, + hasCustomAvatar = hasCustomAvatar, + callStartTime = callStartTime, + recordingConsentRequired = recordingConsentRequired, + remoteServer = remoteServer, + remoteToken = remoteToken + ) diff --git a/app/src/main/java/com/nextcloud/talk/data/database/model/ChatBlockEntity.kt b/app/src/main/java/com/nextcloud/talk/data/database/model/ChatBlockEntity.kt new file mode 100644 index 000000000..f1f8cb6a2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/database/model/ChatBlockEntity.kt @@ -0,0 +1,30 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.database.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity( + tableName = "ChatBlocks" + // indices = [ + // androidx.room.Index(value = ["accountId"]) + // ] +) +data class ChatBlockEntity( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = "id") var id: Int = 0, + // accountId@token + @ColumnInfo(name = "internalConversationId") var internalConversationId: String, + // @ColumnInfo(name = "accountId") var accountId: Long? = null, + // @ColumnInfo(name = "token") var token: String?, + @ColumnInfo(name = "oldestMessageId") var oldestMessageId: Long, + @ColumnInfo(name = "newestMessageId") var newestMessageId: Long, + @ColumnInfo(name = "hasHistory") var hasHistory: Boolean +) 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 new file mode 100644 index 000000000..b0c1d69b6 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/database/model/ChatMessageEntity.kt @@ -0,0 +1,63 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.database.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey +import com.nextcloud.talk.chat.data.model.ChatMessage + +@Entity( + tableName = "ChatMessages", + foreignKeys = [ + ForeignKey( + entity = ConversationEntity::class, + parentColumns = arrayOf("internalId"), + childColumns = arrayOf("internalConversationId"), + onDelete = ForeignKey.CASCADE, + onUpdate = ForeignKey.CASCADE + ) + ], + indices = [ + Index(value = ["internalId"], unique = true), + Index(value = ["internalConversationId"]) + ] +) +data class ChatMessageEntity( + @PrimaryKey + // accountId@roomtoken@messageId + @ColumnInfo(name = "internalId") var internalId: String, + @ColumnInfo(name = "accountId") var accountId: Long? = null, + @ColumnInfo(name = "token") var token: String? = null, + @ColumnInfo(name = "id") var id: Long = 0, + // accountId@roomtoken + @ColumnInfo(name = "internalConversationId") var internalConversationId: String? = null, + + @ColumnInfo(name = "actorType") var actorType: String? = null, + @ColumnInfo(name = "actorId") var actorId: String? = null, + @ColumnInfo(name = "actorDisplayName") var actorDisplayName: String? = null, + @ColumnInfo(name = "timestamp") var timestamp: Long = 0, + @ColumnInfo(name = "systemMessage") var systemMessageType: ChatMessage.SystemMessageType? = null, + @ColumnInfo(name = "messageType") var messageType: String? = null, + @ColumnInfo(name = "isReplyable") var replyable: Boolean = false, + // TODO: add "referenceId" + @ColumnInfo(name = "message") var message: String? = null, + @ColumnInfo(name = "messageParameters") var messageParameters: HashMap<String?, HashMap<String?, String?>>? = null, + @ColumnInfo(name = "expirationTimestamp") var expirationTimestamp: Int = 0, + @ColumnInfo(name = "parent") var parentMessageId: Long? = null, + @ColumnInfo(name = "reactions") var reactions: LinkedHashMap<String, Int>? = null, + @ColumnInfo(name = "reactionsSelf") var reactionsSelf: ArrayList<String>? = null, + @ColumnInfo(name = "markdown") var renderMarkdown: Boolean? = null, + @ColumnInfo(name = "lastEditActorType") var lastEditActorType: String? = null, + @ColumnInfo(name = "lastEditActorId") var lastEditActorId: String? = null, + @ColumnInfo(name = "lastEditActorDisplayName") var lastEditActorDisplayName: String? = null, + @ColumnInfo(name = "lastEditTimestamp") var lastEditTimestamp: Long? = 0 + // TODO: add "silent" +) diff --git a/app/src/main/java/com/nextcloud/talk/data/database/model/ConversationEntity.kt b/app/src/main/java/com/nextcloud/talk/data/database/model/ConversationEntity.kt new file mode 100644 index 000000000..116a57c34 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/database/model/ConversationEntity.kt @@ -0,0 +1,91 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.database.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.nextcloud.talk.data.user.model.UserEntity +import com.nextcloud.talk.models.json.conversations.ConversationEnums +import com.nextcloud.talk.models.json.participants.Participant + +@Entity( + tableName = "Conversations", + foreignKeys = [ + ForeignKey( + entity = UserEntity::class, + parentColumns = arrayOf("id"), + childColumns = arrayOf("accountId"), + onDelete = ForeignKey.CASCADE, + onUpdate = ForeignKey.CASCADE + ) + ], + indices = [ + androidx.room.Index(value = ["accountId"]) + ] +) +data class ConversationEntity( + @PrimaryKey + @ColumnInfo(name = "internalId") + var internalId: String, + + // Defines to which talk app account this conversation belongs to + @ColumnInfo(name = "accountId") var accountId: Long? = null, + + // We don't use token as primary key as we have to manage multiple talk app accounts on + // the phone, thus multiple accounts can have the same conversation in their list. That's why the servers + // conversation token is not suitable as primary key on the phone. Also the conversation attributes such as + // "unread message" etc only match a specific account. + // If multiple talk app accounts have the same conversation, it is stored as another dataset, which is + // exactly what we want for this case. + @ColumnInfo(name = "token") var token: String?, + + @ColumnInfo(name = "name") var name: String? = null, + @ColumnInfo(name = "displayName") var displayName: String? = null, + @ColumnInfo(name = "description") var description: String? = null, + @ColumnInfo(name = "type") var type: ConversationEnums.ConversationType? = null, + @ColumnInfo(name = "lastPing") var lastPing: Long = 0, + // TODO FIX type + @ColumnInfo(name = "participantType") var participantType: Participant.ParticipantType? = null, + @ColumnInfo(name = "hasPassword") var hasPassword: Boolean = false, + @ColumnInfo(name = "sessionId") var sessionId: String? = null, + @ColumnInfo(name = "actorId") var actorId: String? = null, + @ColumnInfo(name = "actorType") var actorType: String? = null, + @ColumnInfo(name = "isFavorite") var favorite: Boolean = false, + @ColumnInfo(name = "lastActivity") var lastActivity: Long = 0, + @ColumnInfo(name = "unreadMessages") var unreadMessages: Int = 0, + @ColumnInfo(name = "unreadMention") var unreadMention: Boolean = false, + @ColumnInfo(name = "lastMessageJson") var lastMessageJson: String? = null, + @ColumnInfo(name = "objectType") var objectType: ConversationEnums.ObjectType? = null, + @ColumnInfo(name = "notificationLevel") var notificationLevel: ConversationEnums.NotificationLevel? = null, + @ColumnInfo(name = "readOnly") var conversationReadOnlyState: ConversationEnums.ConversationReadOnlyState? = null, + @ColumnInfo(name = "lobbyState") var lobbyState: ConversationEnums.LobbyState? = null, + @ColumnInfo(name = "lobbyTimer") var lobbyTimer: Long? = null, + @ColumnInfo(name = "lastReadMessage") var lastReadMessage: Int = 0, + @ColumnInfo(name = "hasCall") var hasCall: Boolean = false, + @ColumnInfo(name = "callFlag") var callFlag: Int = 0, + @ColumnInfo(name = "canStartCall") var canStartCall: Boolean = false, + @ColumnInfo(name = "canLeaveConversation") var canLeaveConversation: Boolean? = null, + @ColumnInfo(name = "canDeleteConversation") var canDeleteConversation: Boolean? = null, + @ColumnInfo(name = "unreadMentionDirect") var unreadMentionDirect: Boolean? = null, + @ColumnInfo(name = "notificationCalls") var notificationCalls: Int? = null, + @ColumnInfo(name = "permissions") var permissions: Int = 0, + @ColumnInfo(name = "messageExpiration") var messageExpiration: Int = 0, + @ColumnInfo(name = "status") var status: String? = null, + @ColumnInfo(name = "statusIcon") var statusIcon: String? = null, + @ColumnInfo(name = "statusMessage") var statusMessage: String? = null, + @ColumnInfo(name = "statusClearAt") var statusClearAt: Long? = 0, + @ColumnInfo(name = "callRecording") var callRecording: Int = 0, + @ColumnInfo(name = "avatarVersion") var avatarVersion: String? = null, + @ColumnInfo(name = "isCustomAvatar") var hasCustomAvatar: Boolean? = null, + @ColumnInfo(name = "callStartTime") var callStartTime: Long? = null, + @ColumnInfo(name = "recordingConsent") var recordingConsentRequired: Int = 0, + @ColumnInfo(name = "remoteServer") var remoteServer: String? = null, + @ColumnInfo(name = "remoteToken") var remoteToken: String? = null +) diff --git a/app/src/main/java/com/nextcloud/talk/data/network/NetworkMonitor.kt b/app/src/main/java/com/nextcloud/talk/data/network/NetworkMonitor.kt new file mode 100644 index 000000000..b83f4e2b5 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/network/NetworkMonitor.kt @@ -0,0 +1,17 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.network + +import kotlinx.coroutines.flow.Flow + +/** + * Utility for reporting app connectivity status. + */ +interface NetworkMonitor { + val isOnline: Flow<Boolean> +} diff --git a/app/src/main/java/com/nextcloud/talk/data/network/NetworkMonitorImpl.kt b/app/src/main/java/com/nextcloud/talk/data/network/NetworkMonitorImpl.kt new file mode 100644 index 000000000..d3a77013d --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/network/NetworkMonitorImpl.kt @@ -0,0 +1,83 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.network + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import android.net.NetworkRequest.Builder +import androidx.core.content.getSystemService +import androidx.core.os.trace +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class NetworkMonitorImpl @Inject constructor( + private val context: Context +) : NetworkMonitor { + override val isOnline: Flow<Boolean> = callbackFlow { + trace("NetworkMonitorImpl.callbackFlow") { + val connectivityManager = context.getSystemService<ConnectivityManager>() + if (connectivityManager == null) { + channel.trySend(false) + channel.close() + return@callbackFlow + } + + /** + * The callback's methods are invoked on changes to *any* network matching the [NetworkRequest], + * not just the active network. So we can simply track the presence (or absence) of such [Network]. + */ + val callback = object : ConnectivityManager.NetworkCallback() { + + private val networks = mutableSetOf<Network>() + + override fun onAvailable(network: Network) { + networks += network + channel.trySend(true) + } + + override fun onLost(network: Network) { + networks -= network + channel.trySend(networks.isNotEmpty()) + } + } + + trace("NetworkMonitorImpl.registerNetworkCallback") { + val request = Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build() + connectivityManager.registerNetworkCallback(request, callback) + } + + /** + * Sends the latest connectivity status to the underlying channel. + */ + channel.trySend(connectivityManager.isCurrentlyConnected()) + + awaitClose { + connectivityManager.unregisterNetworkCallback(callback) + } + } + } + .flowOn(Dispatchers.IO) + .conflate() + + private fun ConnectivityManager.isCurrentlyConnected() = + activeNetwork + ?.let(::getNetworkCapabilities) + ?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?: false +} 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 c2d305a32..ab33b3e18 100644 --- a/app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt @@ -1,7 +1,7 @@ /* * Nextcloud Talk - Android Client * - * SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de> + * SPDX-FileCopyrightText: 2023-2024 Marcel Hibbe <dev@mhibbe.de> * SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de> * SPDX-FileCopyrightText: 2017-2020 Mario Danic <mario@lovelyhq.com> * SPDX-License-Identifier: GPL-3.0-or-later @@ -10,15 +10,24 @@ package com.nextcloud.talk.data.source.local import android.content.Context import android.util.Log +import androidx.room.AutoMigration import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters import androidx.sqlite.db.SupportSQLiteDatabase import com.nextcloud.talk.R +import com.nextcloud.talk.data.database.dao.ChatBlocksDao +import com.nextcloud.talk.data.database.dao.ChatMessagesDao +import com.nextcloud.talk.data.database.dao.ConversationsDao +import com.nextcloud.talk.data.database.model.ChatBlockEntity +import com.nextcloud.talk.data.database.model.ChatMessageEntity +import com.nextcloud.talk.data.database.model.ConversationEntity +import com.nextcloud.talk.data.source.local.converters.ArrayListConverter import com.nextcloud.talk.data.source.local.converters.CapabilitiesConverter import com.nextcloud.talk.data.source.local.converters.ExternalSignalingServerConverter import com.nextcloud.talk.data.source.local.converters.HashMapHashMapConverter +import com.nextcloud.talk.data.source.local.converters.LinkedHashMapConverter import com.nextcloud.talk.data.source.local.converters.PushConfigurationConverter import com.nextcloud.talk.data.source.local.converters.ServerVersionConverter import com.nextcloud.talk.data.source.local.converters.SignalingSettingsConverter @@ -31,10 +40,15 @@ import net.sqlcipher.database.SQLiteDatabase import net.sqlcipher.database.SQLiteDatabaseHook import net.sqlcipher.database.SupportFactory import java.util.Locale -import androidx.room.AutoMigration @Database( - entities = [UserEntity::class, ArbitraryStorageEntity::class], + entities = [ + UserEntity::class, + ArbitraryStorageEntity::class, + ConversationEntity::class, + ChatMessageEntity::class, + ChatBlockEntity::class + ], version = 10, autoMigrations = [ AutoMigration(from = 9, to = 10) @@ -47,11 +61,16 @@ import androidx.room.AutoMigration ServerVersionConverter::class, ExternalSignalingServerConverter::class, SignalingSettingsConverter::class, - HashMapHashMapConverter::class + HashMapHashMapConverter::class, + LinkedHashMapConverter::class, + ArrayListConverter::class ) abstract class TalkDatabase : RoomDatabase() { abstract fun usersDao(): UsersDao + abstract fun conversationsDao(): ConversationsDao + abstract fun chatMessagesDao(): ChatMessagesDao + abstract fun chatBlocksDao(): ChatBlocksDao abstract fun arbitraryStoragesDao(): ArbitraryStoragesDao companion object { @@ -89,7 +108,7 @@ abstract class TalkDatabase : RoomDatabase() { return Room .databaseBuilder(context.applicationContext, TalkDatabase::class.java, dbName) // comment out openHelperFactory to view the database entries in Android Studio for debugging - .openHelperFactory(factory) + // .openHelperFactory(factory) // TODO: uncomment when offline support is production ready!!!!!!! .addMigrations(Migrations.MIGRATION_6_8, Migrations.MIGRATION_7_8, Migrations.MIGRATION_8_9) .allowMainThreadQueries() .addCallback( diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/converters/ArrayListConverter.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/ArrayListConverter.kt new file mode 100644 index 000000000..2248c3fc9 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/ArrayListConverter.kt @@ -0,0 +1,38 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.source.local.converters + +import android.util.Log +import androidx.room.TypeConverter +import com.bluelinelabs.logansquare.LoganSquare + +class ArrayListConverter { + + @TypeConverter + fun arrayListToString(list: ArrayList<String>?): String? { + return if (list == null) { + null + } else { + return try { + LoganSquare.serialize(list) + } catch (e: Exception) { + Log.e("ArrayListConverter", "Error parsing array list $list to String $e") + "" + } + } + } + + @TypeConverter + fun stringToArrayList(value: String?): ArrayList<String>? { + if (value.isNullOrEmpty()) { + return null + } + + return LoganSquare.parseList(value, List::class.java) as ArrayList<String>? + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/converters/HashMapHashMapConverter.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/HashMapHashMapConverter.kt index e1ee64772..3e0a10cd0 100644 --- a/app/src/main/java/com/nextcloud/talk/data/source/local/converters/HashMapHashMapConverter.kt +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/HashMapHashMapConverter.kt @@ -12,7 +12,7 @@ import com.bluelinelabs.logansquare.LoganSquare class HashMapHashMapConverter { @TypeConverter - fun fromDoubleHashMapToString(map: HashMap<String, HashMap<String, String>>?): String? { + fun fromDoubleHashMapToString(map: HashMap<String?, HashMap<String?, String?>>?): String? { return if (map == null) { LoganSquare.serialize(hashMapOf<String, HashMap<String, String>>()) } else { @@ -21,11 +21,11 @@ class HashMapHashMapConverter { } @TypeConverter - fun fromStringToDoubleHashMap(value: String?): HashMap<String, HashMap<String, String>>? { + fun fromStringToDoubleHashMap(value: String?): HashMap<String?, HashMap<String?, String?>>? { if (value.isNullOrEmpty()) { return hashMapOf() } - return LoganSquare.parseMap(value, HashMap::class.java) as HashMap<String, HashMap<String, String>>? + return LoganSquare.parseMap(value, HashMap::class.java) as HashMap<String?, HashMap<String?, String?>>? } } diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/converters/LinkedHashMapConverter.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/LinkedHashMapConverter.kt new file mode 100644 index 000000000..206d3dd8c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/LinkedHashMapConverter.kt @@ -0,0 +1,59 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.source.local.converters + +import android.util.Log +import androidx.room.TypeConverter +import com.fasterxml.jackson.core.JsonFactory +import java.io.IOException + +class LinkedHashMapConverter { + + private val converter = LinkedHashMapStringIntConverter() + private val jsonFactory = JsonFactory() + + @TypeConverter + fun stringToLinkedHashMap(value: String?): LinkedHashMap<String, Int> { + if (value.isNullOrEmpty() || value == "{}") { + return linkedMapOf() + } + // "{"👍":1,"👎":1,"😃":1,"😯":1}" // pretend this is value + return try { + val map = linkedMapOf<String, Int>() + val trimmed = value.replace("{", "").replace("}", "") + // "👍":1,"👎":1,"😃":1,"😯":1 + val mapList = trimmed.split(",") + // ["👍":1]["👎":1]["😃":1]["😯":1] + for (mapStr in mapList) { + val emojiMapList = mapStr.split(":") + val emoji = emojiMapList[0].replace("\"", "") // removes double quotes + val count = emojiMapList[1].toInt() + map[emoji] = count + } + // [👍:1],[👎:1],[😃:1],[😯:1] + return map + } catch (e: IOException) { + Log.e("LinkedHashMapConverter", "Error parsing string: $value to linkedHashMap $e") + linkedMapOf() + } + } + + @TypeConverter + fun linkedHashMapToString(map: LinkedHashMap<String, Int>?): String { + return try { + val stringWriter = java.io.StringWriter() + jsonFactory.createGenerator(stringWriter).use { generator -> + converter.serialize(map ?: linkedMapOf(), null, false, generator) + } + stringWriter.toString() + } catch (e: IOException) { + // e.printStackTrace() + "" + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/converters/LinkedHashMapStringIntConverter.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/LinkedHashMapStringIntConverter.kt new file mode 100644 index 000000000..feed96cba --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/LinkedHashMapStringIntConverter.kt @@ -0,0 +1,50 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.source.local.converters + +import com.bluelinelabs.logansquare.typeconverters.TypeConverter +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.core.JsonGenerator +import java.io.IOException + +class LinkedHashMapStringIntConverter : TypeConverter<LinkedHashMap<String, Int>> { + + @Throws(IOException::class) + override fun parse(jsonParser: JsonParser?): LinkedHashMap<String, Int> { + val map: LinkedHashMap<String, Int> = linkedMapOf() + jsonParser?.apply { + while (nextToken() != null) { + val key = text + nextToken() + val value = intValue + map[key] = value + } + } + return map + } + + @Throws(IOException::class) + override fun serialize( + `object`: LinkedHashMap<String, Int>?, + fieldName: String?, + writeFieldNameForObject: Boolean, + jsonGenerator: JsonGenerator? + ) { + jsonGenerator?.apply { + if (fieldName != null) { + writeFieldName(fieldName) + } + writeStartObject() + `object`?.forEach { (key, value) -> + writeFieldName(key) + writeNumber(value) + } + writeEndObject() + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/sync/SyncUtils.kt b/app/src/main/java/com/nextcloud/talk/data/sync/SyncUtils.kt new file mode 100644 index 000000000..3218a9d32 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/sync/SyncUtils.kt @@ -0,0 +1,93 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Your Name <your@email.com> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.sync + +import android.os.Bundle +import android.util.Log +import com.nextcloud.talk.data.changeListVersion.SyncableModel +import kotlin.coroutines.cancellation.CancellationException + +/** + * Interface marker for a class that manages synchronization between local data and a remote + * source for a [Syncable]. + */ +interface Synchronizer { + + // TODO include any other helper functions here that the Synchronizer needs + + /** + * Syntactic sugar to call [Syncable.syncWith] while omitting the synchronizer argument + */ + suspend fun Syncable.sync(bundle: Bundle) = this@sync.syncWith(bundle, this@Synchronizer) +} + +/** + * Interface marker for a class that is synchronized with a remote source. Syncing must not be + * performed concurrently and it is the [Synchronizer]'s responsibility to ensure this. + */ +interface Syncable { + /** + * Synchronizes the local database backing the repository with the network. + * Takes in a [bundle] to retrieve other metadata needed + * + * Returns if the sync was successful or not. + */ + suspend fun syncWith(bundle: Bundle, synchronizer: Synchronizer): Boolean +} + +/** + * Attempts [block], returning a successful [Result] if it succeeds, otherwise a [Result.Failure] + * taking care not to break structured concurrency + */ +private suspend fun <T> suspendRunCatching(block: suspend () -> T): Result<T> = + try { + Result.success(block()) + } catch (cancellationException: CancellationException) { + throw cancellationException + } catch (exception: Exception) { + Log.e( + "suspendRunCatching", + "Failed to evaluate a suspendRunCatchingBlock. Returning failure Result", + exception + ) + Result.failure(exception) + } + +/** + * Utility function for syncing a repository with the network. + * [modelFetcher] Fetches the change list for the model + * [versionUpdater] Updates the version after a successful sync + * [modelDeleter] Deletes models by consuming the ids of the models that have been deleted. + * [modelUpdater] Updates models by consuming the ids of the models that have changed. + * + * Note that the blocks defined above are never run concurrently, and the [Synchronizer] + * implementation must guarantee this. + */ +suspend fun Synchronizer.changeListSync( + modelFetcher: suspend () -> List<SyncableModel>, + versionUpdater: (Long) -> Unit, + modelDeleter: suspend (List<Long>) -> Unit, + modelUpdater: suspend (List<SyncableModel>) -> Unit +) = suspendRunCatching { + // Fetch the change list since last sync (akin to a git fetch) + val changeList = modelFetcher() + if (changeList.isEmpty()) return@suspendRunCatching true + + // Splits the models marked for deletion from the ones that are updated or new + val (deleted, updated) = changeList.partition(SyncableModel::markedForDeletion) + + // Delete models that have been deleted server-side + modelDeleter(deleted.map(SyncableModel::id)) + + // Using the fetch list, pull down and upsert the changes (akin to a git pull) + modelUpdater(updated) + + // Update the last synced version (akin to updating local git HEAD) + val latestVersion = changeList.last().id + versionUpdater(latestVersion) +}.isSuccess diff --git a/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt b/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt index 1a544552e..ae1dc1856 100644 --- a/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt +++ b/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt @@ -29,9 +29,9 @@ import coil.transform.RoundedCornersTransformation import com.nextcloud.talk.R import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.domain.ConversationModel -import com.nextcloud.talk.models.domain.ConversationType -import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.models.json.conversations.ConversationEnums import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.DisplayUtils @@ -49,7 +49,7 @@ fun ImageView.loadConversationAvatar( ): io.reactivex.disposables.Disposable { return loadConversationAvatar( user, - ConversationModel.mapToConversationModel(conversation), + ConversationModel.mapToConversationModel(conversation, user), ignoreCache, viewThemeUtils ) @@ -72,10 +72,10 @@ fun ImageView.loadConversationAvatar( if (conversation.avatarVersion.isNullOrEmpty() && viewThemeUtils != null) { when (conversation.type) { - ConversationType.ROOM_GROUP_CALL -> + ConversationEnums.ConversationType.ROOM_GROUP_CALL -> return loadDefaultGroupCallAvatar(viewThemeUtils) - ConversationType.ROOM_PUBLIC_CALL -> + ConversationEnums.ConversationType.ROOM_PUBLIC_CALL -> return loadDefaultPublicCallAvatar(viewThemeUtils) else -> {} @@ -86,10 +86,10 @@ fun ImageView.loadConversationAvatar( // when no own images are set. (although these default avatars can not be themed for the android app..) val errorPlaceholder = when (conversation.type) { - ConversationType.ROOM_GROUP_CALL -> + ConversationEnums.ConversationType.ROOM_GROUP_CALL -> ContextCompat.getDrawable(context, R.drawable.ic_circular_group) - ConversationType.ROOM_PUBLIC_CALL -> + ConversationEnums.ConversationType.ROOM_PUBLIC_CALL -> ContextCompat.getDrawable(context, R.drawable.ic_circular_link) else -> ContextCompat.getDrawable(context, R.drawable.account_circle_96dp) 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 b60332775..1a26f698e 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/AccountRemovalWorker.java +++ b/app/src/main/java/com/nextcloud/talk/jobs/AccountRemovalWorker.java @@ -16,6 +16,9 @@ import com.nextcloud.talk.R; import com.nextcloud.talk.api.NcApi; import com.nextcloud.talk.application.NextcloudTalkApplication; import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager; +import com.nextcloud.talk.data.database.dao.ChatBlocksDao; +import com.nextcloud.talk.data.database.dao.ChatMessagesDao; +import com.nextcloud.talk.data.database.dao.ConversationsDao; import com.nextcloud.talk.data.user.model.User; import com.nextcloud.talk.models.json.generic.GenericMeta; import com.nextcloud.talk.models.json.generic.GenericOverall; @@ -46,17 +49,19 @@ import retrofit2.Retrofit; public class AccountRemovalWorker extends Worker { public static final String TAG = "AccountRemovalWorker"; - @Inject - UserManager userManager; + @Inject UserManager userManager; - @Inject - ArbitraryStorageManager arbitraryStorageManager; + @Inject ArbitraryStorageManager arbitraryStorageManager; - @Inject - Retrofit retrofit; + @Inject Retrofit retrofit; - @Inject - OkHttpClient okHttpClient; + @Inject OkHttpClient okHttpClient; + + @Inject ChatMessagesDao chatMessagesDao; + + @Inject ConversationsDao conversationsDao; + + @Inject ChatBlocksDao chatBlocksDao; NcApi ncApi; @@ -177,6 +182,7 @@ public class AccountRemovalWorker extends Worker { try { arbitraryStorageManager.deleteAllEntriesForAccountIdentifier(user.getId()); + deleteAllUserInfo(user); deleteUser(user); } catch (Throwable e) { Log.e(TAG, "error while trying to delete All Entries For Account Identifier", e); @@ -184,6 +190,14 @@ public class AccountRemovalWorker extends Worker { } } + private void deleteAllUserInfo(User user) { + String accountId = Objects.requireNonNull(user.getId()).toString(); + String pattern = accountId + "@%"; // ... LIKE "<accountId>@%" + chatMessagesDao.clearAllMessagesForUser(pattern); + conversationsDao.clearAllConversationsForUser(pattern); + chatBlocksDao.clearChatBlocksForUser(pattern); + } + private void deleteUser(User user) { if (user.getId() != null) { String username = user.getUsername(); diff --git a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt index d500b2c9a..870bb9419 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt @@ -49,11 +49,11 @@ import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager import com.nextcloud.talk.callnotification.CallNotificationActivity -import com.nextcloud.talk.chat.data.ChatRepository +import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource import com.nextcloud.talk.models.SignatureVerification import com.nextcloud.talk.models.domain.ConversationModel -import com.nextcloud.talk.models.domain.ConversationType import com.nextcloud.talk.models.json.chat.ChatUtils.Companion.getParsedMessage +import com.nextcloud.talk.models.json.conversations.ConversationEnums import com.nextcloud.talk.models.json.notifications.NotificationOverall import com.nextcloud.talk.models.json.participants.Participant import com.nextcloud.talk.models.json.participants.ParticipantsOverall @@ -125,7 +125,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor @Inject var retrofit: Retrofit? = null - var chatRepository: ChatRepository? = null + var chatNetworkDataSource: ChatNetworkDataSource? = null @Inject set @Inject @@ -231,7 +231,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor bundle.putLong(KEY_INTERNAL_USER_ID, signatureVerification.user!!.id!!) bundle.putBoolean(KEY_FROM_NOTIFICATION_START_CALL, true) - val isOneToOneCall = conversation.type === ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL + val isOneToOneCall = conversation.type === ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL bundle.putBoolean(KEY_ROOM_ONE_TO_ONE, isOneToOneCall) // ggf change in Activity? not necessary???? bundle.putString(BundleKeys.KEY_CONVERSATION_NAME, conversation.name) @@ -300,7 +300,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor checkIfCallIsActive(signatureVerification, conversation) } - chatRepository?.getRoom(userBeingCalled, roomToken = pushMessage.id!!) + chatNetworkDataSource?.getRoom(userBeingCalled, roomToken = pushMessage.id!!) ?.subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread()) ?.subscribe(object : Observer<ConversationModel> { diff --git a/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt b/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt index 1e7690ef0..b7ad1948f 100644 --- a/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt +++ b/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt @@ -7,17 +7,23 @@ */ package com.nextcloud.talk.models.domain +import com.nextcloud.talk.data.changeListVersion.SyncableModel +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.json.chat.ChatMessageJson import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.models.json.conversations.ConversationEnums +import com.nextcloud.talk.models.json.participants.Participant class ConversationModel( + var internalId: String, var roomId: String? = null, var token: String? = null, var name: String? = null, var displayName: String? = null, var description: String? = null, - var type: ConversationType? = null, + var type: ConversationEnums.ConversationType? = null, var lastPing: Long = 0, - var participantType: ParticipantType? = null, + var participantType: Participant.ParticipantType? = null, var hasPassword: Boolean = false, var sessionId: String? = null, var actorId: String? = null, @@ -27,11 +33,12 @@ class ConversationModel( var lastActivity: Long = 0, var unreadMessages: Int = 0, var unreadMention: Boolean = false, - // var lastMessage: .....? = null, - var objectType: ObjectType? = null, - var notificationLevel: NotificationLevel? = null, - var conversationReadOnlyState: ConversationReadOnlyState? = null, - var lobbyState: LobbyState? = null, + // var lastMessageViaConversationList: LastMessageJson? = null, + var lastMessageViaConversationList: ChatMessageJson? = null, + var objectType: ConversationEnums.ObjectType? = null, + var notificationLevel: ConversationEnums.NotificationLevel? = null, + var conversationReadOnlyState: ConversationEnums.ConversationReadOnlyState? = null, + var lobbyState: ConversationEnums.LobbyState? = null, var lobbyTimer: Long? = null, var lastReadMessage: Int = 0, var hasCall: Boolean = false, @@ -53,20 +60,23 @@ class ConversationModel( var callStartTime: Long? = null, var recordingConsentRequired: Int = 0, var remoteServer: String? = null, - var remoteToken: String? = null -) { + var remoteToken: String? = null, + override var id: Long = roomId?.toLong() ?: 0, + override var markedForDeletion: Boolean = false +) : SyncableModel { companion object { - fun mapToConversationModel(conversation: Conversation): ConversationModel { + fun mapToConversationModel(conversation: Conversation, user: User): ConversationModel { return ConversationModel( + internalId = user.id!!.toString() + "@" + conversation.token, roomId = conversation.roomId, token = conversation.token, name = conversation.name, displayName = conversation.displayName, description = conversation.description, - type = conversation.type?.let { ConversationType.valueOf(it.name) }, + type = conversation.type?.let { ConversationEnums.ConversationType.valueOf(it.name) }, lastPing = conversation.lastPing, - participantType = conversation.participantType?.let { ParticipantType.valueOf(it.name) }, + participantType = conversation.participantType?.let { Participant.ParticipantType.valueOf(it.name) }, hasPassword = conversation.hasPassword, sessionId = conversation.sessionId, actorId = conversation.actorId, @@ -77,18 +87,18 @@ class ConversationModel( unreadMessages = conversation.unreadMessages, unreadMention = conversation.unreadMention, // lastMessage = conversation.lastMessage, to do... - objectType = conversation.objectType?.let { ObjectType.valueOf(it.name) }, + objectType = conversation.objectType?.let { ConversationEnums.ObjectType.valueOf(it.name) }, notificationLevel = conversation.notificationLevel?.let { - NotificationLevel.valueOf( + ConversationEnums.NotificationLevel.valueOf( it.name ) }, conversationReadOnlyState = conversation.conversationReadOnlyState?.let { - ConversationReadOnlyState.valueOf( + ConversationEnums.ConversationReadOnlyState.valueOf( it.name ) }, - lobbyState = conversation.lobbyState?.let { LobbyState.valueOf(it.name) }, + lobbyState = conversation.lobbyState?.let { ConversationEnums.LobbyState.valueOf(it.name) }, lobbyTimer = conversation.lobbyTimer, lastReadMessage = conversation.lastReadMessage, hasCall = conversation.hasCall, @@ -116,46 +126,46 @@ class ConversationModel( } } -enum class ConversationType { - DUMMY, - ROOM_TYPE_ONE_TO_ONE_CALL, - ROOM_GROUP_CALL, - ROOM_PUBLIC_CALL, - ROOM_SYSTEM, - FORMER_ONE_TO_ONE, - NOTE_TO_SELF -} - -enum class ParticipantType { - DUMMY, - OWNER, - MODERATOR, - USER, - GUEST, - USER_FOLLOWING_LINK, - GUEST_MODERATOR -} - -enum class ObjectType { - DEFAULT, - SHARE_PASSWORD, - FILE, - ROOM -} - -enum class NotificationLevel { - DEFAULT, - ALWAYS, - MENTION, - NEVER -} - -enum class ConversationReadOnlyState { - CONVERSATION_READ_WRITE, - CONVERSATION_READ_ONLY -} - -enum class LobbyState { - LOBBY_STATE_ALL_PARTICIPANTS, - LOBBY_STATE_MODERATORS_ONLY -} +// enum class ConversationType { +// DUMMY, +// ROOM_TYPE_ONE_TO_ONE_CALL, +// ROOM_GROUP_CALL, +// ROOM_PUBLIC_CALL, +// ROOM_SYSTEM, +// FORMER_ONE_TO_ONE, +// NOTE_TO_SELF +// } +// +// enum class ParticipantType { +// DUMMY, +// OWNER, +// MODERATOR, +// USER, +// GUEST, +// USER_FOLLOWING_LINK, +// GUEST_MODERATOR +// } +// +// enum class ObjectType { +// DEFAULT, +// SHARE_PASSWORD, +// FILE, +// ROOM +// } +// +// enum class NotificationLevel { +// DEFAULT, +// ALWAYS, +// MENTION, +// NEVER +// } +// +// enum class ConversationReadOnlyState { +// CONVERSATION_READ_WRITE, +// CONVERSATION_READ_ONLY +// } +// +// enum class LobbyState { +// LOBBY_STATE_ALL_PARTICIPANTS, +// LOBBY_STATE_MODERATORS_ONLY +// } diff --git a/app/src/main/java/com/nextcloud/talk/models/domain/ReactionAddedModel.kt b/app/src/main/java/com/nextcloud/talk/models/domain/ReactionAddedModel.kt index 35c1160bb..fe95dc03a 100644 --- a/app/src/main/java/com/nextcloud/talk/models/domain/ReactionAddedModel.kt +++ b/app/src/main/java/com/nextcloud/talk/models/domain/ReactionAddedModel.kt @@ -6,7 +6,7 @@ */ package com.nextcloud.talk.models.domain -import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.chat.data.model.ChatMessage data class ReactionAddedModel( var chatMessage: ChatMessage, diff --git a/app/src/main/java/com/nextcloud/talk/models/domain/ReactionDeletedModel.kt b/app/src/main/java/com/nextcloud/talk/models/domain/ReactionDeletedModel.kt index 8f7b97f02..869207a2b 100644 --- a/app/src/main/java/com/nextcloud/talk/models/domain/ReactionDeletedModel.kt +++ b/app/src/main/java/com/nextcloud/talk/models/domain/ReactionDeletedModel.kt @@ -6,7 +6,7 @@ */ package com.nextcloud.talk.models.domain -import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.chat.data.model.ChatMessage data class ReactionDeletedModel( var chatMessage: ChatMessage, diff --git a/app/src/main/java/com/nextcloud/talk/models/domain/converters/DomainEnumNotificationLevelConverter.kt b/app/src/main/java/com/nextcloud/talk/models/domain/converters/DomainEnumNotificationLevelConverter.kt index 889b1d227..2735af4bf 100644 --- a/app/src/main/java/com/nextcloud/talk/models/domain/converters/DomainEnumNotificationLevelConverter.kt +++ b/app/src/main/java/com/nextcloud/talk/models/domain/converters/DomainEnumNotificationLevelConverter.kt @@ -9,25 +9,25 @@ package com.nextcloud.talk.models.domain.converters import com.bluelinelabs.logansquare.typeconverters.IntBasedTypeConverter -import com.nextcloud.talk.models.domain.NotificationLevel +import com.nextcloud.talk.models.json.conversations.ConversationEnums -class DomainEnumNotificationLevelConverter : IntBasedTypeConverter<NotificationLevel>() { - override fun getFromInt(i: Int): NotificationLevel { +class DomainEnumNotificationLevelConverter : IntBasedTypeConverter<ConversationEnums.NotificationLevel>() { + override fun getFromInt(i: Int): ConversationEnums.NotificationLevel { return when (i) { - DEFAULT -> NotificationLevel.DEFAULT - ALWAYS -> NotificationLevel.ALWAYS - MENTION -> NotificationLevel.MENTION - NEVER -> NotificationLevel.NEVER - else -> NotificationLevel.DEFAULT + DEFAULT -> ConversationEnums.NotificationLevel.DEFAULT + ALWAYS -> ConversationEnums.NotificationLevel.ALWAYS + MENTION -> ConversationEnums.NotificationLevel.MENTION + NEVER -> ConversationEnums.NotificationLevel.NEVER + else -> ConversationEnums.NotificationLevel.DEFAULT } } - override fun convertToInt(`object`: NotificationLevel): Int { + override fun convertToInt(`object`: ConversationEnums.NotificationLevel): Int { return when (`object`) { - NotificationLevel.DEFAULT -> DEFAULT - NotificationLevel.ALWAYS -> ALWAYS - NotificationLevel.MENTION -> MENTION - NotificationLevel.NEVER -> NEVER + ConversationEnums.NotificationLevel.DEFAULT -> DEFAULT + ConversationEnums.NotificationLevel.ALWAYS -> ALWAYS + ConversationEnums.NotificationLevel.MENTION -> MENTION + ConversationEnums.NotificationLevel.NEVER -> NEVER else -> DEFAULT } } 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 new file mode 100644 index 000000000..1cbacea25 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessageJson.kt @@ -0,0 +1,49 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Your Name <your@email.com> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.chat + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.data.changeListVersion.SyncableModel +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType +import com.nextcloud.talk.models.json.converters.EnumSystemMessageTypeConverter +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ChatMessageJson( + @JsonField(name = ["id"]) override var id: Long = 0, + @JsonField(name = ["token"]) var token: String? = null, + @JsonField(name = ["actorType"]) var actorType: String? = null, + @JsonField(name = ["actorId"]) var actorId: String? = null, + @JsonField(name = ["actorDisplayName"]) var actorDisplayName: String? = null, + @JsonField(name = ["timestamp"]) var timestamp: Long = 0, + @JsonField(name = ["message"]) var message: String? = null, + + @JsonField(name = ["messageParameters"]) + var messageParameters: HashMap<String?, HashMap<String?, String?>>? = null, + + @JsonField(name = ["systemMessage"], typeConverter = EnumSystemMessageTypeConverter::class) + var systemMessageType: SystemMessageType? = null, + + @JsonField(name = ["isReplyable"]) var replyable: Boolean = false, + @JsonField(name = ["parent"]) var parentMessage: ChatMessageJson? = null, + @JsonField(name = ["messageType"]) var messageType: String? = null, + @JsonField(name = ["reactions"]) var reactions: LinkedHashMap<String, Int>? = null, + @JsonField(name = ["reactionsSelf"]) var reactionsSelf: ArrayList<String>? = null, + @JsonField(name = ["expirationTimestamp"]) var expirationTimestamp: Int = 0, + @JsonField(name = ["markdown"]) var renderMarkdown: Boolean? = null, + @JsonField(name = ["lastEditActorDisplayName"]) var lastEditActorDisplayName: String? = null, + @JsonField(name = ["lastEditActorId"]) var lastEditActorId: String? = null, + @JsonField(name = ["lastEditActorType"]) var lastEditActorType: String? = null, + @JsonField(name = ["lastEditTimestamp"]) var lastEditTimestamp: Long? = 0, + + // override var markedForDeletion: Boolean = "comment_deleted" == messageType + override var markedForDeletion: Boolean = false +) : Parcelable, SyncableModel diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOCS.kt index e1db9062c..d8f27ab98 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOCS.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOCS.kt @@ -19,7 +19,7 @@ data class ChatOCS( @JsonField(name = ["meta"]) var meta: GenericMeta?, @JsonField(name = ["data"]) - var data: List<ChatMessage>? = null + var data: List<ChatMessageJson>? = null ) : Parcelable { // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' constructor() : this(null, null) diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOCSSingleMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOCSSingleMessage.kt index 8a73e0845..63b52c530 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOCSSingleMessage.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOCSSingleMessage.kt @@ -19,7 +19,7 @@ data class ChatOCSSingleMessage( @JsonField(name = ["meta"]) var meta: GenericMeta?, @JsonField(name = ["data"]) - var data: ChatMessage? = null + var data: ChatMessageJson? = null ) : Parcelable { // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' constructor() : this(null, null) diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatShareOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatShareOCS.kt index e95750702..0c8ba7336 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatShareOCS.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatShareOCS.kt @@ -10,14 +10,13 @@ package com.nextcloud.talk.models.json.chat import android.os.Parcelable import com.bluelinelabs.logansquare.annotation.JsonField import com.bluelinelabs.logansquare.annotation.JsonObject -import java.util.HashMap import kotlinx.parcelize.Parcelize @Parcelize @JsonObject data class ChatShareOCS( @JsonField(name = ["data"]) - var data: HashMap<String, ChatMessage>? = null + var data: HashMap<String, ChatMessageJson>? = null ) : Parcelable { // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' constructor() : this(null) diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.kt index acc200897..c8f2da614 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.kt @@ -33,7 +33,7 @@ class ChatUtils { resultMessage?.replace("{$key}", "@" + individualHashMap["name"]) } else if (type == "geo-location") { individualHashMap["name"] - } else if (individualHashMap?.containsKey("link") == true) { + } else if (individualHashMap.containsKey("link") == true) { if (type == "file") { resultMessage?.replace("{$key}", individualHashMap["name"].toString()) } else { diff --git a/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt b/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt index 263e0aa09..85f5a7cbe 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt @@ -12,9 +12,10 @@ package com.nextcloud.talk.models.json.conversations import android.os.Parcelable import com.bluelinelabs.logansquare.annotation.JsonField import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.chat.ChatMessageJson +import com.nextcloud.talk.data.changeListVersion.SyncableModel import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.domain.ConversationModel -import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.models.json.converters.ConversationObjectTypeConverter import com.nextcloud.talk.models.json.converters.EnumLobbyStateConverter import com.nextcloud.talk.models.json.converters.EnumNotificationLevelConverter @@ -39,7 +40,7 @@ data class Conversation( @JsonField(name = ["description"]) var description: String? = null, @JsonField(name = ["type"], typeConverter = EnumRoomTypeConverter::class) - var type: ConversationType? = null, + var type: ConversationEnums.ConversationType? = null, @JsonField(name = ["lastPing"]) var lastPing: Long = 0, @JsonField(name = ["participantType"], typeConverter = EnumParticipantTypeConverter::class) @@ -67,20 +68,21 @@ data class Conversation( @JsonField(name = ["unreadMention"]) var unreadMention: Boolean = false, + // TODO get this from Json -> map to ChatMessage and fix error @JsonField(name = ["lastMessage"]) - var lastMessage: ChatMessage? = null, + var lastMessage: ChatMessageJson? = null, @JsonField(name = ["objectType"], typeConverter = ConversationObjectTypeConverter::class) - var objectType: ObjectType? = null, + var objectType: ConversationEnums.ObjectType? = null, @JsonField(name = ["notificationLevel"], typeConverter = EnumNotificationLevelConverter::class) - var notificationLevel: NotificationLevel? = null, + var notificationLevel: ConversationEnums.NotificationLevel? = null, @JsonField(name = ["readOnly"], typeConverter = EnumReadOnlyConversationConverter::class) - var conversationReadOnlyState: ConversationReadOnlyState? = null, + var conversationReadOnlyState: ConversationEnums.ConversationReadOnlyState? = null, @JsonField(name = ["lobbyState"], typeConverter = EnumLobbyStateConverter::class) - var lobbyState: LobbyState? = null, + var lobbyState: ConversationEnums.LobbyState? = null, @JsonField(name = ["lobbyTimer"]) var lobbyTimer: Long? = null, @@ -149,15 +151,15 @@ data class Conversation( var remoteServer: String? = null, @JsonField(name = ["remoteToken"]) - var remoteToken: String? = null + var remoteToken: String? = null, -) : Parcelable { - // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' - constructor() : this(null, null) + override var id: Long = 0, + override var markedForDeletion: Boolean = false +) : Parcelable, SyncableModel { @Deprecated("Use ConversationUtil") val isPublic: Boolean - get() = ConversationType.ROOM_PUBLIC_CALL == type + get() = ConversationEnums.ConversationType.ROOM_PUBLIC_CALL == type @Deprecated("Use ConversationUtil") val isGuest: Boolean @@ -175,22 +177,27 @@ data class Conversation( fun canModerate(conversationUser: User): Boolean { return isParticipantOwnerOrModerator && !ConversationUtils.isLockedOneToOne( - ConversationModel.mapToConversationModel(this), + ConversationModel.mapToConversationModel(this, conversationUser), conversationUser.capabilities?.spreedCapability!! ) && - type != ConversationType.FORMER_ONE_TO_ONE && - !ConversationUtils.isNoteToSelfConversation(ConversationModel.mapToConversationModel(this)) + type != ConversationEnums.ConversationType.FORMER_ONE_TO_ONE && + !ConversationUtils.isNoteToSelfConversation( + ConversationModel.mapToConversationModel(this, conversationUser) + ) } @Deprecated("Use ConversationUtil") fun isLobbyViewApplicable(conversationUser: User): Boolean { return !canModerate(conversationUser) && - (type == ConversationType.ROOM_GROUP_CALL || type == ConversationType.ROOM_PUBLIC_CALL) + ( + type == ConversationEnums.ConversationType.ROOM_GROUP_CALL || + type == ConversationEnums.ConversationType.ROOM_PUBLIC_CALL + ) } @Deprecated("Use ConversationUtil") fun isNameEditable(conversationUser: User): Boolean { - return canModerate(conversationUser) && ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL != type + return canModerate(conversationUser) && ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL != type } @Deprecated("Use ConversationUtil") @@ -216,41 +223,6 @@ data class Conversation( @Deprecated("Use ConversationUtil") fun isNoteToSelfConversation(): Boolean { - return type == ConversationType.NOTE_TO_SELF - } - - enum class NotificationLevel { - DEFAULT, - ALWAYS, - MENTION, - NEVER - } - - enum class LobbyState { - LOBBY_STATE_ALL_PARTICIPANTS, - LOBBY_STATE_MODERATORS_ONLY - } - - enum class ConversationReadOnlyState { - CONVERSATION_READ_WRITE, - CONVERSATION_READ_ONLY - } - - @Parcelize - enum class ConversationType : Parcelable { - DUMMY, - ROOM_TYPE_ONE_TO_ONE_CALL, - ROOM_GROUP_CALL, - ROOM_PUBLIC_CALL, - ROOM_SYSTEM, - FORMER_ONE_TO_ONE, - NOTE_TO_SELF - } - - enum class ObjectType { - DEFAULT, - SHARE_PASSWORD, - FILE, - ROOM + return type == ConversationEnums.ConversationType.NOTE_TO_SELF } } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/conversations/ConversationEnums.kt b/app/src/main/java/com/nextcloud/talk/models/json/conversations/ConversationEnums.kt new file mode 100644 index 000000000..15be0a664 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/conversations/ConversationEnums.kt @@ -0,0 +1,48 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Your Name <your@email.com> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.conversations + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +class ConversationEnums { + enum class NotificationLevel { + DEFAULT, + ALWAYS, + MENTION, + NEVER + } + + enum class LobbyState { + LOBBY_STATE_ALL_PARTICIPANTS, + LOBBY_STATE_MODERATORS_ONLY + } + + enum class ConversationReadOnlyState { + CONVERSATION_READ_WRITE, + CONVERSATION_READ_ONLY + } + + @Parcelize + enum class ConversationType : Parcelable { + DUMMY, + ROOM_TYPE_ONE_TO_ONE_CALL, + ROOM_GROUP_CALL, + ROOM_PUBLIC_CALL, + ROOM_SYSTEM, + FORMER_ONE_TO_ONE, + NOTE_TO_SELF + } + + enum class ObjectType { + DEFAULT, + SHARE_PASSWORD, + FILE, + ROOM + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/converters/ConversationObjectTypeConverter.kt b/app/src/main/java/com/nextcloud/talk/models/json/converters/ConversationObjectTypeConverter.kt index e1ce5da42..65ffb639a 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/converters/ConversationObjectTypeConverter.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/converters/ConversationObjectTypeConverter.kt @@ -7,27 +7,27 @@ package com.nextcloud.talk.models.json.converters import com.bluelinelabs.logansquare.typeconverters.StringBasedTypeConverter -import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.models.json.conversations.ConversationEnums -class ConversationObjectTypeConverter : StringBasedTypeConverter<Conversation.ObjectType>() { - override fun getFromString(string: String?): Conversation.ObjectType { +class ConversationObjectTypeConverter : StringBasedTypeConverter<ConversationEnums.ObjectType>() { + override fun getFromString(string: String?): ConversationEnums.ObjectType { return when (string) { - "share:password" -> Conversation.ObjectType.SHARE_PASSWORD - "room" -> Conversation.ObjectType.ROOM - "file" -> Conversation.ObjectType.FILE - else -> Conversation.ObjectType.DEFAULT + "share:password" -> ConversationEnums.ObjectType.SHARE_PASSWORD + "room" -> ConversationEnums.ObjectType.ROOM + "file" -> ConversationEnums.ObjectType.FILE + else -> ConversationEnums.ObjectType.DEFAULT } } - override fun convertToString(`object`: Conversation.ObjectType?): String { + override fun convertToString(`object`: ConversationEnums.ObjectType?): String { if (`object` == null) { return "" } return when (`object`) { - Conversation.ObjectType.SHARE_PASSWORD -> "share:password" - Conversation.ObjectType.ROOM -> "room" - Conversation.ObjectType.FILE -> "file" + ConversationEnums.ObjectType.SHARE_PASSWORD -> "share:password" + ConversationEnums.ObjectType.ROOM -> "room" + ConversationEnums.ObjectType.FILE -> "file" else -> "" } } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumLobbyStateConverter.java b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumLobbyStateConverter.java index efe9f8869..51f78ce43 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumLobbyStateConverter.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumLobbyStateConverter.java @@ -8,22 +8,23 @@ package com.nextcloud.talk.models.json.converters; import com.bluelinelabs.logansquare.typeconverters.IntBasedTypeConverter; import com.nextcloud.talk.models.json.conversations.Conversation; +import com.nextcloud.talk.models.json.conversations.ConversationEnums; -public class EnumLobbyStateConverter extends IntBasedTypeConverter<Conversation.LobbyState> { +public class EnumLobbyStateConverter extends IntBasedTypeConverter<ConversationEnums.LobbyState> { @Override - public Conversation.LobbyState getFromInt(int i) { + public ConversationEnums.LobbyState getFromInt(int i) { switch (i) { case 0: - return Conversation.LobbyState.LOBBY_STATE_ALL_PARTICIPANTS; + return ConversationEnums.LobbyState.LOBBY_STATE_ALL_PARTICIPANTS; case 1: - return Conversation.LobbyState.LOBBY_STATE_MODERATORS_ONLY; + return ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY; default: - return Conversation.LobbyState.LOBBY_STATE_ALL_PARTICIPANTS; + return ConversationEnums.LobbyState.LOBBY_STATE_ALL_PARTICIPANTS; } } @Override - public int convertToInt(Conversation.LobbyState object) { + public int convertToInt(ConversationEnums.LobbyState object) { switch (object) { case LOBBY_STATE_ALL_PARTICIPANTS: return 0; diff --git a/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumNotificationLevelConverter.java b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumNotificationLevelConverter.java index 96d425a4d..e38bcc697 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumNotificationLevelConverter.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumNotificationLevelConverter.java @@ -8,26 +8,27 @@ package com.nextcloud.talk.models.json.converters; import com.bluelinelabs.logansquare.typeconverters.IntBasedTypeConverter; import com.nextcloud.talk.models.json.conversations.Conversation; +import com.nextcloud.talk.models.json.conversations.ConversationEnums; -public class EnumNotificationLevelConverter extends IntBasedTypeConverter<Conversation.NotificationLevel> { +public class EnumNotificationLevelConverter extends IntBasedTypeConverter<ConversationEnums.NotificationLevel> { @Override - public Conversation.NotificationLevel getFromInt(int i) { + public ConversationEnums.NotificationLevel getFromInt(int i) { switch (i) { case 0: - return Conversation.NotificationLevel.DEFAULT; + return ConversationEnums.NotificationLevel.DEFAULT; case 1: - return Conversation.NotificationLevel.ALWAYS; + return ConversationEnums.NotificationLevel.ALWAYS; case 2: - return Conversation.NotificationLevel.MENTION; + return ConversationEnums.NotificationLevel.MENTION; case 3: - return Conversation.NotificationLevel.NEVER; + return ConversationEnums.NotificationLevel.NEVER; default: - return Conversation.NotificationLevel.DEFAULT; + return ConversationEnums.NotificationLevel.DEFAULT; } } @Override - public int convertToInt(Conversation.NotificationLevel object) { + public int convertToInt(ConversationEnums.NotificationLevel object) { switch (object) { case DEFAULT: return 0; diff --git a/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumReadOnlyConversationConverter.java b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumReadOnlyConversationConverter.java index 3d20a8eaf..ba76f71f8 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumReadOnlyConversationConverter.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumReadOnlyConversationConverter.java @@ -8,22 +8,23 @@ package com.nextcloud.talk.models.json.converters; import com.bluelinelabs.logansquare.typeconverters.IntBasedTypeConverter; import com.nextcloud.talk.models.json.conversations.Conversation; +import com.nextcloud.talk.models.json.conversations.ConversationEnums; -public class EnumReadOnlyConversationConverter extends IntBasedTypeConverter<Conversation.ConversationReadOnlyState> { +public class EnumReadOnlyConversationConverter extends IntBasedTypeConverter<ConversationEnums.ConversationReadOnlyState> { @Override - public Conversation.ConversationReadOnlyState getFromInt(int i) { + public ConversationEnums.ConversationReadOnlyState getFromInt(int i) { switch (i) { case 0: - return Conversation.ConversationReadOnlyState.CONVERSATION_READ_WRITE; + return ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_WRITE; case 1: - return Conversation.ConversationReadOnlyState.CONVERSATION_READ_ONLY; + return ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_ONLY; default: - return Conversation.ConversationReadOnlyState.CONVERSATION_READ_WRITE; + return ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_WRITE; } } @Override - public int convertToInt(Conversation.ConversationReadOnlyState object) { + public int convertToInt(ConversationEnums.ConversationReadOnlyState object) { switch (object) { case CONVERSATION_READ_WRITE: return 0; diff --git a/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumRoomTypeConverter.java b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumRoomTypeConverter.java index 37e75b260..702e0a6fa 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumRoomTypeConverter.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumRoomTypeConverter.java @@ -7,31 +7,31 @@ package com.nextcloud.talk.models.json.converters; import com.bluelinelabs.logansquare.typeconverters.IntBasedTypeConverter; -import com.nextcloud.talk.models.json.conversations.Conversation; +import com.nextcloud.talk.models.json.conversations.ConversationEnums; -public class EnumRoomTypeConverter extends IntBasedTypeConverter<Conversation.ConversationType> { +public class EnumRoomTypeConverter extends IntBasedTypeConverter<ConversationEnums.ConversationType> { @Override - public Conversation.ConversationType getFromInt(int i) { + public ConversationEnums.ConversationType getFromInt(int i) { switch (i) { case 1: - return Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL; + return ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL; case 2: - return Conversation.ConversationType.ROOM_GROUP_CALL; + return ConversationEnums.ConversationType.ROOM_GROUP_CALL; case 3: - return Conversation.ConversationType.ROOM_PUBLIC_CALL; + return ConversationEnums.ConversationType.ROOM_PUBLIC_CALL; case 4: - return Conversation.ConversationType.ROOM_SYSTEM; + return ConversationEnums.ConversationType.ROOM_SYSTEM; case 5: - return Conversation.ConversationType.FORMER_ONE_TO_ONE; + return ConversationEnums.ConversationType.FORMER_ONE_TO_ONE; case 6: - return Conversation.ConversationType.NOTE_TO_SELF; + return ConversationEnums.ConversationType.NOTE_TO_SELF; default: - return Conversation.ConversationType.DUMMY; + return ConversationEnums.ConversationType.DUMMY; } } @Override - public int convertToInt(Conversation.ConversationType object) { + public int convertToInt(ConversationEnums.ConversationType object) { switch (object) { case DUMMY: return 0; diff --git a/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumSystemMessageTypeConverter.kt b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumSystemMessageTypeConverter.kt index cb4091d17..6cc84fe2b 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumSystemMessageTypeConverter.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumSystemMessageTypeConverter.kt @@ -9,66 +9,66 @@ package com.nextcloud.talk.models.json.converters import com.bluelinelabs.logansquare.typeconverters.StringBasedTypeConverter -import com.nextcloud.talk.models.json.chat.ChatMessage -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.AUDIO_RECORDING_STARTED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.AUDIO_RECORDING_STOPPED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.AVATAR_REMOVED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.AVATAR_SET -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.BREAKOUT_ROOMS_STARTED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.BREAKOUT_ROOMS_STOPPED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CALL_ENDED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CALL_JOINED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CALL_LEFT -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CALL_MISSED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CALL_STARTED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CALL_TRIED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CIRCLE_ADDED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CIRCLE_REMOVED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CLEARED_CHAT -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CONVERSATION_CREATED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CONVERSATION_RENAMED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.DESCRIPTION_REMOVED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.DESCRIPTION_SET -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.DUMMY -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.FILE_SHARED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.GROUP_ADDED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.GROUP_REMOVED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.GUESTS_ALLOWED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.GUESTS_DISALLOWED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.GUEST_MODERATOR_DEMOTED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.GUEST_MODERATOR_PROMOTED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.LISTABLE_ALL -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.LISTABLE_NONE -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.LISTABLE_USERS -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.LOBBY_NONE -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.LOBBY_NON_MODERATORS -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.LOBBY_OPEN_TO_EVERYONE -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.MATTERBRIDGE_CONFIG_ADDED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.MATTERBRIDGE_CONFIG_DISABLED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.MATTERBRIDGE_CONFIG_EDITED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.MATTERBRIDGE_CONFIG_ENABLED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.MATTERBRIDGE_CONFIG_REMOVED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.MESSAGE_DELETED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.MESSAGE_EXPIRATION_DISABLED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.MESSAGE_EXPIRATION_ENABLED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.MODERATOR_DEMOTED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.MODERATOR_PROMOTED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.OBJECT_SHARED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.PASSWORD_REMOVED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.PASSWORD_SET -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.POLL_CLOSED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.POLL_VOTED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.REACTION -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.REACTION_DELETED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.REACTION_REVOKED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.READ_ONLY -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.READ_ONLY_OFF -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.RECORDING_FAILED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.RECORDING_STARTED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.RECORDING_STOPPED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.USER_ADDED -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.USER_REMOVED +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.AUDIO_RECORDING_STARTED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.AUDIO_RECORDING_STOPPED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.AVATAR_REMOVED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.AVATAR_SET +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.BREAKOUT_ROOMS_STARTED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.BREAKOUT_ROOMS_STOPPED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CALL_ENDED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CALL_JOINED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CALL_LEFT +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CALL_MISSED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CALL_STARTED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CALL_TRIED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CIRCLE_ADDED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CIRCLE_REMOVED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CLEARED_CHAT +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CONVERSATION_CREATED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CONVERSATION_RENAMED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.DESCRIPTION_REMOVED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.DESCRIPTION_SET +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.DUMMY +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.FILE_SHARED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.GROUP_ADDED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.GROUP_REMOVED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.GUESTS_ALLOWED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.GUESTS_DISALLOWED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.GUEST_MODERATOR_DEMOTED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.GUEST_MODERATOR_PROMOTED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.LISTABLE_ALL +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.LISTABLE_NONE +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.LISTABLE_USERS +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.LOBBY_NONE +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.LOBBY_NON_MODERATORS +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.LOBBY_OPEN_TO_EVERYONE +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MATTERBRIDGE_CONFIG_ADDED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MATTERBRIDGE_CONFIG_DISABLED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MATTERBRIDGE_CONFIG_EDITED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MATTERBRIDGE_CONFIG_ENABLED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MATTERBRIDGE_CONFIG_REMOVED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MESSAGE_DELETED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MESSAGE_EXPIRATION_DISABLED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MESSAGE_EXPIRATION_ENABLED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MODERATOR_DEMOTED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MODERATOR_PROMOTED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.OBJECT_SHARED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.PASSWORD_REMOVED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.PASSWORD_SET +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.POLL_CLOSED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.POLL_VOTED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.REACTION +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.REACTION_DELETED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.REACTION_REVOKED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.READ_ONLY +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.READ_ONLY_OFF +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.RECORDING_FAILED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.RECORDING_STARTED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.RECORDING_STOPPED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.USER_ADDED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.USER_REMOVED /* * see https://nextcloud-talk.readthedocs.io/en/latest/chat/#system-messages diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/RoomPropertiesWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/RoomPropertiesWebSocketMessage.kt index 7271d8742..d2cca2853 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/websocket/RoomPropertiesWebSocketMessage.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/RoomPropertiesWebSocketMessage.kt @@ -10,7 +10,7 @@ package com.nextcloud.talk.models.json.websocket import android.os.Parcelable import com.bluelinelabs.logansquare.annotation.JsonField import com.bluelinelabs.logansquare.annotation.JsonObject -import com.nextcloud.talk.models.json.conversations.Conversation.ConversationType +import com.nextcloud.talk.models.json.conversations.ConversationEnums import com.nextcloud.talk.models.json.converters.EnumRoomTypeConverter import kotlinx.parcelize.Parcelize @@ -20,7 +20,7 @@ data class RoomPropertiesWebSocketMessage( @JsonField(name = ["name"]) var name: String? = null, @JsonField(name = ["type"], typeConverter = EnumRoomTypeConverter::class) - var roomType: ConversationType? = null + var roomType: ConversationEnums.ConversationType? = null ) : Parcelable { // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' constructor() : this(null, null) diff --git a/app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepositoryImpl.kt index 3142f3686..4b8972096 100644 --- a/app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepositoryImpl.kt @@ -18,7 +18,10 @@ import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew import io.reactivex.Observable -class ConversationsRepositoryImpl(private val api: NcApi, private val userProvider: CurrentUserProviderNew) : +class ConversationsRepositoryImpl( + private val api: NcApi, + private val userProvider: CurrentUserProviderNew +) : ConversationsRepository { private val user: User diff --git a/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepository.kt b/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepository.kt index 068711818..157df4698 100644 --- a/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepository.kt @@ -8,7 +8,7 @@ package com.nextcloud.talk.repositories.reactions import com.nextcloud.talk.models.domain.ReactionAddedModel import com.nextcloud.talk.models.domain.ReactionDeletedModel -import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.chat.data.model.ChatMessage import io.reactivex.Observable interface ReactionsRepository { diff --git a/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepositoryImpl.kt index e6d4e6c44..b84d36a4a 100644 --- a/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepositoryImpl.kt @@ -7,17 +7,26 @@ package com.nextcloud.talk.repositories.reactions import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.data.database.dao.ChatMessagesDao import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.domain.ReactionAddedModel import com.nextcloud.talk.models.domain.ReactionDeletedModel -import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.models.json.generic.GenericMeta import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew import io.reactivex.Observable +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import javax.inject.Inject -class ReactionsRepositoryImpl(private val ncApi: NcApi, currentUserProvider: CurrentUserProviderNew) : - ReactionsRepository { +class ReactionsRepositoryImpl @Inject constructor( + private val ncApi: NcApi, + private val currentUserProvider: CurrentUserProviderNew, + private val dao: ChatMessagesDao +) : ReactionsRepository { val currentUser: User = currentUserProvider.currentUser.blockingGet() val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)!! @@ -31,7 +40,11 @@ class ReactionsRepositoryImpl(private val ncApi: NcApi, currentUserProvider: Cur message.id ), emoji - ).map { mapToReactionAddedModel(message, emoji, it.ocs?.meta!!) } + ).map { + val model = mapToReactionAddedModel(message, emoji, it.ocs?.meta!!) + persistAddedModel(model, roomToken) + return@map model + } } override fun deleteReaction( @@ -47,7 +60,11 @@ class ReactionsRepositoryImpl(private val ncApi: NcApi, currentUserProvider: Cur message.id ), emoji - ).map { mapToReactionDeletedModel(message, emoji, it.ocs?.meta!!) } + ).map { + val model = mapToReactionDeletedModel(message, emoji, it.ocs?.meta!!) + persistDeletedModel(model, roomToken) + return@map model + } } private fun mapToReactionAddedModel( @@ -76,6 +93,66 @@ class ReactionsRepositoryImpl(private val ncApi: NcApi, currentUserProvider: Cur ) } + private fun persistAddedModel(model: ReactionAddedModel, roomToken: String) = + CoroutineScope(Dispatchers.IO).launch { + // 1. Call DAO, Get a singular ChatMessageEntity with model.chatMessage.{PARAM} + val accountId = currentUser.id!! + val id = model.chatMessage.jsonMessageId.toLong() + val internalConversationId = "$accountId@$roomToken" + val emoji = model.emoji + + val message = dao.getChatMessageForConversation(internalConversationId, id).first() + + // 2. Check state of entity, create params as needed + if (message.reactions == null) { + message.reactions = LinkedHashMap() + } + + if (message.reactionsSelf == null) { + message.reactionsSelf = ArrayList() + } + + var amount = message.reactions!![emoji] + if (amount == null) { + amount = 0 + } + message.reactions!![emoji] = amount + 1 + message.reactionsSelf!!.add(emoji) + + // 3. Call DAO again, to update the singular ChatMessageEntity with params + dao.updateChatMessage(message) + } + + private fun persistDeletedModel(model: ReactionDeletedModel, roomToken: String) = + CoroutineScope(Dispatchers.IO).launch { + // 1. Call DAO, Get a singular ChatMessageEntity with model.chatMessage.{PARAM} + val accountId = currentUser.id!! + val id = model.chatMessage.jsonMessageId.toLong() + val internalConversationId = "$accountId@$roomToken" + val emoji = model.emoji + + val message = dao.getChatMessageForConversation(internalConversationId, id).first() + + // 2. Check state of entity, create params as needed + if (message.reactions == null) { + message.reactions = LinkedHashMap() + } + + if (message.reactionsSelf == null) { + message.reactionsSelf = ArrayList() + } + + var amount = message.reactions!![emoji] + if (amount == null) { + amount = 0 + } + message.reactions!![emoji] = amount - 1 + message.reactionsSelf!!.remove(emoji) + + // 3. Call DAO again, to update the singular ChatMessageEntity with params + dao.updateChatMessage(message) + } + companion object { private const val HTTP_OK: Int = 200 private const val HTTP_CREATED: Int = 201 diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepositoryImpl.kt index 28475518d..d306aec83 100644 --- a/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepositoryImpl.kt @@ -26,7 +26,6 @@ import com.nextcloud.talk.utils.DateConstants import com.nextcloud.talk.utils.DateUtils import io.reactivex.Observable import retrofit2.Response -import java.util.HashMap import java.util.Locale import javax.inject.Inject @@ -79,7 +78,7 @@ class SharedItemsRepositoryImpl @Inject constructor(private val ncApi: NcApi, pr val previewAvailable = "yes".equals(fileParameters["preview-available"]!!, ignoreCase = true) - items[it.value.id] = SharedFileItem( + items[it.value.id.toString()] = SharedFileItem( fileParameters["id"]!!, fileParameters["name"]!!, actorParameters["id"]!!, @@ -94,7 +93,7 @@ class SharedItemsRepositoryImpl @Inject constructor(private val ncApi: NcApi, pr ) } else if (it.value.messageParameters?.containsKey("object") == true) { val objectParameters = it.value.messageParameters!!["object"]!! - items[it.value.id] = itemFromObject(objectParameters, actorParameters, dateTime) + items[it.value.id.toString()] = itemFromObject(objectParameters, actorParameters, dateTime) } else { Log.w(TAG, "Item contains neither 'file' or 'object'.") } diff --git a/app/src/main/java/com/nextcloud/talk/ui/bottom/sheet/ProfileBottomSheet.kt b/app/src/main/java/com/nextcloud/talk/ui/bottom/sheet/ProfileBottomSheet.kt index 1204d7ebb..e6bfa53a0 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/bottom/sheet/ProfileBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/bottom/sheet/ProfileBottomSheet.kt @@ -21,7 +21,7 @@ import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.bottomsheet.items.BasicListItemWithImage import com.nextcloud.talk.bottomsheet.items.listItemsWithImage import com.nextcloud.talk.data.user.model.User -import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.models.json.conversations.RoomOverall import com.nextcloud.talk.models.json.hovercard.HoverCardAction import com.nextcloud.talk.models.json.hovercard.HoverCardOverall diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/ConversationsListBottomDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/ConversationsListBottomDialog.kt index a5f8bdbd5..5b1e7fe23 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/ConversationsListBottomDialog.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/ConversationsListBottomDialog.kt @@ -25,12 +25,13 @@ import com.nextcloud.talk.conversationlist.ConversationsListActivity import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.DialogConversationOperationsBinding import com.nextcloud.talk.jobs.LeaveConversationWorker -import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.CapabilitiesUtil +import com.nextcloud.talk.utils.ConversationUtils import com.nextcloud.talk.utils.ShareUtils import com.nextcloud.talk.utils.SpreedFeatures import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID @@ -45,7 +46,7 @@ import javax.inject.Inject class ConversationsListBottomDialog( val activity: ConversationsListActivity, val currentUser: User, - val conversation: Conversation + val conversation: ConversationModel ) : BottomSheetDialog(activity) { private lateinit var binding: DialogConversationOperationsBinding @@ -98,7 +99,7 @@ class ConversationsListBottomDialog( currentUser.capabilities?.spreedCapability!!, SpreedFeatures.FAVORITES ) - val canModerate = conversation.canModerate(currentUser) + val canModerate = ConversationUtils.canModerate(conversation, currentUser.capabilities?.spreedCapability!!) binding.conversationRemoveFromFavorites.visibility = setVisibleIf( hasFavoritesCapability && conversation.favorite @@ -122,10 +123,10 @@ class ConversationsListBottomDialog( ) binding.conversationOperationRename.visibility = setVisibleIf( - conversation.isNameEditable(currentUser) + ConversationUtils.isNameEditable(conversation, currentUser.capabilities!!.spreedCapability!!) ) binding.conversationLinkShare.visibility = setVisibleIf( - !conversation.isNoteToSelfConversation() + !ConversationUtils.isNoteToSelfConversation(conversation) ) binding.conversationOperationDelete.visibility = setVisibleIf( @@ -133,10 +134,10 @@ class ConversationsListBottomDialog( ) binding.conversationOperationLeave.visibility = setVisibleIf( - conversation.canLeave() && + ConversationUtils.canLeave(conversation) && // leaving is by api not possible for the last user with moderator permissions. // for now, hide this option for all moderators. - !conversation.canModerate(currentUser) + !ConversationUtils.canModerate(conversation, currentUser.capabilities!!.spreedCapability!!) ) } @@ -311,7 +312,7 @@ class ConversationsListBottomDialog( private fun markConversationAsRead() { val messageId = if (conversation.remoteServer.isNullOrEmpty()) { - conversation.lastMessage!!.jsonMessageId + conversation.lastMessageViaConversationList?.id } else { null } @@ -323,7 +324,7 @@ class ConversationsListBottomDialog( currentUser.baseUrl!!, conversation.token!! ), - messageId + messageId?.toInt() ) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt index 7fddcf2e9..55f40854f 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt @@ -23,16 +23,15 @@ import com.google.android.material.bottomsheet.BottomSheetDialog import com.nextcloud.talk.R import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.chat.viewmodels.ChatViewModel import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.DialogMessageActionsBinding import com.nextcloud.talk.models.domain.ConversationModel -import com.nextcloud.talk.models.domain.ConversationReadOnlyState -import com.nextcloud.talk.models.domain.ConversationType import com.nextcloud.talk.models.domain.ReactionAddedModel import com.nextcloud.talk.models.domain.ReactionDeletedModel import com.nextcloud.talk.models.json.capabilities.SpreedCapability -import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.models.json.conversations.ConversationEnums import com.nextcloud.talk.repositories.reactions.ReactionsRepository import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.ApiUtils @@ -89,7 +88,7 @@ class MessageActionsDialog( private val isUserAllowedToEdit = chatActivity.userAllowedByPrivilages(message) - private val isMessageEditable = CapabilitiesUtil.hasSpreedFeatureCapability( + private val isMessageEditable = hasSpreedFeatureCapability( spreedCapabilities, SpreedFeatures.EDIT_MESSAGES ) && messageHasRegularText && !isOlderThanTwentyFourHours && isUserAllowedToEdit @@ -108,7 +107,7 @@ class MessageActionsDialog( initMenuItemCopy(!message.isDeleted) val apiVersion = ApiUtils.getConversationApiVersion(user!!, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1)) chatActivity.chatViewModel.checkForNoteToSelf( - ApiUtils.getCredentials(user!!.username, user.token)!!, + ApiUtils.getCredentials(user.username, user.token)!!, ApiUtils.getUrlForRooms( apiVersion, user.baseUrl @@ -136,13 +135,13 @@ class MessageActionsDialog( ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() && CapabilitiesUtil.isTranslationsSupported(spreedCapabilities) ) - initMenuEditorDetails(message.lastEditTimestamp != 0L && !message.isDeleted) + initMenuEditorDetails(message.lastEditTimestamp!! != 0L && !message.isDeleted) initMenuReplyToMessage(message.replyable && hasChatPermission) initMenuReplyPrivately( message.replyable && hasUserId(user) && hasUserActorId(message) && - currentConversation?.type != ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL + currentConversation?.type != ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL ) initMenuEditMessage(isMessageEditable) initMenuDeleteMessage(showMessageDeletionButton) @@ -276,7 +275,7 @@ class MessageActionsDialog( } private fun isPermitted(hasChatPermission: Boolean): Boolean { - return hasChatPermission && ConversationReadOnlyState.CONVERSATION_READ_ONLY != + return hasChatPermission && ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_ONLY != currentConversation?.conversationReadOnlyState } @@ -367,7 +366,7 @@ class MessageActionsDialog( private fun initMenuEditorDetails(showEditorDetails: Boolean) { if (showEditorDetails) { val editedTime = dateUtils.getLocalDateTimeStringFromTimestamp( - message.lastEditTimestamp * + message.lastEditTimestamp!! * DateConstants.SECOND_DIVIDER ) val lastEditorName = message.lastEditActorDisplayName ?: "" diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/ShowReactionsDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/ShowReactionsDialog.kt index e87c2e6fb..8020ffdf0 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/ShowReactionsDialog.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/ShowReactionsDialog.kt @@ -25,10 +25,11 @@ import com.nextcloud.talk.adapters.ReactionItemClickListener import com.nextcloud.talk.adapters.ReactionsAdapter import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.DialogMessageReactionsBinding import com.nextcloud.talk.databinding.ItemReactionsTabBinding -import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.reactions.ReactionsOverall import com.nextcloud.talk.ui.theme.ViewThemeUtils @@ -42,7 +43,7 @@ import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) class ShowReactionsDialog( - activity: Activity, + val activity: Activity, private val roomToken: String, private val chatMessage: ChatMessage, private val user: User?, @@ -86,7 +87,7 @@ class ShowReactionsDialog( if (chatMessage.reactions != null && chatMessage.reactions!!.isNotEmpty()) { var reactionsTotal = 0 for ((emoji, amount) in chatMessage.reactions!!) { - reactionsTotal = reactionsTotal.plus(amount as Int) + reactionsTotal = reactionsTotal.plus(amount) val tab: TabLayout.Tab = binding.emojiReactionsTabs.newTab() // Create a new Tab names "First Tab" val itemBinding = ItemReactionsTabBinding.inflate(layoutInflater) @@ -163,7 +164,7 @@ class ShowReactionsDialog( } } - Collections.sort(reactionVoters, ReactionComparator(user?.userId)) + Collections.sort(reactionVoters, ReactionComparator(user.userId)) adapter?.list?.addAll(reactionVoters) adapter?.notifyDataSetChanged() @@ -185,13 +186,13 @@ class ShowReactionsDialog( override fun onClick(reactionItem: ReactionItem) { if (hasChatPermission && reactionItem.reactionVoter.actorId?.equals(user?.userId) == true) { deleteReaction(chatMessage, reactionItem.reaction!!) + adapter?.list?.remove(reactionItem) dismiss() } } private fun deleteReaction(message: ChatMessage, emoji: String) { val credentials = ApiUtils.getCredentials(user?.username, user?.token) - ncApi.deleteReaction( credentials, ApiUtils.getUrlForMessageReaction( @@ -210,6 +211,7 @@ class ShowReactionsDialog( override fun onNext(genericOverall: GenericOverall) { Log.d(TAG, "deleted reaction: $emoji") + (activity as ChatActivity).updateUiToDeleteReaction(message, emoji) } override fun onError(e: Throwable) { diff --git a/app/src/main/java/com/nextcloud/talk/utils/ConversationUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/ConversationUtils.kt index 623c0d9b9..793ed6eff 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ConversationUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/ConversationUtils.kt @@ -7,52 +7,52 @@ package com.nextcloud.talk.utils import com.nextcloud.talk.models.domain.ConversationModel -import com.nextcloud.talk.models.domain.ConversationType -import com.nextcloud.talk.models.domain.ParticipantType import com.nextcloud.talk.models.json.capabilities.SpreedCapability +import com.nextcloud.talk.models.json.conversations.ConversationEnums +import com.nextcloud.talk.models.json.participants.Participant object ConversationUtils { private val TAG = ConversationUtils::class.java.simpleName fun isPublic(conversation: ConversationModel): Boolean { - return ConversationType.ROOM_PUBLIC_CALL == conversation.type + return ConversationEnums.ConversationType.ROOM_PUBLIC_CALL == conversation.type } fun isGuest(conversation: ConversationModel): Boolean { - return ParticipantType.GUEST == conversation.participantType || - ParticipantType.GUEST_MODERATOR == conversation.participantType || - ParticipantType.USER_FOLLOWING_LINK == conversation.participantType + return Participant.ParticipantType.GUEST == conversation.participantType || + Participant.ParticipantType.GUEST_MODERATOR == conversation.participantType || + Participant.ParticipantType.USER_FOLLOWING_LINK == conversation.participantType } fun isParticipantOwnerOrModerator(conversation: ConversationModel): Boolean { - return ParticipantType.OWNER == conversation.participantType || - ParticipantType.GUEST_MODERATOR == conversation.participantType || - ParticipantType.MODERATOR == conversation.participantType + return Participant.ParticipantType.OWNER == conversation.participantType || + Participant.ParticipantType.GUEST_MODERATOR == conversation.participantType || + Participant.ParticipantType.MODERATOR == conversation.participantType } fun isLockedOneToOne(conversation: ConversationModel, spreedCapabilities: SpreedCapability): Boolean { - return conversation.type == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL && + return conversation.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL && CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.LOCKED_ONE_TO_ONE) } fun canModerate(conversation: ConversationModel, spreedCapabilities: SpreedCapability): Boolean { return isParticipantOwnerOrModerator(conversation) && !isLockedOneToOne(conversation, spreedCapabilities) && - conversation.type != ConversationType.FORMER_ONE_TO_ONE && + conversation.type != ConversationEnums.ConversationType.FORMER_ONE_TO_ONE && !isNoteToSelfConversation(conversation) } fun isLobbyViewApplicable(conversation: ConversationModel, spreedCapabilities: SpreedCapability): Boolean { return !canModerate(conversation, spreedCapabilities) && ( - conversation.type == ConversationType.ROOM_GROUP_CALL || - conversation.type == ConversationType.ROOM_PUBLIC_CALL + conversation.type == ConversationEnums.ConversationType.ROOM_GROUP_CALL || + conversation.type == ConversationEnums.ConversationType.ROOM_PUBLIC_CALL ) } fun isNameEditable(conversation: ConversationModel, spreedCapabilities: SpreedCapability): Boolean { return canModerate(conversation, spreedCapabilities) && - ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL != conversation.type + ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL != conversation.type } fun canLeave(conversation: ConversationModel): Boolean { @@ -75,6 +75,7 @@ object ConversationUtils { } fun isNoteToSelfConversation(currentConversation: ConversationModel?): Boolean { - return currentConversation != null && currentConversation.type == ConversationType.NOTE_TO_SELF + return currentConversation != null && + currentConversation.type == ConversationEnums.ConversationType.NOTE_TO_SELF } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt index f5c1e33c9..fac47d5c6 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt @@ -29,7 +29,7 @@ import com.nextcloud.talk.fullscreenfile.FullScreenTextViewerActivity import com.nextcloud.talk.adapters.messages.PreviewMessageViewHolder import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.jobs.DownloadFileToCacheWorker -import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.utils.AccountUtils.canWeOpenFilesApp import com.nextcloud.talk.utils.Mimetype.AUDIO_MPEG import com.nextcloud.talk.utils.Mimetype.AUDIO_OGG @@ -128,7 +128,7 @@ class FileViewerUtils(private val context: Context, private val user: User) { ) { val file = File(context.cacheDir, fileInfo.fileName) if (file.exists()) { - openFileByMimetype(fileInfo.fileName!!, mimetype) + openFileByMimetype(fileInfo.fileName, mimetype) } else { downloadFileToCache( fileInfo, @@ -267,7 +267,7 @@ class FileViewerUtils(private val context: Context, private val user: User) { openWhenDownloaded: Boolean ) { // check if download worker is already running - val workers = WorkManager.getInstance(context).getWorkInfosByTag(fileInfo.fileId!!) + val workers = WorkManager.getInstance(context).getWorkInfosByTag(fileInfo.fileId) try { for (workInfo in workers.get()) { if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) { diff --git a/app/src/main/java/com/nextcloud/talk/utils/ParticipantPermissions.kt b/app/src/main/java/com/nextcloud/talk/utils/ParticipantPermissions.kt index 05233f1f5..32ec0f48d 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ParticipantPermissions.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/ParticipantPermissions.kt @@ -9,7 +9,6 @@ package com.nextcloud.talk.utils import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.json.capabilities.SpreedCapability -import com.nextcloud.talk.models.json.conversations.Conversation /** * see https://nextcloud-talk.readthedocs.io/en/latest/constants/#attendee-permissions @@ -18,13 +17,6 @@ class ParticipantPermissions( private val spreedCapabilities: SpreedCapability, private val conversation: ConversationModel ) { - - @Deprecated("Use ChatRepository.ConversationModel") - constructor(spreedCapabilities: SpreedCapability, conversation: Conversation) : this( - spreedCapabilities, - ConversationModel.mapToConversationModel(conversation) - ) - val isDefault = (conversation.permissions and DEFAULT) == DEFAULT val isCustom = (conversation.permissions and CUSTOM) == CUSTOM private val canStartCall = (conversation.permissions and START_CALL) == START_CALL diff --git a/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt index 50e8c28fd..53de01b27 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt @@ -77,4 +77,7 @@ object BundleKeys { const val KEY_REMOTE_TALK_SHARE = "KEY_REMOTE_TALK_SHARE" const val KEY_CHAT_API_VERSION = "KEY_CHAT_API_VERSION" const val KEY_CALL_FLAG = "KEY_CALL_FLAG" + const val KEY_CREDENTIALS: String = "KEY_CREDENTIALS" + const val KEY_FIELD_MAP: String = "KEY_FIELD_MAP" + const val KEY_CHAT_URL: String = "KEY_CHAT_URL" } diff --git a/app/src/main/java/com/nextcloud/talk/utils/message/MessageUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/message/MessageUtils.kt index 9ac61bb0a..fa3a3b93c 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/message/MessageUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/message/MessageUtils.kt @@ -14,7 +14,7 @@ import android.text.Spanned import android.util.Log import android.view.View import com.nextcloud.talk.R -import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.DisplayUtils import io.noties.markwon.AbstractMarkwonPlugin diff --git a/app/src/main/java/com/nextcloud/talk/utils/power/PowerManagerUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/power/PowerManagerUtils.kt index e678ada94..d03033fba 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/power/PowerManagerUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/power/PowerManagerUtils.kt @@ -40,7 +40,7 @@ class PowerManagerUtils { init { sharedApplication!!.componentApplication.inject(this) - val pm = context!!.getSystemService(Context.POWER_SERVICE) as PowerManager + val pm = context!!.getSystemService(POWER_SERVICE) as PowerManager fullLock = pm.newWakeLock( PowerManager.SCREEN_BRIGHT_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP, "nctalk:fullwakelock" 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 12683ac50..1769aa23d 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 @@ -10,6 +10,10 @@ package com.nextcloud.talk.utils.preferences; import android.annotation.SuppressLint; +import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel; + +import java.util.List; + @SuppressLint("NonConstantResourceId") public interface AppPreferences { @@ -164,5 +168,14 @@ public interface AppPreferences { Float[] getWaveFormFromFile(String filename); + void saveLastKnownId(String internalConversationId, int lastReadId); + + int getLastKnownId(String internalConversationId, int defaultValue); + + void saveMessageQueue(String internalConversationId, List<MessageInputViewModel.QueuedMessage> queue); + + List<MessageInputViewModel.QueuedMessage> getMessageQueue(String internalConversationId); + + void clear(); } 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 df45c5e1f..849755d8b 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 @@ -7,6 +7,7 @@ package com.nextcloud.talk.utils.preferences import android.content.Context +import android.util.Log import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey @@ -15,6 +16,7 @@ import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import com.nextcloud.talk.R +import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.flow.Flow @@ -460,6 +462,62 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { return if (string.isNotEmpty()) string.convertStringToArray() else floatArrayOf().toTypedArray() } + override fun saveLastKnownId(internalConversationId: String, lastReadId: Int) { + runBlocking<Unit> { + async { + writeString(internalConversationId, lastReadId.toString()) + } + } + } + + override fun getLastKnownId(internalConversationId: String, defaultValue: Int): Int { + val lastReadId = runBlocking { async { readString(internalConversationId).first() } }.getCompleted() + return if (lastReadId.isNotEmpty()) lastReadId.toInt() else defaultValue + } + + override fun saveMessageQueue( + internalConversationId: String, + queue: MutableList<MessageInputViewModel.QueuedMessage>? + ) { + runBlocking<Unit> { + async { + var queueStr = "" + queue?.let { + for (msg in queue) { + val msgStr = "[${msg.message},${msg.replyTo},${msg.displayName},${msg.sendWithoutNotification}]" + queueStr += msgStr + } + } + writeString(internalConversationId + MESSAGE_QUEUE, queueStr) + } + } + } + + override fun getMessageQueue(internalConversationId: String): MutableList<MessageInputViewModel.QueuedMessage> { + val queueStr = + runBlocking { async { readString(internalConversationId + MESSAGE_QUEUE).first() } }.getCompleted() + + val queue: MutableList<MessageInputViewModel.QueuedMessage> = mutableListOf() + if (queueStr.isEmpty()) return queue + + for (msgStr in queueStr.split("]")) { + try { + val msgArray = msgStr.replace("[", "").split(",") + val message = msgArray[0] + val replyTo = msgArray[1].toInt() + val displayName = msgArray[2] + val silent = msgArray[3].toBoolean() + + val qMsg = MessageInputViewModel.QueuedMessage(message, displayName, replyTo, silent) + queue.add(qMsg) + } catch (e: IndexOutOfBoundsException) { + Log.e(TAG, "Message string: $msgStr\n $e") + } + } + + return queue + } + override fun clear() {} private suspend fun writeString(key: String, value: String) = @@ -538,6 +596,7 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { const val DB_ROOM_MIGRATED = "db_room_migrated" const val PHONE_BOOK_INTEGRATION_LAST_RUN = "phone_book_integration_last_run" const val TYPING_STATUS = "typing_status" + const val MESSAGE_QUEUE = "@message_queue" private fun String.convertStringToArray(): Array<Float> { var varString = this val floatList = mutableListOf<Float>() diff --git a/app/src/main/res/layout/fragment_message_input.xml b/app/src/main/res/layout/fragment_message_input.xml index f854414bd..3b5fead29 100644 --- a/app/src/main/res/layout/fragment_message_input.xml +++ b/app/src/main/res/layout/fragment_message_input.xml @@ -10,8 +10,20 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" + xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical"> + <com.google.android.material.textview.MaterialTextView + android:id="@+id/fragment_connection_lost" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center" + android:text="@string/connection_lost_sent_messages_are_queued" + android:textColor="@color/white" + android:background="@color/hwSecurityRed" + android:visibility="gone" + tools:visibility="visible" /> + <include android:id="@+id/fragment_editView" layout="@layout/edit_message_view" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f7de089f5..d8d507bce 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -795,6 +795,9 @@ How to translate with transifex: <string name="ban_participant">Ban participant</string> <string name="show_banned_participants">Show banned participants</string> <string name="bans_list">Bans list</string> + <string name="connection_lost_sent_messages_are_queued">Connection lost - Sent messages are queued</string> + <string name="connection_gained">Connection gained</string> + <string name="message_deleted_by_you">Message deleted by you</string> <string name="unban">Unban</string> <string name="internal_note">Internal note</string> <string name="ban">Ban</string> diff --git a/app/src/test/java/com/nextcloud/talk/utils/ParticipantPermissionsTest.kt b/app/src/test/java/com/nextcloud/talk/utils/ParticipantPermissionsTest.kt index be75924ce..2b4ab0b39 100644 --- a/app/src/test/java/com/nextcloud/talk/utils/ParticipantPermissionsTest.kt +++ b/app/src/test/java/com/nextcloud/talk/utils/ParticipantPermissionsTest.kt @@ -17,7 +17,7 @@ class ParticipantPermissionsTest : TestCase() { @Test fun test_areFlagsSet() { val spreedCapability = SpreedCapability() - val conversation = Conversation() + val conversation = Conversation(null, null) conversation.permissions = ParticipantPermissions.PUBLISH_SCREEN or ParticipantPermissions.JOIN_CALL or ParticipantPermissions.DEFAULT diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index a18342eed..e89bbbfc8 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -255,7 +255,10 @@ <trusted-key id="FC411CD3CB7DCB0ABC9801058118B3BCDB1A5000" group="jakarta.xml.bind"/> <trusted-key id="FF6E2C001948C5F2F38B0CC385911F425EC61B51"> <trusting group="junit"/> + <trusting group="org.apiguardian" name="apiguardian-api" version="1.1.2"/> <trusting group="org.junit"/> + <trusting group="org.opentest4j"/> + <trusting group="^org[.]junit($|([.].*))" regex="true"/> </trusted-key> </trusted-keys> </configuration> @@ -1045,6 +1048,9 @@ </artifact> </component> <component group="androidx.lifecycle" name="lifecycle-common" version="2.3.1"> + <artifact name="lifecycle-common-2.3.1.jar"> + <sha256 value="15848fb56db32f4c7cdc72b324003183d52a4884d6bf09be708ac7f587d139b5" origin="Generated by Gradle" reason="Artifact is not signed"/> + </artifact> <artifact name="lifecycle-common-2.3.1.module"> <sha256 value="5fb7c8514d8c56cada5e29ef89dc0289e71942ab4cb0b2e6dca137b9dcb8fdd4" origin="Generated by Gradle" reason="Artifact is not signed"/> </artifact> @@ -1669,7 +1675,18 @@ <sha256 value="18a6187c16b8d14b51b6a69e32de4a1416a0dc12cff3357d56fbe5feb70a15f2" origin="Generated by Gradle" reason="Artifact is not signed"/> </artifact> </component> + <component group="androidx.test" name="core-ktx" version="1.5.0"> + <artifact name="core-ktx-1.5.0.aar"> + <sha256 value="f20f34e4bbb52d3085bd67ff3e1e10af7c428a33de5f556650b415346f1bfde2" origin="Generated by Gradle" reason="Artifact is not signed"/> + </artifact> + <artifact name="core-ktx-1.5.0.pom"> + <sha256 value="5f59afc55208b4efb9901f37323e710c66b4bb4304c0e167a24cdb3f9214a58e" origin="Generated by Gradle" reason="Artifact is not signed"/> + </artifact> + </component> <component group="androidx.test" name="monitor" version="1.6.0"> + <artifact name="monitor-1.6.0.aar"> + <sha256 value="05ed2e6ee93271d1d8ffe3f06739e5b621cbe37880b24adf4ce2f49ebd59fda3" origin="Generated by Gradle" reason="Artifact is not signed"/> + </artifact> <artifact name="monitor-1.6.0.pom"> <sha256 value="e6e32c3c34a33e81a64fa79ddd06cd47484594e40aea400bbb370ba102bea238" origin="Generated by Gradle" reason="Artifact is not signed"/> </artifact> @@ -1682,6 +1699,14 @@ <sha256 value="30c0dab3944d63721e2023b27ff35ef343130c9eb7b88f49f331b4e0f2ecfbce" origin="Generated by Gradle" reason="Artifact is not signed"/> </artifact> </component> + <component group="androidx.test" name="orchestrator" version="1.4.2"> + <artifact name="orchestrator-1.4.2.apk"> + <sha256 value="b7a2e7d0184b03e12c7357f3914d539da40b52a11e90815edff1022c655f459b" origin="Generated by Gradle" reason="Artifact is not signed"/> + </artifact> + <artifact name="orchestrator-1.4.2.pom"> + <sha256 value="a7621cc2e0c949ae0630fd390ac70331c5f2a68ac0dc48e8d6af74e5cbd87989" origin="Generated by Gradle" reason="Artifact is not signed"/> + </artifact> + </component> <component group="androidx.test" name="rules" version="1.1.0-alpha3"> <artifact name="rules-1.1.0-alpha3.aar"> <sha256 value="cdffdd4df12f39876f518cfcf66e2bb1e406f68e34394a1f53177bfdf3922f0d" origin="Generated by Gradle" reason="Artifact is not signed"/> @@ -1690,6 +1715,14 @@ <sha256 value="611ceb43cb75b3d3454cec947554bce730e219e7f33c92c1412740fdd8adc830" origin="Generated by Gradle" reason="Artifact is not signed"/> </artifact> </component> + <component group="androidx.test" name="rules" version="1.5.0"> + <artifact name="rules-1.5.0.aar"> + <sha256 value="dd645929c63e24f41be8e6f57bb06f305d3b02ed1b2a7d961647027ae1d9e17f" origin="Generated by Gradle" reason="Artifact is not signed"/> + </artifact> + <artifact name="rules-1.5.0.pom"> + <sha256 value="aa11334fba453ceaccc650ee5894aea940b61df088307384fcdd6473ad2874b0" origin="Generated by Gradle" reason="Artifact is not signed"/> + </artifact> + </component> <component group="androidx.test" name="runner" version="1.5.2"> <artifact name="runner-1.5.2.aar"> <sha256 value="36cd6bc876daa1f183ccd11f9898e094c71f06960fde85a373422959613a44d6" origin="Generated by Gradle" reason="Artifact is not signed"/> @@ -1738,6 +1771,14 @@ <sha256 value="077a4014f3981be443c9af624a09e5ae5c541bea32903c7c76e2a15be420990b" origin="Generated by Gradle" reason="Artifact is not signed"/> </artifact> </component> + <component group="androidx.test.espresso" name="espresso-intents" version="3.5.1"> + <artifact name="espresso-intents-3.5.1.aar"> + <sha256 value="63917bce0b9050166875edb8a60904307418c0731cc2e009feb7aa8dacbd2366" origin="Generated by Gradle" reason="Artifact is not signed"/> + </artifact> + <artifact name="espresso-intents-3.5.1.pom"> + <sha256 value="c2a62f3799477ae340f4ba449ff3c599c8f4de00ffb92a3d076343198d546812" origin="Generated by Gradle" reason="Artifact is not signed"/> + </artifact> + </component> <component group="androidx.test.espresso" name="espresso-web" version="3.5.1"> <artifact name="espresso-web-3.5.1.aar"> <sha256 value="9bd3fd7c88a5e05d515995d2c2c649601eb36a7206e751e4f22018a52621e535" origin="Generated by Gradle" reason="Artifact is not signed"/> @@ -1754,6 +1795,22 @@ <sha256 value="4cff0df04cae25831e821ef2f9129245783460e98d0fd67d8f6824065a134c4e" origin="Generated by Gradle" reason="Artifact is not signed"/> </artifact> </component> + <component group="androidx.test.ext" name="junit" version="1.1.5"> + <artifact name="junit-1.1.5.aar"> + <sha256 value="4307c0e60f5d701db9c59bcd9115af705113c36a9132fa3dbad58db1294e9bfd" origin="Generated by Gradle" reason="Artifact is not signed"/> + </artifact> + <artifact name="junit-1.1.5.pom"> + <sha256 value="4cff0df04cae25831e821ef2f9129245783460e98d0fd67d8f6824065a134c4e" origin="Generated by Gradle" reason="Artifact is not signed"/> + </artifact> + </component> + <component group="androidx.test.ext" name="junit-ktx" version="1.1.5"> + <artifact name="junit-ktx-1.1.5.aar"> + <sha256 value="3f32de8f372bc6300b6d2ff2f219269aefcf7bcea8e876b1e715d35aef0ccc6d" origin="Generated by Gradle" reason="Artifact is not signed"/> + </artifact> + <artifact name="junit-ktx-1.1.5.pom"> + <sha256 value="30d72f918d7860da06d8e23ea0bcbd8f4e40f65e8feba37a29857557d544cddb" origin="Generated by Gradle" reason="Artifact is not signed"/> + </artifact> + </component> <component group="androidx.test.services" name="storage" version="1.4.2"> <artifact name="storage-1.4.2.aar"> <sha256 value="b34861f0cd920cb1089f08c3f27e5865b7f920284cc45f4ed12ef8d6980dac48" origin="Generated by Gradle" reason="Artifact is not signed"/> @@ -1762,6 +1819,14 @@ <sha256 value="9b6301b11212641fa2cea00e48a98d0bf6f9de9fa4eabfdb31828b9a97c3ad89" origin="Generated by Gradle" reason="Artifact is not signed"/> </artifact> </component> + <component group="androidx.test.services" name="test-services" version="1.4.2"> + <artifact name="test-services-1.4.2.apk"> + <sha256 value="c6bc74268b29bdabad8da962e00e2f6fd613c24b42c69e81b258397b4819f156" origin="Generated by Gradle" reason="Artifact is not signed"/> + </artifact> + <artifact name="test-services-1.4.2.pom"> + <sha256 value="c33a2ab686ab27b20ca7c3c3f4a4f81265efd2813ea4266eaa304332d384d4aa" origin="Generated by Gradle" reason="Artifact is not signed"/> + </artifact> + </component> <component group="androidx.tracing" name="tracing" version="1.0.0"> <artifact name="tracing-1.0.0.aar"> <sha256 value="07b8b6139665b884a162eccf97891ca50f7f56831233bf25168ae04f7b568612" origin="Generated by Gradle" reason="Artifact is not signed"/>