Offline support for conversations and chats

Authors: Julius Linus and Marcel Hibbe

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2024-07-17 08:43:11 +02:00
parent b15c1787c2
commit 2408d639e4
No known key found for this signature in database
GPG Key ID: C793F8B59F43CE7B
119 changed files with 5186 additions and 1369 deletions

View File

@ -39,5 +39,6 @@
<option name="previewFile" value="true" /> <option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="RedundantSemicolon" enabled="true" level="ERROR" enabled_by_default="true" /> <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> </profile>
</component> </component>

View File

@ -93,6 +93,12 @@ android {
buildConfigField "String", "PERMISSION_LOCAL_BROADCAST", "\"${localBroadcastPermission}\"" buildConfigField "String", "PERMISSION_LOCAL_BROADCAST", "\"${localBroadcastPermission}\""
} }
testOptions {
unitTests.all {
useJUnitPlatform()
}
}
buildTypes { buildTypes {
release { release {
minifyEnabled false minifyEnabled false
@ -146,7 +152,7 @@ ext {
coilKtVersion = "2.7.0" coilKtVersion = "2.7.0"
daggerVersion = "2.52" daggerVersion = "2.52"
emojiVersion = "1.4.0" emojiVersion = "1.4.0"
fidoVersion = "4.1.0-patch2" fidoVersion = "4.4.0"
lifecycleVersion = '2.8.4' lifecycleVersion = '2.8.4'
okhttpVersion = "4.12.0" okhttpVersion = "4.12.0"
markwonVersion = "4.6.2" markwonVersion = "4.6.2"
@ -157,6 +163,7 @@ ext {
roomVersion = "2.6.1" roomVersion = "2.6.1"
workVersion = "2.9.1" workVersion = "2.9.1"
espressoVersion = "3.6.1" espressoVersion = "3.6.1"
androidxTestVersion = "1.5.0"
media3_version = "1.4.0" media3_version = "1.4.0"
coroutines_version = "1.8.1" coroutines_version = "1.8.1"
mockitoKotlinVersion = "5.4.0" mockitoKotlinVersion = "5.4.0"
@ -170,10 +177,14 @@ configurations.configureEach {
} }
dependencies { 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.compose.runtime:runtime:1.6.8")
implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.datastore:datastore-core:1.1.1' implementation 'androidx.datastore:datastore-core:1.1.1'
implementation 'androidx.datastore:datastore-preferences: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") detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.6")
implementation fileTree(include: ['*'], dir: 'libs') implementation fileTree(include: ['*'], dir: 'libs')
@ -192,7 +203,6 @@ dependencies {
implementation "androidx.work:work-runtime:${workVersion}" implementation "androidx.work:work-runtime:${workVersion}"
implementation "androidx.work:work-rxjava2:${workVersion}" implementation "androidx.work:work-rxjava2:${workVersion}"
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
androidTestImplementation "androidx.work:work-testing:${workVersion}"
implementation 'com.google.android.flexbox:flexbox:3.0.0' implementation 'com.google.android.flexbox:flexbox:3.0.0'
implementation ('com.github.bitfireAT:dav4jvm:2.1.3', { implementation ('com.github.bitfireAT:dav4jvm:2.1.3', {
exclude group: 'org.ogce', module: 'xpp3' // Android comes with its own XmlPullParser 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.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 //compose
implementation(platform("androidx.compose:compose-bom:2024.06.00")) implementation(platform("androidx.compose:compose-bom:2024.06.00"))
@ -305,11 +321,14 @@ dependencies {
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-core:5.12.0' 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' 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 // Espresso core
androidTestImplementation ("androidx.test.espresso:espresso-core:$espressoVersion", { androidTestImplementation ("androidx.test.espresso:espresso-core:$espressoVersion", {
exclude group: 'com.android.support', module: 'support-annotations' 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-contrib:$espressoVersion"
androidTestImplementation "androidx.test.espresso:espresso-web:$espressoVersion" androidTestImplementation "androidx.test.espresso:espresso-web:$espressoVersion"
androidTestImplementation "androidx.test.espresso:espresso-accessibility:$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') androidTestImplementation('com.android.support.test.espresso:espresso-intents:3.0.2')
spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.13.0' spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.13.0'

View File

@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 10, "version": 10,
"identityHash": "1b2dab0ea495c45c9c9ee6e64ba74039", "identityHash": "93ef64fac7a9a811c4a3c2f5a6406f87",
"entities": [ "entities": [
{ {
"tableName": "User", "tableName": "User",
@ -135,12 +135,539 @@
}, },
"indices": [], "indices": [],
"foreignKeys": [] "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": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "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')"
] ]
} }
} }

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -22,21 +22,21 @@ import androidx.core.content.res.ResourcesCompat
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.adapters.items.ConversationItem.ConversationItemViewHolder import com.nextcloud.talk.adapters.items.ConversationItem.ConversationItemViewHolder
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication 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.data.user.model.User
import com.nextcloud.talk.databinding.RvItemConversationWithLastMessageBinding import com.nextcloud.talk.databinding.RvItemConversationWithLastMessageBinding
import com.nextcloud.talk.extensions.loadConversationAvatar import com.nextcloud.talk.extensions.loadConversationAvatar
import com.nextcloud.talk.extensions.loadNoteToSelfAvatar import com.nextcloud.talk.extensions.loadNoteToSelfAvatar
import com.nextcloud.talk.extensions.loadSystemAvatar import com.nextcloud.talk.extensions.loadSystemAvatar
import com.nextcloud.talk.extensions.loadUserAvatar import com.nextcloud.talk.extensions.loadUserAvatar
import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.models.domain.ConversationModel
import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.models.json.conversations.ConversationEnums
import com.nextcloud.talk.models.json.conversations.Conversation.ConversationType
import com.nextcloud.talk.ui.StatusDrawable import com.nextcloud.talk.ui.StatusDrawable
import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability
import com.nextcloud.talk.utils.SpreedFeatures
import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.SpreedFeatures
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFilterable import eu.davidea.flexibleadapter.items.IFilterable
@ -46,7 +46,7 @@ import eu.davidea.viewholders.FlexibleViewHolder
import java.util.regex.Pattern import java.util.regex.Pattern
class ConversationItem( class ConversationItem(
val model: Conversation, val model: ConversationModel,
private val user: User, private val user: User,
private val context: Context, private val context: Context,
private val viewThemeUtils: ViewThemeUtils private val viewThemeUtils: ViewThemeUtils
@ -54,9 +54,10 @@ class ConversationItem(
ISectionable<ConversationItemViewHolder, GenericTextHeaderItem?>, ISectionable<ConversationItemViewHolder, GenericTextHeaderItem?>,
IFilterable<String?> { IFilterable<String?> {
private var header: GenericTextHeaderItem? = null private var header: GenericTextHeaderItem? = null
private val chatMessage = model.lastMessageViaConversationList?.asModel()
constructor( constructor(
conversation: Conversation, conversation: ConversationModel,
user: User, user: User,
activityContext: Context, activityContext: Context,
genericTextHeaderItem: GenericTextHeaderItem?, genericTextHeaderItem: GenericTextHeaderItem?,
@ -127,7 +128,7 @@ class ConversationItem(
} else { } else {
holder.binding.favoriteConversationImageView.visibility = View.GONE 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) val size = DisplayUtils.convertDpToPixel(STATUS_SIZE_IN_DP, appContext)
holder.binding.userStatusImage.visibility = View.VISIBLE holder.binding.userStatusImage.visibility = View.VISIBLE
holder.binding.userStatusImage.setImageDrawable( holder.binding.userStatusImage.setImageDrawable(
@ -149,13 +150,13 @@ class ConversationItem(
private fun showAvatar(holder: ConversationItemViewHolder) { private fun showAvatar(holder: ConversationItemViewHolder) {
holder.binding.dialogAvatar.visibility = View.VISIBLE holder.binding.dialogAvatar.visibility = View.VISIBLE
var shouldLoadAvatar = shouldLoadAvatar(holder) var shouldLoadAvatar = shouldLoadAvatar(holder)
if (ConversationType.ROOM_SYSTEM == model.type) { if (ConversationEnums.ConversationType.ROOM_SYSTEM == model.type) {
holder.binding.dialogAvatar.loadSystemAvatar() holder.binding.dialogAvatar.loadSystemAvatar()
shouldLoadAvatar = false shouldLoadAvatar = false
} }
if (shouldLoadAvatar) { if (shouldLoadAvatar) {
when (model.type) { when (model.type) {
ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> { ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> {
if (!TextUtils.isEmpty(model.name)) { if (!TextUtils.isEmpty(model.name)) {
holder.binding.dialogAvatar.loadUserAvatar( holder.binding.dialogAvatar.loadUserAvatar(
user, user,
@ -168,11 +169,12 @@ class ConversationItem(
} }
} }
ConversationType.ROOM_GROUP_CALL, ConversationEnums.ConversationType.ROOM_GROUP_CALL,
ConversationType.FORMER_ONE_TO_ONE, ConversationEnums.ConversationType.FORMER_ONE_TO_ONE,
ConversationType.ROOM_PUBLIC_CALL -> ConversationEnums.ConversationType.ROOM_PUBLIC_CALL ->
holder.binding.dialogAvatar.loadConversationAvatar(user, model, false, viewThemeUtils) holder.binding.dialogAvatar.loadConversationAvatar(user, model, false, viewThemeUtils)
ConversationType.NOTE_TO_SELF ->
ConversationEnums.ConversationType.NOTE_TO_SELF ->
holder.binding.dialogAvatar.loadNoteToSelfAvatar() holder.binding.dialogAvatar.loadNoteToSelfAvatar()
else -> holder.binding.dialogAvatar.visibility = View.GONE else -> holder.binding.dialogAvatar.visibility = View.GONE
@ -182,7 +184,7 @@ class ConversationItem(
private fun shouldLoadAvatar(holder: ConversationItemViewHolder): Boolean { private fun shouldLoadAvatar(holder: ConversationItemViewHolder): Boolean {
return when (model.objectType) { return when (model.objectType) {
Conversation.ObjectType.SHARE_PASSWORD -> { ConversationEnums.ObjectType.SHARE_PASSWORD -> {
holder.binding.dialogAvatar.setImageDrawable( holder.binding.dialogAvatar.setImageDrawable(
ContextCompat.getDrawable( ContextCompat.getDrawable(
context, context,
@ -192,7 +194,7 @@ class ConversationItem(
false false
} }
Conversation.ObjectType.FILE -> { ConversationEnums.ObjectType.FILE -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
holder.binding.dialogAvatar.loadUserAvatar( holder.binding.dialogAvatar.loadUserAvatar(
viewThemeUtils.talk.themePlaceholderAvatar( viewThemeUtils.talk.themePlaceholderAvatar(
@ -213,7 +215,7 @@ class ConversationItem(
} }
private fun setLastMessage(holder: ConversationItemViewHolder, appContext: Context) { private fun setLastMessage(holder: ConversationItemViewHolder, appContext: Context) {
if (model.lastMessage != null) { if (chatMessage != null) {
holder.binding.dialogDate.visibility = View.VISIBLE holder.binding.dialogDate.visibility = View.VISIBLE
holder.binding.dialogDate.text = DateUtils.getRelativeTimeSpanString( holder.binding.dialogDate.text = DateUtils.getRelativeTimeSpanString(
model.lastActivity * MILLIES, model.lastActivity * MILLIES,
@ -221,20 +223,20 @@ class ConversationItem(
0, 0,
DateUtils.FORMAT_ABBREV_RELATIVE DateUtils.FORMAT_ABBREV_RELATIVE
) )
if (!TextUtils.isEmpty(model.lastMessage!!.systemMessage) || if (!TextUtils.isEmpty(chatMessage?.systemMessage) ||
ConversationType.ROOM_SYSTEM === model.type ConversationEnums.ConversationType.ROOM_SYSTEM === model.type
) { ) {
holder.binding.dialogLastMessage.text = model.lastMessage!!.text holder.binding.dialogLastMessage.text = chatMessage?.text
} else { } else {
model.lastMessage!!.activeUser = user chatMessage?.activeUser = user
val text = val text =
if ( if (
model.lastMessage!!.getCalculateMessageType() === ChatMessage.MessageType.REGULAR_TEXT_MESSAGE chatMessage?.messageType === MessageType.REGULAR_TEXT_MESSAGE.toString()
) { ) {
calculateRegularLastMessageText(appContext) calculateRegularLastMessageText(appContext)
} else { } else {
model.lastMessage!!.lastMessageDisplayText lastMessageDisplayText
} }
holder.binding.dialogLastMessage.text = text holder.binding.dialogLastMessage.text = text
} }
@ -245,16 +247,16 @@ class ConversationItem(
} }
private fun calculateRegularLastMessageText(appContext: Context): String { private fun calculateRegularLastMessageText(appContext: Context): String {
return if (model.lastMessage!!.actorId == user.userId) { return if (chatMessage?.actorId == user.userId) {
String.format( String.format(
appContext.getString(R.string.nc_formatted_message_you), appContext.getString(R.string.nc_formatted_message_you),
model.lastMessage!!.lastMessageDisplayText lastMessageDisplayText
) )
} else { } else {
val authorDisplayName = val authorDisplayName =
if (!TextUtils.isEmpty(model.lastMessage!!.actorDisplayName)) { if (!TextUtils.isEmpty(chatMessage?.actorDisplayName)) {
model.lastMessage!!.actorDisplayName chatMessage?.actorDisplayName
} else if ("guests" == model.lastMessage!!.actorType) { } else if ("guests" == chatMessage?.actorType) {
appContext.getString(R.string.nc_guest) appContext.getString(R.string.nc_guest)
} else { } else {
"" ""
@ -262,7 +264,7 @@ class ConversationItem(
String.format( String.format(
appContext.getString(R.string.nc_formatted_message), appContext.getString(R.string.nc_formatted_message),
authorDisplayName, authorDisplayName,
model.lastMessage!!.lastMessageDisplayText lastMessageDisplayText
) )
} }
} }
@ -286,7 +288,7 @@ class ConversationItem(
context, context,
R.color.conversation_unread_bubble_text 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) viewThemeUtils.material.colorChipBackground(holder.binding.dialogUnreadBubble)
} else if (model.unreadMention) { } else if (model.unreadMention) {
if (hasSpreedFeatureCapability(user.capabilities?.spreedCapability!!, SpreedFeatures.DIRECT_MENTION_FLAG)) { if (hasSpreedFeatureCapability(user.capabilities?.spreedCapability!!, SpreedFeatures.DIRECT_MENTION_FLAG)) {
@ -323,6 +325,94 @@ class ConversationItem(
this.header = header 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) { class ConversationItemViewHolder(view: View?, adapter: FlexibleAdapter<*>?) : FlexibleViewHolder(view, adapter) {
var binding: RvItemConversationWithLastMessageBinding var binding: RvItemConversationWithLastMessageBinding

View File

@ -16,7 +16,7 @@ import coil.target.Target
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.databinding.CallStartedMessageBinding 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.ui.theme.ViewThemeUtils
import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils

View File

@ -6,7 +6,7 @@
*/ */
package com.nextcloud.talk.adapters.messages package com.nextcloud.talk.adapters.messages
import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.chat.data.model.ChatMessage
interface CommonMessageInterface { interface CommonMessageInterface {
fun onLongClickReactions(chatMessage: ChatMessage) fun onLongClickReactions(chatMessage: ChatMessage)

View File

@ -10,26 +10,35 @@ package com.nextcloud.talk.adapters.messages
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.text.TextUtils import android.text.TextUtils
import android.util.Log
import android.view.View import android.view.View
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import autodagger.AutoInjector import autodagger.AutoInjector
import coil.load import coil.load
import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.adapters.messages.OutcomingPollMessageViewHolder.Companion
import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.databinding.ItemCustomIncomingLinkPreviewMessageBinding import com.nextcloud.talk.databinding.ItemCustomIncomingLinkPreviewMessageBinding
import com.nextcloud.talk.extensions.loadBotsAvatar import com.nextcloud.talk.extensions.loadBotsAvatar
import com.nextcloud.talk.extensions.loadChangelogBotAvatar import com.nextcloud.talk.extensions.loadChangelogBotAvatar
import com.nextcloud.talk.extensions.loadFederatedUserAvatar 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.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DateUtils import com.nextcloud.talk.utils.DateUtils
import com.nextcloud.talk.utils.message.MessageUtils import com.nextcloud.talk.utils.message.MessageUtils
import com.nextcloud.talk.utils.preferences.AppPreferences import com.nextcloud.talk.utils.preferences.AppPreferences
import com.stfalcon.chatkit.messages.MessageHolders 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 import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class) @AutoInjector(NextcloudTalkApplication::class)
@ -168,9 +177,24 @@ class IncomingLinkPreviewMessageViewHolder(incomingView: View, payload: Any) :
} }
private fun setParentMessageDataOnMessageItem(message: ChatMessage) { private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (!message.isDeleted && message.parentMessage != null) { if (message.parentMessageId != null && !message.isDeleted) {
val parentChatMessage = message.parentMessage CoroutineScope(Dispatchers.Main).launch {
parentChatMessage!!.activeUser = message.activeUser 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 { parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) { binding.messageQuote.quotedMessageImage.load(it) {
@ -196,12 +220,19 @@ class IncomingLinkPreviewMessageViewHolder(incomingView: View, payload: Any) :
.setTextColor(ContextCompat.getColor(context, R.color.textColorMaxContrast)) .setTextColor(ContextCompat.getColor(context, R.color.textColorMaxContrast))
if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) { if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) {
viewThemeUtils.platform.colorViewBackground(binding.messageQuote.quoteColoredView, ColorRole.PRIMARY) viewThemeUtils.platform.colorViewBackground(
binding.messageQuote.quoteColoredView,
ColorRole.PRIMARY
)
} else { } else {
binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast) binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast)
} }
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
}
} else { } else {
binding.messageQuote.quotedChatMessageView.visibility = View.GONE binding.messageQuote.quotedChatMessageView.visibility = View.GONE
} }

View File

@ -20,18 +20,21 @@ import android.view.MotionEvent
import android.view.View import android.view.View
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import androidx.lifecycle.lifecycleScope
import autodagger.AutoInjector import autodagger.AutoInjector
import coil.load import coil.load
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.talk.R 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
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.databinding.ItemCustomIncomingLocationMessageBinding import com.nextcloud.talk.databinding.ItemCustomIncomingLocationMessageBinding
import com.nextcloud.talk.extensions.loadBotsAvatar import com.nextcloud.talk.extensions.loadBotsAvatar
import com.nextcloud.talk.extensions.loadChangelogBotAvatar import com.nextcloud.talk.extensions.loadChangelogBotAvatar
import com.nextcloud.talk.extensions.loadFederatedUserAvatar 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.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DateUtils 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.message.MessageUtils
import com.nextcloud.talk.utils.preferences.AppPreferences import com.nextcloud.talk.utils.preferences.AppPreferences
import com.stfalcon.chatkit.messages.MessageHolders 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 java.net.URLEncoder
import javax.inject.Inject import javax.inject.Inject
@ -150,9 +158,24 @@ class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) :
} }
private fun setParentMessageDataOnMessageItem(message: ChatMessage) { private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (!message.isDeleted && message.parentMessage != null) { if (message.parentMessageId != null && !message.isDeleted) {
val parentChatMessage = message.parentMessage CoroutineScope(Dispatchers.Main).launch {
parentChatMessage!!.activeUser = message.activeUser 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 { parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) { binding.messageQuote.quotedMessageImage.load(it) {
@ -178,12 +201,19 @@ class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) :
.setTextColor(context.resources.getColor(R.color.textColorMaxContrast, null)) .setTextColor(context.resources.getColor(R.color.textColorMaxContrast, null))
if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) { if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) {
viewThemeUtils.platform.colorViewBackground(binding.messageQuote.quoteColoredView, ColorRole.PRIMARY) viewThemeUtils.platform.colorViewBackground(
binding.messageQuote.quoteColoredView,
ColorRole.PRIMARY
)
} else { } else {
binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast) binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast)
} }
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
}
} else { } else {
binding.messageQuote.quotedChatMessageView.visibility = View.GONE binding.messageQuote.quotedChatMessageView.visibility = View.GONE
} }

View File

@ -9,12 +9,15 @@ package com.nextcloud.talk.adapters.messages
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.text.TextUtils import android.text.TextUtils
import android.util.Log
import android.view.View import android.view.View
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import autodagger.AutoInjector import autodagger.AutoInjector
import coil.load import coil.load
import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.adapters.messages.IncomingTextMessageViewHolder.Companion
import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication 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.loadBotsAvatar
import com.nextcloud.talk.extensions.loadChangelogBotAvatar import com.nextcloud.talk.extensions.loadChangelogBotAvatar
import com.nextcloud.talk.extensions.loadFederatedUserAvatar 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.polls.ui.PollMainDialogFragment
import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.ApiUtils 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.message.MessageUtils
import com.nextcloud.talk.utils.preferences.AppPreferences import com.nextcloud.talk.utils.preferences.AppPreferences
import com.stfalcon.chatkit.messages.MessageHolders 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 import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class) @AutoInjector(NextcloudTalkApplication::class)
@ -176,9 +184,24 @@ class IncomingPollMessageViewHolder(incomingView: View, payload: Any) :
} }
private fun setParentMessageDataOnMessageItem(message: ChatMessage) { private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (!message.isDeleted && message.parentMessage != null) { if (message.parentMessageId != null && !message.isDeleted) {
val parentChatMessage = message.parentMessage CoroutineScope(Dispatchers.Main).launch {
parentChatMessage!!.activeUser = message.activeUser 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 { parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) { binding.messageQuote.quotedMessageImage.load(it) {
@ -204,12 +227,18 @@ class IncomingPollMessageViewHolder(incomingView: View, payload: Any) :
.setTextColor(ContextCompat.getColor(context, R.color.textColorMaxContrast)) .setTextColor(ContextCompat.getColor(context, R.color.textColorMaxContrast))
if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) { if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) {
viewThemeUtils.platform.colorViewBackground(binding.messageQuote.quoteColoredView, ColorRole.PRIMARY) viewThemeUtils.platform.colorViewBackground(
binding.messageQuote.quoteColoredView,
ColorRole.PRIMARY
)
} else { } else {
binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast) binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast)
} }
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
}
} else { } else {
binding.messageQuote.quotedChatMessageView.visibility = View.GONE binding.messageQuote.quotedChatMessageView.visibility = View.GONE
} }

View File

@ -18,7 +18,7 @@ import com.google.android.material.card.MaterialCardView;
import com.nextcloud.talk.R; import com.nextcloud.talk.R;
import com.nextcloud.talk.databinding.ItemCustomIncomingPreviewMessageBinding; import com.nextcloud.talk.databinding.ItemCustomIncomingPreviewMessageBinding;
import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding; 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 com.nextcloud.talk.utils.TextMatchers;
import java.util.HashMap; import java.util.HashMap;

View File

@ -11,9 +11,11 @@ package com.nextcloud.talk.adapters.messages
import android.content.Context import android.content.Context
import android.text.TextUtils import android.text.TextUtils
import android.util.Log
import android.util.TypedValue import android.util.TypedValue
import android.view.View import android.view.View
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import autodagger.AutoInjector import autodagger.AutoInjector
import coil.load import coil.load
import com.nextcloud.android.common.ui.theme.utils.ColorRole 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.loadBotsAvatar
import com.nextcloud.talk.extensions.loadChangelogBotAvatar import com.nextcloud.talk.extensions.loadChangelogBotAvatar
import com.nextcloud.talk.extensions.loadFederatedUserAvatar 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.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DateUtils 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.message.MessageUtils
import com.nextcloud.talk.utils.preferences.AppPreferences import com.nextcloud.talk.utils.preferences.AppPreferences
import com.stfalcon.chatkit.messages.MessageHolders 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 import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class) @AutoInjector(NextcloudTalkApplication::class)
@ -99,14 +108,14 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
if (message.lastEditTimestamp != 0L && !message.isDeleted) { if (message.lastEditTimestamp != 0L && !message.isDeleted) {
binding.messageEditIndicator.visibility = View.VISIBLE binding.messageEditIndicator.visibility = View.VISIBLE
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.lastEditTimestamp) binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.lastEditTimestamp!!)
} else { } else {
binding.messageEditIndicator.visibility = View.GONE binding.messageEditIndicator.visibility = View.GONE
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
} }
// parent message handling // parent message handling
if (!message.isDeleted && message.parentMessage != null) { if (!message.isDeleted && message.parentMessageId != null) {
processParentMessage(message) processParentMessage(message)
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} else { } else {
@ -176,8 +185,25 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
} }
private fun processParentMessage(message: ChatMessage) { private fun processParentMessage(message: ChatMessage) {
val parentChatMessage = message.parentMessage if (message.parentMessageId != null && !message.isDeleted) {
parentChatMessage!!.activeUser = message.activeUser 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 { parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) { binding.messageQuote.quotedMessageImage.load(it) {
@ -189,7 +215,8 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
} ?: run { } ?: run {
binding.messageQuote.quotedMessageImage.visibility = View.GONE binding.messageQuote.quotedMessageImage.visibility = View.GONE
} }
binding.messageQuote.quotedMessageAuthor.text = if (parentChatMessage.actorDisplayName.isNullOrEmpty()) { binding.messageQuote.quotedMessageAuthor.text =
if (parentChatMessage.actorDisplayName.isNullOrEmpty()) {
context.getText(R.string.nc_nick_guest) context.getText(R.string.nc_nick_guest)
} else { } else {
parentChatMessage.actorDisplayName parentChatMessage.actorDisplayName
@ -204,10 +231,16 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
) )
if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) { if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) {
viewThemeUtils.platform.colorViewBackground(binding.messageQuote.quoteColoredView, ColorRole.PRIMARY) viewThemeUtils.platform.colorViewBackground(
binding.messageQuote.quoteColoredView,
ColorRole.PRIMARY
)
} else { } else {
binding.messageQuote.quoteColoredView.setBackgroundColor( binding.messageQuote.quoteColoredView.setBackgroundColor(
ContextCompat.getColor(binding.messageQuote.quoteColoredView.context, R.color.high_emphasis_text) ContextCompat.getColor(
binding.messageQuote.quoteColoredView.context,
R.color.high_emphasis_text
)
) )
} }
@ -215,6 +248,11 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
val chatActivity = commonMessageInterface as ChatActivity val chatActivity = commonMessageInterface as ChatActivity
chatActivity.jumpToQuotedMessage(parentChatMessage) chatActivity.jumpToQuotedMessage(parentChatMessage)
} }
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
}
}
} }
private fun showAvatarOnChatMessage(message: ChatMessage) { private fun showAvatarOnChatMessage(message: ChatMessage) {
@ -234,5 +272,6 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
companion object { companion object {
const val TEXT_SIZE_MULTIPLIER = 2.5 const val TEXT_SIZE_MULTIPLIER = 2.5
private val TAG = IncomingTextMessageViewHolder::class.java.simpleName
} }
} }

View File

@ -24,17 +24,23 @@ import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication 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.databinding.ItemCustomIncomingVoiceMessageBinding
import com.nextcloud.talk.extensions.loadBotsAvatar import com.nextcloud.talk.extensions.loadBotsAvatar
import com.nextcloud.talk.extensions.loadChangelogBotAvatar import com.nextcloud.talk.extensions.loadChangelogBotAvatar
import com.nextcloud.talk.extensions.loadFederatedUserAvatar 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.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DateUtils import com.nextcloud.talk.utils.DateUtils
import com.nextcloud.talk.utils.message.MessageUtils import com.nextcloud.talk.utils.message.MessageUtils
import com.nextcloud.talk.utils.preferences.AppPreferences import com.nextcloud.talk.utils.preferences.AppPreferences
import com.stfalcon.chatkit.messages.MessageHolders 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 java.util.concurrent.ExecutionException
import javax.inject.Inject import javax.inject.Inject
@ -203,14 +209,17 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) :
Log.d(TAG, "WorkInfo.State.RUNNING in ViewHolder") Log.d(TAG, "WorkInfo.State.RUNNING in ViewHolder")
showVoiceMessageLoading() showVoiceMessageLoading()
} }
WorkInfo.State.SUCCEEDED -> { WorkInfo.State.SUCCEEDED -> {
Log.d(TAG, "WorkInfo.State.SUCCEEDED in ViewHolder") Log.d(TAG, "WorkInfo.State.SUCCEEDED in ViewHolder")
showPlayButton() showPlayButton()
} }
WorkInfo.State.FAILED -> { WorkInfo.State.FAILED -> {
Log.d(TAG, "WorkInfo.State.FAILED in ViewHolder") Log.d(TAG, "WorkInfo.State.FAILED in ViewHolder")
showPlayButton() showPlayButton()
} }
else -> { else -> {
} }
} }
@ -269,9 +278,24 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) :
} }
private fun setParentMessageDataOnMessageItem(message: ChatMessage) { private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (!message.isDeleted && message.parentMessage != null) { if (message.parentMessageId != null && !message.isDeleted) {
val parentChatMessage = message.parentMessage CoroutineScope(Dispatchers.Main).launch {
parentChatMessage!!.activeUser = message.activeUser 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 { parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) { binding.messageQuote.quotedMessageImage.load(it) {
@ -297,12 +321,19 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) :
.setTextColor(ContextCompat.getColor(context!!, R.color.textColorMaxContrast)) .setTextColor(ContextCompat.getColor(context!!, R.color.textColorMaxContrast))
if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) { if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) {
viewThemeUtils.platform.colorViewBackground(binding.messageQuote.quoteColoredView, ColorRole.PRIMARY) viewThemeUtils.platform.colorViewBackground(
binding.messageQuote.quoteColoredView,
ColorRole.PRIMARY
)
} else { } else {
binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast) binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast)
} }
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
}
} else { } else {
binding.messageQuote.quotedChatMessageView.visibility = View.GONE binding.messageQuote.quotedChatMessageView.visibility = View.GONE
} }

View File

@ -14,7 +14,7 @@ import android.view.View
import coil.load import coil.load
import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.databinding.ReferenceInsideMessageBinding 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.models.json.opengraph.OpenGraphOverall
import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils
import io.reactivex.Observer import io.reactivex.Observer

View File

@ -9,17 +9,21 @@ package com.nextcloud.talk.adapters.messages
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.util.Log
import android.view.View import android.view.View
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.lifecycle.lifecycleScope
import autodagger.AutoInjector import autodagger.AutoInjector
import coil.load import coil.load
import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.adapters.messages.OutcomingPollMessageViewHolder.Companion
import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.databinding.ItemCustomOutcomingLinkPreviewMessageBinding 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.models.json.chat.ReadStatus
import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.ApiUtils 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.message.MessageUtils
import com.nextcloud.talk.utils.preferences.AppPreferences import com.nextcloud.talk.utils.preferences.AppPreferences
import com.stfalcon.chatkit.messages.MessageHolders 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 import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class) @AutoInjector(NextcloudTalkApplication::class)
@ -138,9 +147,24 @@ class OutcomingLinkPreviewMessageViewHolder(outcomingView: View, payload: Any) :
} }
private fun setParentMessageDataOnMessageItem(message: ChatMessage) { private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (!message.isDeleted && message.parentMessage != null) { if (message.parentMessageId != null && !message.isDeleted) {
val parentChatMessage = message.parentMessage CoroutineScope(Dispatchers.Main).launch {
parentChatMessage!!.activeUser = message.activeUser 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 { parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) { binding.messageQuote.quotedMessageImage.load(it) {
@ -166,6 +190,10 @@ class OutcomingLinkPreviewMessageViewHolder(outcomingView: View, payload: Any) :
viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView) viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView)
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
}
} else { } else {
binding.messageQuote.quotedChatMessageView.visibility = View.GONE binding.messageQuote.quotedChatMessageView.visibility = View.GONE
} }

View File

@ -18,16 +18,19 @@ import android.view.View
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.lifecycle.lifecycleScope
import autodagger.AutoInjector import autodagger.AutoInjector
import coil.load import coil.load
import com.google.android.flexbox.FlexboxLayout import com.google.android.flexbox.FlexboxLayout
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.talk.R 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
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.databinding.ItemCustomOutcomingLocationMessageBinding 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.models.json.chat.ReadStatus
import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.ApiUtils 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.UriUtils
import com.nextcloud.talk.utils.message.MessageUtils import com.nextcloud.talk.utils.message.MessageUtils
import com.stfalcon.chatkit.messages.MessageHolders 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 java.net.URLEncoder
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -190,9 +198,24 @@ class OutcomingLocationMessageViewHolder(incomingView: View) :
} }
private fun setParentMessageDataOnMessageItem(message: ChatMessage) { private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (!message.isDeleted && message.parentMessage != null) { if (message.parentMessageId != null && !message.isDeleted) {
val parentChatMessage = message.parentMessage CoroutineScope(Dispatchers.Main).launch {
parentChatMessage!!.activeUser = message.activeUser 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 { parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) { binding.messageQuote.quotedMessageImage.load(it) {
@ -218,6 +241,10 @@ class OutcomingLocationMessageViewHolder(incomingView: View) :
viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView) viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView)
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
}
} else { } else {
binding.messageQuote.quotedChatMessageView.visibility = View.GONE binding.messageQuote.quotedChatMessageView.visibility = View.GONE
} }

View File

@ -9,18 +9,21 @@ package com.nextcloud.talk.adapters.messages
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.util.Log
import android.view.View import android.view.View
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.lifecycle.lifecycleScope
import autodagger.AutoInjector import autodagger.AutoInjector
import coil.load import coil.load
import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.adapters.messages.IncomingPollMessageViewHolder.Companion
import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.databinding.ItemCustomOutcomingPollMessageBinding 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.models.json.chat.ReadStatus
import com.nextcloud.talk.polls.ui.PollMainDialogFragment import com.nextcloud.talk.polls.ui.PollMainDialogFragment
import com.nextcloud.talk.ui.theme.ViewThemeUtils 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.message.MessageUtils
import com.nextcloud.talk.utils.preferences.AppPreferences import com.nextcloud.talk.utils.preferences.AppPreferences
import com.stfalcon.chatkit.messages.MessageHolders 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 import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class) @AutoInjector(NextcloudTalkApplication::class)
@ -153,9 +161,24 @@ class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) :
} }
private fun setParentMessageDataOnMessageItem(message: ChatMessage) { private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (!message.isDeleted && message.parentMessage != null) { if (message.parentMessageId != null && !message.isDeleted) {
val parentChatMessage = message.parentMessage CoroutineScope(Dispatchers.Main).launch {
parentChatMessage!!.activeUser = message.activeUser 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 { parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) { binding.messageQuote.quotedMessageImage.load(it) {
@ -181,6 +204,10 @@ class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) :
viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView) viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView)
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
}
} else { } else {
binding.messageQuote.quotedChatMessageView.visibility = View.GONE binding.messageQuote.quotedChatMessageView.visibility = View.GONE
} }

View File

@ -17,7 +17,7 @@ import com.google.android.material.card.MaterialCardView;
import com.nextcloud.talk.R; import com.nextcloud.talk.R;
import com.nextcloud.talk.databinding.ItemCustomOutcomingPreviewMessageBinding; import com.nextcloud.talk.databinding.ItemCustomOutcomingPreviewMessageBinding;
import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding; 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 com.nextcloud.talk.utils.TextMatchers;
import java.util.HashMap; import java.util.HashMap;

View File

@ -9,6 +9,7 @@
package com.nextcloud.talk.adapters.messages package com.nextcloud.talk.adapters.messages
import android.content.Context import android.content.Context
import android.util.Log
import android.util.TypedValue import android.util.TypedValue
import android.view.View import android.view.View
import androidx.core.content.res.ResourcesCompat 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
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.databinding.ItemCustomOutcomingTextMessageBinding 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.models.json.chat.ReadStatus
import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.ApiUtils 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.TextMatchers
import com.nextcloud.talk.utils.message.MessageUtils import com.nextcloud.talk.utils.message.MessageUtils
import com.stfalcon.chatkit.messages.MessageHolders.OutcomingTextMessageViewHolder 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 import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class) @AutoInjector(NextcloudTalkApplication::class)
@ -91,14 +97,14 @@ class OutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessageViewH
if (message.lastEditTimestamp != 0L && !message.isDeleted) { if (message.lastEditTimestamp != 0L && !message.isDeleted) {
binding.messageEditIndicator.visibility = View.VISIBLE binding.messageEditIndicator.visibility = View.VISIBLE
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.lastEditTimestamp) binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.lastEditTimestamp!!)
} else { } else {
binding.messageEditIndicator.visibility = View.GONE binding.messageEditIndicator.visibility = View.GONE
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
} }
// parent message handling // parent message handling
if (!message.isDeleted && message.parentMessage != null) { if (!message.isDeleted && message.parentMessageId != null) {
processParentMessage(message) processParentMessage(message)
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} else { } else {
@ -148,7 +154,24 @@ class OutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessageViewH
} }
private fun processParentMessage(message: ChatMessage) { private fun processParentMessage(message: ChatMessage) {
val parentChatMessage = message.parentMessage 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!!.activeUser = message.activeUser
parentChatMessage.imageUrl?.let { parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
@ -179,6 +202,11 @@ class OutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessageViewH
val chatActivity = commonMessageInterface as ChatActivity val chatActivity = commonMessageInterface as ChatActivity
chatActivity.jumpToQuotedMessage(parentChatMessage) chatActivity.jumpToQuotedMessage(parentChatMessage)
} }
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
}
}
} }
private fun setBubbleOnChatMessage(message: ChatMessage) { private fun setBubbleOnChatMessage(message: ChatMessage) {
@ -191,5 +219,6 @@ class OutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessageViewH
companion object { companion object {
const val TEXT_SIZE_MULTIPLIER = 2.5 const val TEXT_SIZE_MULTIPLIER = 2.5
private val TAG = OutcomingTextMessageViewHolder::class.java.simpleName
} }
} }

View File

@ -17,16 +17,19 @@ import android.view.View
import android.widget.SeekBar import android.widget.SeekBar
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.work.WorkInfo import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import autodagger.AutoInjector import autodagger.AutoInjector
import coil.load import coil.load
import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.talk.R 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
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.databinding.ItemCustomOutcomingVoiceMessageBinding 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.models.json.chat.ReadStatus
import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.ApiUtils 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.message.MessageUtils
import com.nextcloud.talk.utils.preferences.AppPreferences import com.nextcloud.talk.utils.preferences.AppPreferences
import com.stfalcon.chatkit.messages.MessageHolders 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 java.util.concurrent.ExecutionException
import javax.inject.Inject import javax.inject.Inject
@ -238,14 +246,17 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) :
Log.d(TAG, "WorkInfo.State.RUNNING in ViewHolder") Log.d(TAG, "WorkInfo.State.RUNNING in ViewHolder")
showVoiceMessageLoading() showVoiceMessageLoading()
} }
WorkInfo.State.SUCCEEDED -> { WorkInfo.State.SUCCEEDED -> {
Log.d(TAG, "WorkInfo.State.SUCCEEDED in ViewHolder") Log.d(TAG, "WorkInfo.State.SUCCEEDED in ViewHolder")
showPlayButton() showPlayButton()
} }
WorkInfo.State.FAILED -> { WorkInfo.State.FAILED -> {
Log.d(TAG, "WorkInfo.State.FAILED in ViewHolder") Log.d(TAG, "WorkInfo.State.FAILED in ViewHolder")
showPlayButton() showPlayButton()
} }
else -> { else -> {
Log.d(TAG, "WorkInfo.State unused in ViewHolder") Log.d(TAG, "WorkInfo.State unused in ViewHolder")
} }
@ -264,9 +275,24 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) :
} }
private fun setParentMessageDataOnMessageItem(message: ChatMessage) { private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (!message.isDeleted && message.parentMessage != null) { if (message.parentMessageId != null && !message.isDeleted) {
val parentChatMessage = message.parentMessage CoroutineScope(Dispatchers.Main).launch {
parentChatMessage!!.activeUser = message.activeUser 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 { parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) { binding.messageQuote.quotedMessageImage.load(it) {
@ -292,6 +318,10 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) :
viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView) viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView)
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
}
} else { } else {
binding.messageQuote.quotedChatMessageView.visibility = View.GONE binding.messageQuote.quotedChatMessageView.visibility = View.GONE
} }

View File

@ -6,7 +6,7 @@
*/ */
package com.nextcloud.talk.adapters.messages package com.nextcloud.talk.adapters.messages
import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.chat.data.model.ChatMessage
interface PreviewMessageInterface { interface PreviewMessageInterface {
fun onPreviewMessageLongClick(chatMessage: ChatMessage) fun onPreviewMessageLongClick(chatMessage: ChatMessage)

View File

@ -34,7 +34,7 @@ import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding
import com.nextcloud.talk.extensions.loadChangelogBotAvatar import com.nextcloud.talk.extensions.loadChangelogBotAvatar
import com.nextcloud.talk.extensions.loadFederatedUserAvatar 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.ui.theme.ViewThemeUtils
import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.DateUtils import com.nextcloud.talk.utils.DateUtils

View File

@ -12,7 +12,7 @@ import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding 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.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.DisplayUtils
import com.vanniktech.emoji.EmojiTextView import com.vanniktech.emoji.EmojiTextView

View File

@ -6,7 +6,7 @@
*/ */
package com.nextcloud.talk.adapters.messages package com.nextcloud.talk.adapters.messages
import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.chat.data.model.ChatMessage
interface SystemMessageInterface { interface SystemMessageInterface {
fun expandSystemMessage(chatMessage: ChatMessage) fun expandSystemMessage(chatMessage: ChatMessage)

View File

@ -19,7 +19,7 @@ import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.databinding.ItemSystemMessageBinding 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.DateUtils
import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.preferences.AppPreferences import com.nextcloud.talk.utils.preferences.AppPreferences

View File

@ -33,7 +33,7 @@ public class TalkMessagesListAdapter<M extends IMessage> extends MessagesListAda
@Override @Override
public void onBindViewHolder(ViewHolder holder, int position) { public void onBindViewHolder(ViewHolder holder, int position) {
super.onBindViewHolder(holder, position);
if (holder instanceof IncomingTextMessageViewHolder) { if (holder instanceof IncomingTextMessageViewHolder) {
((IncomingTextMessageViewHolder) holder).assignCommonMessageInterface(chatActivity); ((IncomingTextMessageViewHolder) holder).assignCommonMessageInterface(chatActivity);
@ -66,5 +66,7 @@ public class TalkMessagesListAdapter<M extends IMessage> extends MessagesListAda
} else if (holder instanceof CallStartedViewHolder) { } else if (holder instanceof CallStartedViewHolder) {
((CallStartedViewHolder) holder).assignCallStartedMessageInterface(chatActivity); ((CallStartedViewHolder) holder).assignCallStartedMessageInterface(chatActivity);
} }
super.onBindViewHolder(holder, position);
} }
} }

View File

@ -9,7 +9,7 @@ package com.nextcloud.talk.adapters.messages;
import android.view.View; 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; import com.stfalcon.chatkit.messages.MessageHolders;
public class UnreadNoticeMessageViewHolder extends MessageHolders.SystemMessageViewHolder<ChatMessage> { public class UnreadNoticeMessageViewHolder extends MessageHolders.SystemMessageViewHolder<ChatMessage> {

View File

@ -6,7 +6,7 @@
*/ */
package com.nextcloud.talk.adapters.messages package com.nextcloud.talk.adapters.messages
import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.chat.data.model.ChatMessage
interface VoiceMessageInterface { interface VoiceMessageInterface {
fun updateMediaPlayerProgressBySlider(message: ChatMessage, progress: Int) fun updateMediaPlayerProgressBySlider(message: ChatMessage, progress: Int)

View File

@ -36,6 +36,7 @@ import com.nextcloud.talk.BuildConfig
import com.nextcloud.talk.components.filebrowser.webdav.DavUtils import com.nextcloud.talk.components.filebrowser.webdav.DavUtils
import com.nextcloud.talk.dagger.modules.BusModule import com.nextcloud.talk.dagger.modules.BusModule
import com.nextcloud.talk.dagger.modules.ContextModule 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.DatabaseModule
import com.nextcloud.talk.dagger.modules.ManagerModule import com.nextcloud.talk.dagger.modules.ManagerModule
import com.nextcloud.talk.dagger.modules.RepositoryModule import com.nextcloud.talk.dagger.modules.RepositoryModule
@ -79,7 +80,8 @@ import javax.inject.Singleton
RepositoryModule::class, RepositoryModule::class,
UtilsModule::class, UtilsModule::class,
ThemeModule::class, ThemeModule::class,
ManagerModule::class ManagerModule::class,
DaosModule::class
] ]
) )
@Singleton @Singleton

View File

@ -59,6 +59,7 @@ import androidx.emoji2.text.EmojiCompat
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.commit import androidx.fragment.app.commit
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView 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.adapters.messages.VoiceMessageInterface
import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication 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.ChatViewModel
import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel
import com.nextcloud.talk.conversationinfo.ConversationInfoActivity 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.location.LocationPickerActivity
import com.nextcloud.talk.messagesearch.MessageSearchActivity import com.nextcloud.talk.messagesearch.MessageSearchActivity
import com.nextcloud.talk.models.domain.ConversationModel 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.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.chat.ReadStatus
import com.nextcloud.talk.models.json.conversations.ConversationEnums
import com.nextcloud.talk.polls.ui.PollCreateDialogFragment import com.nextcloud.talk.polls.ui.PollCreateDialogFragment
import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity
import com.nextcloud.talk.shareditems.activities.SharedItemsActivity import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
@ -183,6 +180,8 @@ import com.stfalcon.chatkit.messages.MessagesListAdapter
import com.stfalcon.chatkit.utils.DateFormatter import com.stfalcon.chatkit.utils.DateFormatter
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
@ -408,6 +407,7 @@ class ChatActivity :
handleIntent(intent) handleIntent(intent)
chatViewModel = ViewModelProvider(this, viewModelFactory)[ChatViewModel::class.java] chatViewModel = ViewModelProvider(this, viewModelFactory)[ChatViewModel::class.java]
messageInputViewModel = ViewModelProvider(this, viewModelFactory)[MessageInputViewModel::class.java] messageInputViewModel = ViewModelProvider(this, viewModelFactory)[MessageInputViewModel::class.java]
binding.progressBar.visibility = View.VISIBLE binding.progressBar.visibility = View.VISIBLE
@ -521,12 +521,37 @@ class ChatActivity :
@Suppress("LongMethod") @Suppress("LongMethod")
private fun initObservers() { private fun initObservers() {
Log.d(TAG, "initObservers Called") 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 -> chatViewModel.getRoomViewState.observe(this) { state ->
when (state) { when (state) {
is ChatViewModel.GetRoomSuccessState -> { is ChatViewModel.GetRoomSuccessState -> {
currentConversation = state.conversationModel // unused atm
logConversationInfos("GetRoomSuccessState")
chatViewModel.getCapabilities(conversationUser!!, roomToken, currentConversation!!)
} }
is ChatViewModel.GetRoomErrorState -> { is ChatViewModel.GetRoomErrorState -> {
@ -569,24 +594,29 @@ class ChatActivity :
binding.chatToolbar.setOnClickListener { _ -> showConversationInfoScreen() } binding.chatToolbar.setOnClickListener { _ -> showConversationInfoScreen() }
} }
if (adapter == null) { // if (adapter == null) {
initAdapter() // initAdapter()
binding.messagesListView.setAdapter(adapter) // binding.messagesListView.setAdapter(adapter)
layoutManager = binding.messagesListView.layoutManager as LinearLayoutManager? // layoutManager = binding.messagesListView.layoutManager as LinearLayoutManager?
} // }
loadAvatarForStatusBar() loadAvatarForStatusBar()
setupSwipeToReply() setupSwipeToReply()
setActionBarTitle() setActionBarTitle()
updateRoomTimerHandler() updateRoomTimerHandler()
chatViewModel.refreshChatParams( val urlForChatting = ApiUtils.getUrlForChat(chatApiVersion, conversationUser?.baseUrl, roomToken)
setupFieldsForPullChatMessages(
false, chatViewModel.loadMessages(
0, withCredentials = credentials!!,
false withUrl = urlForChatting,
)
) )
// chatViewModel.initMessagePolling(
// withCredentials = credentials!!,
// withUrl = urlForChatting,
// roomToken = currentConversation!!.token!!
// )
} }
is ChatViewModel.GetCapabilitiesErrorState -> { is ChatViewModel.GetCapabilitiesErrorState -> {
@ -705,6 +735,11 @@ class ChatActivity :
Snackbar.LENGTH_LONG Snackbar.LENGTH_LONG
).show() ).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 -> { is ChatViewModel.DeleteChatMessageErrorState -> {
@ -738,44 +773,36 @@ class ChatActivity :
} }
} }
chatViewModel.getFieldMapForChat.observe(this) { fieldMap -> chatViewModel.chatMessageViewState.observe(this) { state ->
if (fieldMap.isNotEmpty()) {
chatViewModel.pullChatMessages(
credentials!!,
ApiUtils.getUrlForChat(chatApiVersion, conversationUser?.baseUrl, roomToken)
)
}
}
chatViewModel.pullChatMessageViewState.observe(this) { state ->
when (state) { when (state) {
is ChatViewModel.PullChatMessageSuccessState -> { is ChatViewModel.ChatMessageStartState -> {
Log.d(TAG, "PullChatMessageSuccess: Code: ${state.response.code()}") // Handle UI on first load
when (state.response.code()) { cancelNotificationsForCurrentConversation()
HTTP_CODE_OK -> { binding.progressBar.visibility = View.GONE
Log.d(TAG, "lookIntoFuture: ${state.lookIntoFuture}") binding.messagesListView.visibility = View.VISIBLE
val chatOverall = state.response.body() as ChatOverall? collapseSystemMessages()
var chatMessageList = chatOverall?.ocs!!.data!!
val newXChatLastCommonRead = state.response.headers()["X-Chat-Last-Common-Read"]?.let {
Integer.parseInt(it)
} }
processHeaderChatLastGiven(state.response, state.lookIntoFuture) is ChatViewModel.ChatMessageUpdateState -> {
// unused atm
}
is ChatViewModel.ChatMessageErrorState -> {
// unused atm
}
else -> {}
}
}
this.lifecycleScope.launch {
chatViewModel.getMessageFlow
.onEach { pair ->
val lookIntoFuture = pair.first
var chatMessageList = pair.second
chatMessageList = handleSystemMessages(chatMessageList) chatMessageList = handleSystemMessages(chatMessageList)
if (chatMessageList.isEmpty()) {
chatViewModel.refreshChatParams(
setupFieldsForPullChatMessages(
true,
newXChatLastCommonRead,
true
)
)
return@observe
}
determinePreviousMessageIds(chatMessageList) determinePreviousMessageIds(chatMessageList)
handleExpandableSystemMessages(chatMessageList) handleExpandableSystemMessages(chatMessageList)
@ -785,81 +812,31 @@ class ChatActivity :
) { ) {
adapter?.clear() adapter?.clear()
adapter?.notifyDataSetChanged() adapter?.notifyDataSetChanged()
// TODO: remove messages from DB, Should be handled beforehand (in viewModel?)
} }
var lastAdapterId = getLastAdapterId() if (lookIntoFuture) {
val oneNewMessage = (lastAdapterId != 0 || chatMessageList.size == 1)
if (
state.lookIntoFuture &&
oneNewMessage &&
chatMessageList[0].jsonMessageId > lastAdapterId
) {
processMessagesFromTheFuture(chatMessageList) processMessagesFromTheFuture(chatMessageList)
} else if (!state.lookIntoFuture) { } else {
processMessagesNotFromTheFuture(chatMessageList) processMessagesNotFromTheFuture(chatMessageList)
collapseSystemMessages() collapseSystemMessages()
} }
updateReadStatusOfAllMessages(newXChatLastCommonRead)
processCallStartedMessages(chatMessageList) processCallStartedMessages(chatMessageList)
adapter?.notifyDataSetChanged() adapter?.notifyDataSetChanged()
chatViewModel.refreshChatParams(
setupFieldsForPullChatMessages(
true,
newXChatLastCommonRead,
true
)
)
} }
.collect()
HTTP_CODE_NOT_MODIFIED -> {
chatViewModel.refreshChatParams(
setupFieldsForPullChatMessages(
true,
globalLastKnownPastMessageId,
true
)
)
}
HTTP_CODE_PRECONDITION_FAILED -> { chatViewModel.getUpdateMessageFlow
chatViewModel.refreshChatParams( .onEach { pair ->
setupFieldsForPullChatMessages( val lookIntoFuture = pair.first
true, var chatMessageList = pair.second
globalLastKnownPastMessageId,
true
)
)
}
else -> {} adapter!!.update(chatMessageList[0])
}
processExpiredMessages()
if (isFirstMessagesProcessing) {
cancelNotificationsForCurrentConversation()
isFirstMessagesProcessing = false
binding.progressBar.visibility = View.GONE
binding.messagesListView.visibility = View.VISIBLE
collapseSystemMessages()
}
}
is ChatViewModel.PullChatMessageCompleteState -> {
Log.d(TAG, "PullChatMessageCompleted")
}
is ChatViewModel.PullChatMessageErrorState -> {
Log.d(TAG, "PullChatMessageError")
}
else -> {}
} }
.collect()
} }
chatViewModel.reactionDeletedViewState.observe(this) { state -> chatViewModel.reactionDeletedViewState.observe(this) { state ->
@ -916,6 +893,11 @@ class ChatActivity :
).show() ).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 -> { is MessageInputViewModel.EditMessageErrorState -> {
@ -1412,15 +1394,15 @@ class ChatActivity :
fun isOneToOneConversation() = fun isOneToOneConversation() =
currentConversation != null && currentConversation?.type != null && 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() = private fun isGroupConversation() =
currentConversation != null && currentConversation?.type != null && currentConversation != null && currentConversation?.type != null &&
currentConversation?.type == ConversationType.ROOM_GROUP_CALL currentConversation?.type == ConversationEnums.ConversationType.ROOM_GROUP_CALL
private fun isPublicConversation() = private fun isPublicConversation() =
currentConversation != null && currentConversation?.type != null && currentConversation != null && currentConversation?.type != null &&
currentConversation?.type == ConversationType.ROOM_PUBLIC_CALL currentConversation?.type == ConversationEnums.ConversationType.ROOM_PUBLIC_CALL
private fun updateRoomTimerHandler() { private fun updateRoomTimerHandler() {
val delayForRecursiveCall = if (shouldShowLobby()) { val delayForRecursiveCall = if (shouldShowLobby()) {
@ -1443,7 +1425,7 @@ class ChatActivity :
private fun switchToRoom(token: String, startCallAfterRoomSwitch: Boolean, isVoiceOnlyCall: Boolean) { private fun switchToRoom(token: String, startCallAfterRoomSwitch: Boolean, isVoiceOnlyCall: Boolean) {
if (conversationUser != null) { if (conversationUser != null) {
runOnUiThread { runOnUiThread {
if (currentConversation?.objectType == ObjectType.ROOM) { if (currentConversation?.objectType == ConversationEnums.ObjectType.ROOM) {
Snackbar.make( Snackbar.make(
binding.root, binding.root,
context.resources.getString(R.string.switch_to_main_room), context.resources.getString(R.string.switch_to_main_room),
@ -1826,7 +1808,7 @@ class ChatActivity :
private fun shouldShowLobby(): Boolean { private fun shouldShowLobby(): Boolean {
if (currentConversation != null) { if (currentConversation != null) {
return CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.WEBINARY_LOBBY) && 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) && !ConversationUtils.canModerate(currentConversation!!, spreedCapabilities) &&
!participantPermissions.canIgnoreLobby() !participantPermissions.canIgnoreLobby()
} }
@ -1862,7 +1844,7 @@ class ChatActivity :
private fun isReadOnlyConversation(): Boolean { private fun isReadOnlyConversation(): Boolean {
return currentConversation?.conversationReadOnlyState != null && return currentConversation?.conversationReadOnlyState != null &&
currentConversation?.conversationReadOnlyState == currentConversation?.conversationReadOnlyState ==
ConversationReadOnlyState.CONVERSATION_READ_ONLY ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_ONLY
} }
private fun checkLobbyState() { 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 = "" var statusMessage = ""
if (currentConversation?.statusIcon != null) { if (currentConversation?.statusIcon != null) {
statusMessage += currentConversation?.statusIcon statusMessage += currentConversation?.statusIcon
@ -2337,8 +2319,8 @@ class ChatActivity :
} }
statusMessageViewContents(statusMessage) statusMessageViewContents(statusMessage)
} else { } else {
if (currentConversation?.type == ConversationType.ROOM_GROUP_CALL || if (currentConversation?.type == ConversationEnums.ConversationType.ROOM_GROUP_CALL ||
currentConversation?.type == ConversationType.ROOM_PUBLIC_CALL currentConversation?.type == ConversationEnums.ConversationType.ROOM_PUBLIC_CALL
) { ) {
var descriptionMessage = "" var descriptionMessage = ""
descriptionMessage += currentConversation?.description descriptionMessage += currentConversation?.description
@ -2610,9 +2592,9 @@ class ChatActivity :
GROUPED_MESSAGES_SAME_AUTHOR_THRESHOLD > 0 GROUPED_MESSAGES_SAME_AUTHOR_THRESHOLD > 0
) )
chatMessage.isOneToOneConversation = chatMessage.isOneToOneConversation =
(currentConversation?.type == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) (currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL)
chatMessage.isFormerOneToOneConversation = chatMessage.isFormerOneToOneConversation =
(currentConversation?.type == ConversationType.FORMER_ONE_TO_ONE) (currentConversation?.type == ConversationEnums.ConversationType.FORMER_ONE_TO_ONE)
it.addToStart(chatMessage, scrollToEndOnUpdate) it.addToStart(chatMessage, scrollToEndOnUpdate)
} }
} }
@ -2640,9 +2622,9 @@ class ChatActivity :
val chatMessage = chatMessageList[i] val chatMessage = chatMessageList[i]
chatMessage.isOneToOneConversation = chatMessage.isOneToOneConversation =
currentConversation?.type == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
chatMessage.isFormerOneToOneConversation = chatMessage.isFormerOneToOneConversation =
(currentConversation?.type == ConversationType.FORMER_ONE_TO_ONE) (currentConversation?.type == ConversationEnums.ConversationType.FORMER_ONE_TO_ONE)
chatMessage.activeUser = conversationUser chatMessage.activeUser = conversationUser
chatMessage.token = roomToken chatMessage.token = roomToken
} }
@ -2721,6 +2703,7 @@ class ChatActivity :
if (!voiceMessageToRestoreId.equals("")) { if (!voiceMessageToRestoreId.equals("")) {
Log.d(RESUME_AUDIO_TAG, "begin method to resume audio playback") Log.d(RESUME_AUDIO_TAG, "begin method to resume audio playback")
// TODO: replace this logic by calling getItemFromAdapter(messageId)
if (adapter != null) { if (adapter != null) {
Log.d(RESUME_AUDIO_TAG, "adapter is not null, proceeding") Log.d(RESUME_AUDIO_TAG, "adapter is not null, proceeding")
val voiceMessagePosition = adapter!!.items!!.indexOfFirst { val voiceMessagePosition = adapter!!.items!!.indexOfFirst {
@ -2748,7 +2731,7 @@ class ChatActivity :
) )
} }
} else { } else {
Log.d(RESUME_AUDIO_TAG, "TalkMessagesListAdapater is null") Log.d(RESUME_AUDIO_TAG, "TalkMessagesListAdapter is null")
} }
} else { } else {
Log.d(RESUME_AUDIO_TAG, "No voice message to restore") Log.d(RESUME_AUDIO_TAG, "No voice message to restore")
@ -2758,6 +2741,29 @@ class ChatActivity :
voiceMessageToRestoreWasPlaying = false 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() { private fun scrollToRequestedMessageIfNeeded() {
intent.getStringExtra(BundleKeys.KEY_MESSAGE_ID)?.let { intent.getStringExtra(BundleKeys.KEY_MESSAGE_ID)?.let {
scrollToMessageWithId(it) scrollToMessageWithId(it)
@ -2771,16 +2777,21 @@ class ChatActivity :
} }
override fun onLoadMore(page: Int, totalItemsCount: Int) { override fun onLoadMore(page: Int, totalItemsCount: Int) {
val calculatedPage = totalItemsCount / PAGE_SIZE val id = (
if (calculatedPage > 0) { adapter?.items?.last {
chatViewModel.refreshChatParams( it.item is ChatMessage
setupFieldsForPullChatMessages( }?.item as ChatMessage
false, ).jsonMessageId
null,
true 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 { override fun format(date: Date): String {
@ -2923,18 +2934,25 @@ class ChatActivity :
// setDeletionFlagsAndRemoveInfomessages // setDeletionFlagsAndRemoveInfomessages
if (isInfoMessageAboutDeletion(currentMessage)) { 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), // if chatMessageMap doesn't contain message to delete (this happens when lookingIntoFuture),
// the message to delete has to be modified directly inside the adapter // 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 { } else {
chatMessageMap[currentMessage.value.parentMessage!!.id]!!.isDeleted = true chatMessageMap[currentMessage.value.parentMessageId.toString()]!!.isDeleted = true
} }
chatMessageIterator.remove() chatMessageIterator.remove()
} else if (isReactionsMessage(currentMessage)) { } else if (isReactionsMessage(currentMessage)) {
// delete reactions system messages // delete reactions system messages
if (!chatMessageMap.containsKey(currentMessage.value.parentMessage!!.id)) { if (!chatMessageMap.containsKey(currentMessage.value.parentMessageId.toString())) {
updateAdapterForReaction(currentMessage.value.parentMessage) // updateAdapterForReaction(currentMessage.value.parentMessage) TODO
} }
chatMessageIterator.remove() chatMessageIterator.remove()
@ -2942,8 +2960,8 @@ class ChatActivity :
// delete poll system messages // delete poll system messages
chatMessageIterator.remove() chatMessageIterator.remove()
} else if (isEditMessage(currentMessage)) { } else if (isEditMessage(currentMessage)) {
if (!chatMessageMap.containsKey(currentMessage.value.parentMessage!!.id)) { if (!chatMessageMap.containsKey(currentMessage.value.parentMessageId.toString())) {
setMessageAsEdited(currentMessage.value.parentMessage) // setMessageAsEdited(currentMessage.value.parentMessage) TODO
} }
chatMessageIterator.remove() chatMessageIterator.remove()
@ -2977,7 +2995,7 @@ class ChatActivity :
} }
private fun isInfoMessageAboutDeletion(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean { 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 .SystemMessageType.MESSAGE_DELETED
} }
@ -2988,7 +3006,7 @@ class ChatActivity :
} }
private fun isEditMessage(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean { 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 .SystemMessageType.MESSAGE_EDITED
} }
@ -3039,7 +3057,7 @@ class ChatActivity :
bundle.putBoolean(BundleKeys.KEY_CALL_WITHOUT_NOTIFICATION, true) 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) bundle.putBoolean(KEY_IS_BREAKOUT_ROOM, true)
} }
@ -3350,7 +3368,7 @@ class ChatActivity :
conversationUser?.userId?.isNotEmpty() == true && conversationUser!!.userId != "?" && conversationUser?.userId?.isNotEmpty() == true && conversationUser!!.userId != "?" &&
message.user.id.startsWith("users/") && message.user.id.startsWith("users/") &&
message.user.id.substring(ACTOR_LENGTH) != currentConversation?.actorId && 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 isShowMessageDeletionButton(message) || // delete
ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() || // forward ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() || // forward
message.previousMessageId > NO_PREVIOUS_MESSAGE_ID && // mark as unread message.previousMessageId > NO_PREVIOUS_MESSAGE_ID && // mark as unread
@ -3361,40 +3379,44 @@ class ChatActivity :
private fun setMessageAsDeleted(message: IMessage?) { private fun setMessageAsDeleted(message: IMessage?) {
val messageTemp = message as ChatMessage val messageTemp = message as ChatMessage
messageTemp.isDeleted = true messageTemp.isDeleted = true
messageTemp.message = getString(R.string.message_deleted_by_you)
messageTemp.isOneToOneConversation = messageTemp.isOneToOneConversation =
currentConversation?.type == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
messageTemp.activeUser = conversationUser messageTemp.activeUser = conversationUser
adapter?.update(messageTemp) adapter?.update(messageTemp)
} }
private fun setMessageAsEdited(message: IMessage?) { private fun setMessageAsEdited(message: IMessage?, newString: String) {
val messageTemp = message as ChatMessage val messageTemp = message as ChatMessage
messageTemp.lastEditTimestamp = message.lastEditTimestamp messageTemp.lastEditTimestamp = message.lastEditTimestamp
messageTemp.message = newString
val index = adapter?.getMessagePositionById(messageTemp.id)!! val index = adapter?.getMessagePositionById(messageTemp.id)!!
if (index > 0) { if (index > 0) {
val adapterMsg = adapter?.items?.get(index)?.item as ChatMessage val adapterMsg = adapter?.items?.get(index)?.item as ChatMessage
messageTemp.parentMessage = adapterMsg.parentMessage messageTemp.parentMessageId = adapterMsg.parentMessageId
} }
messageTemp.isOneToOneConversation = messageTemp.isOneToOneConversation =
currentConversation?.type == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
messageTemp.activeUser = conversationUser messageTemp.activeUser = conversationUser
adapter?.update(messageTemp) adapter?.update(messageTemp)
} }
private fun updateAdapterForReaction(message: IMessage?) { private fun updateAdapterForReaction(message: IMessage?) {
message?.let {
val messageTemp = message as ChatMessage val messageTemp = message as ChatMessage
messageTemp.isOneToOneConversation = messageTemp.isOneToOneConversation =
currentConversation?.type == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
messageTemp.activeUser = conversationUser messageTemp.activeUser = conversationUser
adapter?.update(messageTemp) adapter?.update(messageTemp)
} }
}
fun updateUiToAddReaction(message: ChatMessage, emoji: String) { fun updateUiToAddReaction(message: ChatMessage, emoji: String) {
if (message.reactions == null) { if (message.reactions == null) {
@ -3428,6 +3450,9 @@ class ChatActivity :
amount = 0 amount = 0
} }
message.reactions!![emoji] = amount - 1 message.reactions!![emoji] = amount - 1
if (message.reactions!![emoji]!! <= 0) {
message.reactions!!.remove(emoji)
}
message.reactionsSelf!!.remove(emoji) message.reactionsSelf!!.remove(emoji)
adapter?.update(message) adapter?.update(message)
} }
@ -3529,7 +3554,7 @@ class ChatActivity :
@Subscribe(threadMode = ThreadMode.BACKGROUND) @Subscribe(threadMode = ThreadMode.BACKGROUND)
fun onMessageEvent(userMentionClickEvent: UserMentionClickEvent) { 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 currentConversation?.name != userMentionClickEvent.userId
) { ) {
var apiVersion = 1 var apiVersion = 1
@ -3602,13 +3627,21 @@ class ChatActivity :
} }
fun jumpToQuotedMessage(parentMessage: ChatMessage) { fun jumpToQuotedMessage(parentMessage: ChatMessage) {
var foundMessage = false
for (position in 0 until (adapter!!.items.size)) { for (position in 0 until (adapter!!.items.size)) {
val currentItem = adapter?.items?.get(position)?.item val currentItem = adapter?.items?.get(position)?.item
if (currentItem is ChatMessage && currentItem.id == parentMessage.id) { if (currentItem is ChatMessage && currentItem.id == parentMessage.id) {
layoutManager!!.scrollToPosition(position) layoutManager!!.scrollToPosition(position)
foundMessage = true
break 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() { override fun joinAudioCall() {
@ -3688,6 +3721,7 @@ class ChatActivity :
private const val QUOTED_MESSAGE_IMAGE_MAX_HEIGHT = 96f private const val QUOTED_MESSAGE_IMAGE_MAX_HEIGHT = 96f
private const val MENTION_AUTO_COMPLETE_ELEVATION = 6f private const val MENTION_AUTO_COMPLETE_ELEVATION = 6f
private const val MESSAGE_PULL_LIMIT = 100 private const val MESSAGE_PULL_LIMIT = 100
private const val PAGE_SIZE = 100
private const val INVITE_LENGTH = 6 private const val INVITE_LENGTH = 6
private const val ACTOR_LENGTH = 6 private const val ACTOR_LENGTH = 6
private const val ANIMATION_DURATION: Long = 750 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_POSITION_KEY = "CURRENT_AUDIO_POSITION"
private const val CURRENT_AUDIO_WAS_PLAYING_KEY = "CURRENT_AUDIO_PLAYING" private const val CURRENT_AUDIO_WAS_PLAYING_KEY = "CURRENT_AUDIO_PLAYING"
private const val RESUME_AUDIO_TAG = "RESUME_AUDIO_TAG" private const val RESUME_AUDIO_TAG = "RESUME_AUDIO_TAG"
private const val PAGE_SIZE = 50
} }
} }

View File

@ -27,6 +27,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.AlphaAnimation import android.view.animation.AlphaAnimation
import android.view.animation.Animation import android.view.animation.Animation
import android.view.animation.Animation.AnimationListener
import android.view.animation.LinearInterpolator import android.view.animation.LinearInterpolator
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.ImageView import android.widget.ImageView
@ -40,6 +41,7 @@ import androidx.core.content.ContextCompat
import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doAfterTextChanged
import androidx.emoji2.widget.EmojiTextView import androidx.emoji2.widget.EmojiTextView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import autodagger.AutoInjector import autodagger.AutoInjector
import coil.load import coil.load
import com.google.android.flexbox.FlexboxLayout 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
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.callbacks.MentionAutocompleteCallback 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.chat.viewmodels.ChatViewModel
import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.databinding.FragmentMessageInputBinding import com.nextcloud.talk.databinding.FragmentMessageInputBinding
import com.nextcloud.talk.jobs.UploadAndShareFilesWorker 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.mention.Mention
import com.nextcloud.talk.models.json.signaling.NCSignalingMessage import com.nextcloud.talk.models.json.signaling.NCSignalingMessage
import com.nextcloud.talk.presenters.MentionAutocompletePresenter import com.nextcloud.talk.presenters.MentionAutocompletePresenter
@ -70,6 +73,9 @@ import com.nextcloud.talk.utils.text.Spans
import com.otaliastudios.autocomplete.Autocomplete import com.otaliastudios.autocomplete.Autocomplete
import com.stfalcon.chatkit.commons.models.IMessage import com.stfalcon.chatkit.commons.models.IMessage
import com.vanniktech.emoji.EmojiPopup import com.vanniktech.emoji.EmojiPopup
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import java.util.Objects import java.util.Objects
import javax.inject.Inject import javax.inject.Inject
@ -101,6 +107,9 @@ class MessageInputFragment : Fragment() {
@Inject @Inject
lateinit var userManager: UserManager lateinit var userManager: UserManager
@Inject
lateinit var networkMonitor: NetworkMonitor
lateinit var binding: FragmentMessageInputBinding lateinit var binding: FragmentMessageInputBinding
private var typedWhileTypingTimerIsRunning: Boolean = false private var typedWhileTypingTimerIsRunning: Boolean = false
private var typingTimer: CountDownTimer? = null private var typingTimer: CountDownTimer? = null
@ -158,6 +167,76 @@ class MessageInputFragment : Fragment() {
else -> {} 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() { private fun restoreState() {
@ -694,6 +773,7 @@ class MessageInputFragment : Fragment() {
private fun sendMessage(message: CharSequence, replyTo: Int?, sendWithoutNotification: Boolean) { private fun sendMessage(message: CharSequence, replyTo: Int?, sendWithoutNotification: Boolean) {
chatActivity.messageInputViewModel.sendChatMessage( chatActivity.messageInputViewModel.sendChatMessage(
chatActivity.roomToken,
chatActivity.conversationUser!!.getCredentials(), chatActivity.conversationUser!!.getCredentials(),
ApiUtils.getUrlForChat( ApiUtils.getUrlForChat(
chatActivity.chatApiVersion, chatActivity.chatApiVersion,

View File

@ -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>
}

View File

@ -2,120 +2,88 @@
* Nextcloud Talk - Android Client * Nextcloud Talk - Android Client
* *
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de> * 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: 2021 Tim Krüger <t@timkrueger.me>
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com> * SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
* SPDX-License-Identifier: GPL-3.0-or-later * 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.text.TextUtils
import android.util.Log import android.util.Log
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonIgnore import com.bluelinelabs.logansquare.annotation.JsonIgnore
import com.bluelinelabs.logansquare.annotation.JsonObject
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.data.user.model.User 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.ChatUtils.Companion.getParsedMessage
import com.nextcloud.talk.models.json.chat.ReadStatus
import com.nextcloud.talk.models.json.converters.EnumSystemMessageTypeConverter import com.nextcloud.talk.models.json.converters.EnumSystemMessageTypeConverter
import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.CapabilitiesUtil import com.nextcloud.talk.utils.CapabilitiesUtil
import com.stfalcon.chatkit.commons.models.IUser import com.stfalcon.chatkit.commons.models.IUser
import com.stfalcon.chatkit.commons.models.MessageContentType import com.stfalcon.chatkit.commons.models.MessageContentType
import kotlinx.parcelize.Parcelize
import java.security.MessageDigest import java.security.MessageDigest
import java.util.Date import java.util.Date
@Parcelize
@JsonObject
data class ChatMessage( data class ChatMessage(
@JsonIgnore
var isGrouped: Boolean = false, var isGrouped: Boolean = false,
@JsonIgnore
var isOneToOneConversation: Boolean = false, var isOneToOneConversation: Boolean = false,
@JsonIgnore
var isFormerOneToOneConversation: Boolean = false, var isFormerOneToOneConversation: Boolean = false,
@JsonIgnore
var activeUser: User? = null, var activeUser: User? = null,
@JsonIgnore
var selectedIndividualHashMap: Map<String?, String?>? = null, var selectedIndividualHashMap: Map<String?, String?>? = null,
@JsonIgnore
var isDeleted: Boolean = false, var isDeleted: Boolean = false,
@JsonField(name = ["id"])
var jsonMessageId: Int = 0, var jsonMessageId: Int = 0,
@JsonIgnore
var previousMessageId: Int = -1, var previousMessageId: Int = -1,
@JsonField(name = ["token"])
var token: String? = null, var token: String? = null,
// guests or users // guests or users
@JsonField(name = ["actorType"])
var actorType: String? = null, var actorType: String? = null,
@JsonField(name = ["actorId"])
var actorId: String? = null, var actorId: String? = null,
// send when crafting a message // send when crafting a message
@JsonField(name = ["actorDisplayName"])
var actorDisplayName: String? = null, var actorDisplayName: String? = null,
@JsonField(name = ["timestamp"])
var timestamp: Long = 0, var timestamp: Long = 0,
// send when crafting a message, max 1000 lines // send when crafting a message, max 1000 lines
@JsonField(name = ["message"])
var message: String? = null, var message: String? = null,
@JsonField(name = ["messageParameters"])
var messageParameters: HashMap<String?, HashMap<String?, String?>>? = null, var messageParameters: HashMap<String?, HashMap<String?, String?>>? = null,
@JsonField(name = ["systemMessage"], typeConverter = EnumSystemMessageTypeConverter::class)
var systemMessageType: SystemMessageType? = null, var systemMessageType: SystemMessageType? = null,
@JsonField(name = ["isReplyable"])
var replyable: Boolean = false, var replyable: Boolean = false,
@JsonField(name = ["parent"]) var parentMessageId: Long? = null,
var parentMessage: ChatMessage? = null,
var readStatus: Enum<ReadStatus> = ReadStatus.NONE, var readStatus: Enum<ReadStatus> = ReadStatus.NONE,
@JsonField(name = ["messageType"])
var messageType: String? = null, var messageType: String? = null,
@JsonField(name = ["reactions"])
var reactions: LinkedHashMap<String, Int>? = null, var reactions: LinkedHashMap<String, Int>? = null,
@JsonField(name = ["reactionsSelf"])
var reactionsSelf: ArrayList<String>? = null, var reactionsSelf: ArrayList<String>? = null,
@JsonField(name = ["expirationTimestamp"])
var expirationTimestamp: Int = 0, var expirationTimestamp: Int = 0,
@JsonField(name = ["markdown"])
var renderMarkdown: Boolean? = null, var renderMarkdown: Boolean? = null,
@JsonField(name = ["lastEditActorDisplayName"])
var lastEditActorDisplayName: String? = null, var lastEditActorDisplayName: String? = null,
@JsonField(name = ["lastEditActorId"])
var lastEditActorId: String? = null, var lastEditActorId: String? = null,
@JsonField(name = ["lastEditActorType"])
var lastEditActorType: String? = null, var lastEditActorType: String? = null,
@JsonField(name = ["lastEditTimestamp"]) var lastEditTimestamp: Long? = 0,
var lastEditTimestamp: Long = 0,
var isDownloadingVoiceMessage: Boolean = false, var isDownloadingVoiceMessage: Boolean = false,
@ -145,7 +113,7 @@ data class ChatMessage(
var openWhenDownloaded: Boolean = true var openWhenDownloaded: Boolean = true
) : Parcelable, MessageContentType, MessageContentType.Image { ) : MessageContentType, MessageContentType.Image {
var extractedUrlToPreview: String? = null var extractedUrlToPreview: String? = null
@ -282,95 +250,7 @@ data class ChatMessage(
} }
} }
val lastMessageDisplayText: String fun getNullsafeActorDisplayName() =
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() =
if (!TextUtils.isEmpty(actorDisplayName)) { if (!TextUtils.isEmpty(actorDisplayName)) {
actorDisplayName actorDisplayName
} else { } else {

View File

@ -4,7 +4,7 @@
* SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de> * SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later * 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.data.user.model.User
import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.domain.ConversationModel
@ -19,7 +19,7 @@ import io.reactivex.Observable
import retrofit2.Response import retrofit2.Response
@Suppress("LongParameterList", "TooManyFunctions") @Suppress("LongParameterList", "TooManyFunctions")
interface ChatRepository { interface ChatNetworkDataSource {
fun getRoom(user: User, roomToken: String): Observable<ConversationModel> fun getRoom(user: User, roomToken: String): Observable<ConversationModel>
fun getCapabilities(user: User, roomToken: String): Observable<SpreedCapability> fun getCapabilities(user: User, roomToken: String): Observable<SpreedCapability>
fun joinRoom(user: User, roomToken: String, roomPassword: String): Observable<ConversationModel> fun joinRoom(user: User, roomToken: String, roomPassword: String): Observable<ConversationModel>

View File

@ -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
}
}

View File

@ -7,7 +7,6 @@
package com.nextcloud.talk.chat.data.network package com.nextcloud.talk.chat.data.network
import com.nextcloud.talk.api.NcApi 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.data.user.model.User
import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.domain.ConversationModel
import com.nextcloud.talk.models.json.capabilities.SpreedCapability import com.nextcloud.talk.models.json.capabilities.SpreedCapability
@ -21,7 +20,7 @@ import com.nextcloud.talk.utils.ApiUtils
import io.reactivex.Observable import io.reactivex.Observable
import retrofit2.Response 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> { override fun getRoom(user: User, roomToken: String): Observable<ConversationModel> {
val credentials: String = ApiUtils.getCredentials(user.username, user.token)!! val credentials: String = ApiUtils.getCredentials(user.username, user.token)!!
val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1)) 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( return ncApi.getRoom(
credentials, credentials,
ApiUtils.getUrlForRoom(apiVersion, user.baseUrl!!, roomToken) 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> { override fun getCapabilities(user: User, roomToken: String): Observable<SpreedCapability> {
@ -50,7 +49,7 @@ class NetworkChatRepositoryImpl(private val ncApi: NcApi) : ChatRepository {
credentials, credentials,
ApiUtils.getUrlForParticipantsActive(apiVersion, user.baseUrl!!, roomToken), ApiUtils.getUrlForParticipantsActive(apiVersion, user.baseUrl!!, roomToken),
roomPassword roomPassword
).map { ConversationModel.mapToConversationModel(it.ocs?.data!!) } ).map { ConversationModel.mapToConversationModel(it.ocs?.data!!, user) }
} }
override fun setReminder( override fun setReminder(

View File

@ -8,22 +8,25 @@ package com.nextcloud.talk.chat.viewmodels
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel 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.AudioFocusRequestManager
import com.nextcloud.talk.chat.data.io.MediaRecorderManager 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.data.user.model.User
import com.nextcloud.talk.jobs.UploadAndShareFilesWorker import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.domain.ConversationModel
import com.nextcloud.talk.models.domain.ReactionAddedModel import com.nextcloud.talk.models.domain.ReactionAddedModel
import com.nextcloud.talk.models.domain.ReactionDeletedModel import com.nextcloud.talk.models.domain.ReactionDeletedModel
import com.nextcloud.talk.models.json.capabilities.SpreedCapability 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.chat.ChatOverallSingleMessage
import com.nextcloud.talk.models.json.conversations.RoomOverall import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.models.json.conversations.RoomsOverall 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.models.json.reminder.Reminder
import com.nextcloud.talk.repositories.reactions.ReactionsRepository import com.nextcloud.talk.repositories.reactions.ReactionsRepository
import com.nextcloud.talk.utils.ConversationUtils 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.Observer
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers 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 java.io.File
import javax.inject.Inject import javax.inject.Inject
@Suppress("TooManyFunctions", "LongParameterList") @Suppress("TooManyFunctions", "LongParameterList")
class ChatViewModel @Inject constructor( 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 reactionsRepository: ReactionsRepository,
private val mediaRecorderManager: MediaRecorderManager, private val mediaRecorderManager: MediaRecorderManager,
private val audioFocusRequestManager: AudioFocusRequestManager private val audioFocusRequestManager: AudioFocusRequestManager,
private val userProvider: CurrentUserProviderNew
) : ViewModel(), DefaultLifecycleObserver { ) : ViewModel(), DefaultLifecycleObserver {
enum class LifeCycleFlag { enum class LifeCycleFlag {
@ -52,6 +65,7 @@ class ChatViewModel @Inject constructor(
RESUMED, RESUMED,
STOPPED STOPPED
} }
lateinit var currentLifeCycleFlag: LifeCycleFlag lateinit var currentLifeCycleFlag: LifeCycleFlag
val disposableSet = mutableSetOf<Disposable>() val disposableSet = mutableSetOf<Disposable>()
@ -59,6 +73,7 @@ class ChatViewModel @Inject constructor(
super.onResume(owner) super.onResume(owner)
currentLifeCycleFlag = LifeCycleFlag.RESUMED currentLifeCycleFlag = LifeCycleFlag.RESUMED
mediaRecorderManager.handleOnResume() mediaRecorderManager.handleOnResume()
chatRepository.handleOnResume()
} }
override fun onPause(owner: LifecycleOwner) { override fun onPause(owner: LifecycleOwner) {
@ -67,13 +82,16 @@ class ChatViewModel @Inject constructor(
disposableSet.forEach { disposable -> disposable.dispose() } disposableSet.forEach { disposable -> disposable.dispose() }
disposableSet.clear() disposableSet.clear()
mediaRecorderManager.handleOnPause() mediaRecorderManager.handleOnPause()
chatRepository.handleOnPause()
} }
override fun onStop(owner: LifecycleOwner) { override fun onStop(owner: LifecycleOwner) {
super.onStop(owner) super.onStop(owner)
currentLifeCycleFlag = LifeCycleFlag.STOPPED currentLifeCycleFlag = LifeCycleFlag.STOPPED
mediaRecorderManager.handleOnStop() mediaRecorderManager.handleOnStop()
chatRepository.handleOnStop()
} }
val getAudioFocusChange: LiveData<AudioFocusRequestManager.ManagerState> val getAudioFocusChange: LiveData<AudioFocusRequestManager.ManagerState>
get() = audioFocusRequestManager.getManagerState get() = audioFocusRequestManager.getManagerState
@ -89,9 +107,26 @@ class ChatViewModel @Inject constructor(
val getVoiceRecordingLocked: LiveData<Boolean> val getVoiceRecordingLocked: LiveData<Boolean>
get() = _getVoiceRecordingLocked get() = _getVoiceRecordingLocked
private val _getFieldMapForChat: MutableLiveData<HashMap<String, Int>> = MutableLiveData() val getMessageFlow = chatRepository.messageFlow
val getFieldMapForChat: LiveData<HashMap<String, Int>> .onEach {
get() = _getFieldMapForChat _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 sealed interface ViewState
object GetReminderStartState : ViewState object GetReminderStartState : ViewState
@ -111,7 +146,7 @@ class ChatViewModel @Inject constructor(
object GetRoomStartState : ViewState object GetRoomStartState : ViewState
object GetRoomErrorState : ViewState object GetRoomErrorState : ViewState
open class GetRoomSuccessState(val conversationModel: ConversationModel) : ViewState object GetRoomSuccessState : ViewState
private val _getRoomViewState: MutableLiveData<ViewState> = MutableLiveData(GetRoomStartState) private val _getRoomViewState: MutableLiveData<ViewState> = MutableLiveData(GetRoomStartState)
val getRoomViewState: LiveData<ViewState> val getRoomViewState: LiveData<ViewState>
@ -136,28 +171,24 @@ class ChatViewModel @Inject constructor(
object LeaveRoomStartState : ViewState object LeaveRoomStartState : ViewState
class LeaveRoomSuccessState(val funToCallWhenLeaveSuccessful: (() -> Unit)?) : ViewState class LeaveRoomSuccessState(val funToCallWhenLeaveSuccessful: (() -> Unit)?) : ViewState
private val _leaveRoomViewState: MutableLiveData<ViewState> = MutableLiveData(LeaveRoomStartState) private val _leaveRoomViewState: MutableLiveData<ViewState> = MutableLiveData(LeaveRoomStartState)
val leaveRoomViewState: LiveData<ViewState> val leaveRoomViewState: LiveData<ViewState>
get() = _leaveRoomViewState get() = _leaveRoomViewState
object SendChatMessageStartState : ViewState object ChatMessageInitialState : ViewState
class SendChatMessageSuccessState(val message: CharSequence) : ViewState object ChatMessageStartState : ViewState
class SendChatMessageErrorState(val e: Throwable, val message: CharSequence) : ViewState object ChatMessageUpdateState : ViewState
private val _sendChatMessageViewState: MutableLiveData<ViewState> = MutableLiveData(SendChatMessageStartState) object ChatMessageErrorState : ViewState
val sendChatMessageViewState: LiveData<ViewState>
get() = _sendChatMessageViewState
object PullChatMessageStartState : ViewState private val _chatMessageViewState: MutableLiveData<ViewState> = MutableLiveData(ChatMessageInitialState)
class PullChatMessageSuccessState(val response: Response<*>, val lookIntoFuture: Boolean) : ViewState val chatMessageViewState: LiveData<ViewState>
object PullChatMessageErrorState : ViewState get() = _chatMessageViewState
object PullChatMessageCompleteState : ViewState
private val _pullChatMessageViewState: MutableLiveData<ViewState> = MutableLiveData(PullChatMessageStartState)
val pullChatMessageViewState: LiveData<ViewState>
get() = _pullChatMessageViewState
object DeleteChatMessageStartState : ViewState object DeleteChatMessageStartState : ViewState
class DeleteChatMessageSuccessState(val msg: ChatOverallSingleMessage) : ViewState class DeleteChatMessageSuccessState(val msg: ChatOverallSingleMessage) : ViewState
object DeleteChatMessageErrorState : ViewState object DeleteChatMessageErrorState : ViewState
private val _deleteChatMessageViewState: MutableLiveData<ViewState> = MutableLiveData(DeleteChatMessageStartState) private val _deleteChatMessageViewState: MutableLiveData<ViewState> = MutableLiveData(DeleteChatMessageStartState)
val deleteChatMessageViewState: LiveData<ViewState> val deleteChatMessageViewState: LiveData<ViewState>
get() = _deleteChatMessageViewState get() = _deleteChatMessageViewState
@ -172,29 +203,38 @@ class ChatViewModel @Inject constructor(
object ReactionAddedStartState : ViewState object ReactionAddedStartState : ViewState
class ReactionAddedSuccessState(val reactionAddedModel: ReactionAddedModel) : ViewState class ReactionAddedSuccessState(val reactionAddedModel: ReactionAddedModel) : ViewState
private val _reactionAddedViewState: MutableLiveData<ViewState> = MutableLiveData(ReactionAddedStartState) private val _reactionAddedViewState: MutableLiveData<ViewState> = MutableLiveData(ReactionAddedStartState)
val reactionAddedViewState: LiveData<ViewState> val reactionAddedViewState: LiveData<ViewState>
get() = _reactionAddedViewState get() = _reactionAddedViewState
object ReactionDeletedStartState : ViewState object ReactionDeletedStartState : ViewState
class ReactionDeletedSuccessState(val reactionDeletedModel: ReactionDeletedModel) : ViewState class ReactionDeletedSuccessState(val reactionDeletedModel: ReactionDeletedModel) : ViewState
private val _reactionDeletedViewState: MutableLiveData<ViewState> = MutableLiveData(ReactionDeletedStartState) private val _reactionDeletedViewState: MutableLiveData<ViewState> = MutableLiveData(ReactionDeletedStartState)
val reactionDeletedViewState: LiveData<ViewState> val reactionDeletedViewState: LiveData<ViewState>
get() = _reactionDeletedViewState get() = _reactionDeletedViewState
fun refreshChatParams(pullChatMessagesFieldMap: HashMap<String, Int>, overrideRefresh: Boolean = false) { fun setData(
if (pullChatMessagesFieldMap != _getFieldMapForChat.value || overrideRefresh) { conversationModel: ConversationModel,
_getFieldMapForChat.postValue(pullChatMessagesFieldMap) credentials: String,
Log.d(TAG, "FieldMap Refreshed with $pullChatMessagesFieldMap vs ${_getFieldMapForChat.value}") urlForChatting: String
} ) {
chatRepository.setData(
conversationModel,
credentials,
urlForChatting
)
} }
fun getRoom(user: User, token: String) { fun getRoom(user: User, token: String) {
_getRoomViewState.value = GetRoomStartState _getRoomViewState.value = GetRoomStartState
chatRepository.getRoom(user, token) conversationRepository.getConversationSettings(token)
.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread()) // chatNetworkDataSource.getRoom(user, token)
?.subscribe(GetRoomObserver()) // .subscribeOn(Schedulers.io())
// ?.observeOn(AndroidSchedulers.mainThread())
// ?.subscribe(GetRoomObserver())
} }
fun getCapabilities(user: User, token: String, conversationModel: ConversationModel) { fun getCapabilities(user: User, token: String, conversationModel: ConversationModel) {
@ -208,7 +248,7 @@ class ChatViewModel @Inject constructor(
_getCapabilitiesViewState.value = GetCapabilitiesUpdateState(user.capabilities!!.spreedCapability!!) _getCapabilitiesViewState.value = GetCapabilitiesUpdateState(user.capabilities!!.spreedCapability!!)
} }
} else { } else {
chatRepository.getCapabilities(user, token) chatNetworkDataSource.getCapabilities(user, token)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread()) ?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(object : Observer<SpreedCapability> { ?.subscribe(object : Observer<SpreedCapability> {
@ -238,7 +278,7 @@ class ChatViewModel @Inject constructor(
fun joinRoom(user: User, token: String, roomPassword: String) { fun joinRoom(user: User, token: String, roomPassword: String) {
_joinRoomViewState.value = JoinRoomStartState _joinRoomViewState.value = JoinRoomStartState
chatRepository.joinRoom(user, token, roomPassword) chatNetworkDataSource.joinRoom(user, token, roomPassword)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread()) ?.observeOn(AndroidSchedulers.mainThread())
?.retry(JOIN_ROOM_RETRY_COUNT) ?.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) { 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()) .subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread()) ?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(SetReminderObserver()) ?.subscribe(SetReminderObserver())
} }
fun getReminder(user: User, roomToken: String, messageId: String, chatApiVersion: Int) { 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()) .subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread()) ?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(GetReminderObserver()) ?.subscribe(GetReminderObserver())
} }
fun deleteReminder(user: User, roomToken: String, messageId: String, chatApiVersion: Int) { 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()) .subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread()) ?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(object : Observer<GenericOverall> { ?.subscribe(object : Observer<GenericOverall> {
@ -284,7 +324,7 @@ class ChatViewModel @Inject constructor(
fun leaveRoom(credentials: String, url: String, funToCallWhenLeaveSuccessful: (() -> Unit)?) { fun leaveRoom(credentials: String, url: String, funToCallWhenLeaveSuccessful: (() -> Unit)?) {
val startNanoTime = System.nanoTime() val startNanoTime = System.nanoTime()
chatRepository.leaveRoom(credentials, url) chatNetworkDataSource.leaveRoom(credentials, url)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread()) ?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(object : Observer<GenericOverall> { ?.subscribe(object : Observer<GenericOverall> {
@ -309,7 +349,7 @@ class ChatViewModel @Inject constructor(
} }
fun createRoom(credentials: String, url: String, queryMap: Map<String, String>) { fun createRoom(credentials: String, url: String, queryMap: Map<String, String>) {
chatRepository.createRoom(credentials, url, queryMap) chatNetworkDataSource.createRoom(credentials, url, queryMap)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<RoomOverall> { .subscribe(object : Observer<RoomOverall> {
@ -332,72 +372,42 @@ class ChatViewModel @Inject constructor(
}) })
} }
fun sendChatMessage( fun loadMessages(withCredentials: String, withUrl: String) {
credentials: String, val bundle = Bundle()
url: String, bundle.putString(BundleKeys.KEY_CHAT_URL, withUrl)
message: CharSequence, bundle.putString(BundleKeys.KEY_CREDENTIALS, withCredentials)
displayName: String, chatRepository.loadInitialMessages(
replyTo: Int, withNetworkParams = bundle
sendWithoutNotification: Boolean )
}
fun loadMoreMessages(
beforeMessageId: Long,
roomToken: String,
withMessageLimit: Int,
withCredentials: String,
withUrl: String
) { ) {
chatRepository.sendChatMessage( val bundle = Bundle()
credentials, bundle.putString(BundleKeys.KEY_CHAT_URL, withUrl)
url, bundle.putString(BundleKeys.KEY_CREDENTIALS, withCredentials)
message, chatRepository.loadMoreMessages(
displayName, beforeMessageId,
replyTo, roomToken,
sendWithoutNotification withMessageLimit,
).subscribeOn(Schedulers.io()) withNetworkParams = bundle
?.observeOn(AndroidSchedulers.mainThread()) )
?.subscribe(object : Observer<GenericOverall> {
override fun onSubscribe(d: Disposable) {
disposableSet.add(d)
} }
override fun onError(e: Throwable) { // fun initMessagePolling(withCredentials: String, withUrl: String, roomToken: String) {
_sendChatMessageViewState.value = SendChatMessageErrorState(e, message) // val bundle = Bundle()
} // bundle.putString(BundleKeys.KEY_CHAT_URL, withUrl)
// bundle.putString(BundleKeys.KEY_CREDENTIALS, withCredentials)
override fun onComplete() { // chatRepository.initMessagePolling(roomToken, withNetworkParams = bundle)
// unused atm // }
}
override fun onNext(t: GenericOverall) {
_sendChatMessageViewState.value = SendChatMessageSuccessState(message)
}
})
}
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 deleteChatMessages(credentials: String, url: String, messageId: String) { fun deleteChatMessages(credentials: String, url: String, messageId: String) {
chatRepository.deleteChatMessage(credentials, url) chatNetworkDataSource.deleteChatMessage(credentials, url)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread()) ?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(object : Observer<ChatOverallSingleMessage> { ?.subscribe(object : Observer<ChatOverallSingleMessage> {
@ -426,7 +436,7 @@ class ChatViewModel @Inject constructor(
} }
fun setChatReadMarker(credentials: String, url: String, previousMessageId: Int) { fun setChatReadMarker(credentials: String, url: String, previousMessageId: Int) {
chatRepository.setChatReadMarker(credentials, url, previousMessageId) chatNetworkDataSource.setChatReadMarker(credentials, url, previousMessageId)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<GenericOverall> { .subscribe(object : Observer<GenericOverall> {
@ -449,7 +459,7 @@ class ChatViewModel @Inject constructor(
} }
fun shareToNotes(credentials: String, url: String, message: String, displayName: String) { 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()) .subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread()) ?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(object : Observer<GenericOverall> { ?.subscribe(object : Observer<GenericOverall> {
@ -472,13 +482,13 @@ class ChatViewModel @Inject constructor(
} }
fun checkForNoteToSelf(credentials: String, baseUrl: String, includeStatus: Boolean) { 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()) ?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(CheckForNoteToSelfObserver()) ?.subscribe(CheckForNoteToSelfObserver())
} }
fun shareLocationToNotes(credentials: String, url: String, objectType: String, objectId: String, metadata: String) { 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()) .subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread()) ?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(object : Observer<GenericOverall> { ?.subscribe(object : Observer<GenericOverall> {
@ -575,6 +585,7 @@ class ChatViewModel @Inject constructor(
uploadFile(uri.toString(), room, displayName, metaData) uploadFile(uri.toString(), room, displayName, metaData)
} }
} }
fun stopAndDiscardAudioRecording() { fun stopAndDiscardAudioRecording() {
stopAudioRecording() stopAudioRecording()
Log.d(TAG, "File discarded") Log.d(TAG, "File discarded")
@ -619,24 +630,38 @@ class ChatViewModel @Inject constructor(
_getCapabilitiesViewState.value = GetCapabilitiesStartState _getCapabilitiesViewState.value = GetCapabilitiesStartState
} }
inner class GetRoomObserver : Observer<ConversationModel> { suspend fun getMessageById(url: String, conversationModel: ConversationModel, messageId: Long): Flow<ChatMessage> =
override fun onSubscribe(d: Disposable) { flow {
// unused atm 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) { // inner class GetRoomObserver : Observer<ConversationModel> {
_getRoomViewState.value = GetRoomSuccessState(conversationModel) // override fun onSubscribe(d: Disposable) {
} // // unused atm
// }
override fun onError(e: Throwable) { //
Log.e(TAG, "Error when fetching room") // override fun onNext(conversationModel: ConversationModel) {
_getRoomViewState.value = GetRoomErrorState // _getRoomViewState.value = GetRoomSuccessState(conversationModel)
} // }
//
override fun onComplete() { // override fun onError(e: Throwable) {
// unused atm // Log.e(TAG, "Error when fetching room")
} // _getRoomViewState.value = GetRoomErrorState
} // }
//
// override fun onComplete() {
// // unused atm
// }
// }
inner class JoinRoomObserver : Observer<ConversationModel> { inner class JoinRoomObserver : Observer<ConversationModel> {
override fun onSubscribe(d: Disposable) { override fun onSubscribe(d: Disposable) {
@ -704,7 +729,7 @@ class ChatViewModel @Inject constructor(
rooms?.let { rooms?.let {
try { try {
val noteToSelf = rooms.first { val noteToSelf = rooms.first {
val model = ConversationModel.mapToConversationModel(it) val model = ConversationModel.mapToConversationModel(it, userProvider.currentUser.blockingGet())
ConversationUtils.isNoteToSelfConversation(model) ConversationUtils.isNoteToSelfConversation(model)
} }
_getNoteToSelfAvaliability.value = NoteToSelfAvaliableState(noteToSelf.token!!) _getNoteToSelfAvaliability.value = NoteToSelfAvaliableState(noteToSelf.token!!)

View File

@ -14,12 +14,13 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel 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.AudioFocusRequestManager
import com.nextcloud.talk.chat.data.io.AudioRecorderManager import com.nextcloud.talk.chat.data.io.AudioRecorderManager
import com.nextcloud.talk.chat.data.io.MediaPlayerManager 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.chat.ChatOverallSingleMessage
import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.utils.preferences.AppPreferences
import com.stfalcon.chatkit.commons.models.IMessage import com.stfalcon.chatkit.commons.models.IMessage
import io.reactivex.Observer import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
@ -28,10 +29,11 @@ import io.reactivex.schedulers.Schedulers
import javax.inject.Inject import javax.inject.Inject
class MessageInputViewModel @Inject constructor( class MessageInputViewModel @Inject constructor(
private val chatRepository: ChatRepository, private val chatNetworkDataSource: ChatNetworkDataSource,
private val audioRecorderManager: AudioRecorderManager, private val audioRecorderManager: AudioRecorderManager,
private val mediaPlayerManager: MediaPlayerManager, private val mediaPlayerManager: MediaPlayerManager,
private val audioFocusRequestManager: AudioFocusRequestManager private val audioFocusRequestManager: AudioFocusRequestManager,
private val dataStore: AppPreferences
) : ViewModel(), DefaultLifecycleObserver { ) : ViewModel(), DefaultLifecycleObserver {
enum class LifeCycleFlag { enum class LifeCycleFlag {
PAUSED, PAUSED,
@ -41,6 +43,16 @@ class MessageInputViewModel @Inject constructor(
lateinit var currentLifeCycleFlag: LifeCycleFlag lateinit var currentLifeCycleFlag: LifeCycleFlag
val disposableSet = mutableSetOf<Disposable>() 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) { override fun onResume(owner: LifecycleOwner) {
super.onResume(owner) super.onResume(owner)
currentLifeCycleFlag = LifeCycleFlag.RESUMED currentLifeCycleFlag = LifeCycleFlag.RESUMED
@ -109,6 +121,7 @@ class MessageInputViewModel @Inject constructor(
@Suppress("LongParameterList") @Suppress("LongParameterList")
fun sendChatMessage( fun sendChatMessage(
roomToken: String,
credentials: String, credentials: String,
url: String, url: String,
message: CharSequence, message: CharSequence,
@ -116,7 +129,13 @@ class MessageInputViewModel @Inject constructor(
replyTo: Int, replyTo: Int,
sendWithoutNotification: Boolean sendWithoutNotification: Boolean
) { ) {
chatRepository.sendChatMessage( if (isQueueing) {
messageQueue.add(QueuedMessage(message, displayName, replyTo, sendWithoutNotification))
dataStore.saveMessageQueue(roomToken, messageQueue)
return
}
chatNetworkDataSource.sendChatMessage(
credentials, credentials,
url, url,
message, message,
@ -145,7 +164,7 @@ class MessageInputViewModel @Inject constructor(
} }
fun editChatMessage(credentials: String, url: String, text: String) { fun editChatMessage(credentials: String, url: String, text: String) {
chatRepository.editChatMessage(credentials, url, text) chatNetworkDataSource.editChatMessage(credentials, url, text)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread()) ?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(object : Observer<ChatOverallSingleMessage> { ?.subscribe(object : Observer<ChatOverallSingleMessage> {
@ -216,4 +235,28 @@ class MessageInputViewModel @Inject constructor(
fun setRecordingTime(time: Long) { fun setRecordingTime(time: Long) {
_getRecordingTime.postValue(time) _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
}
} }

View File

@ -46,7 +46,7 @@ import com.nextcloud.talk.jobs.AddParticipantsToConversation
import com.nextcloud.talk.models.RetrofitBucket import com.nextcloud.talk.models.RetrofitBucket
import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall
import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser 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.conversations.RoomOverall
import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter
import com.nextcloud.talk.models.json.participants.Participant 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 // if there are more participants to add, ask for roomName and add them one after another
} else { } else {
val roomType: Conversation.ConversationType = if (isPublicCall) { val roomType: ConversationEnums.ConversationType = if (isPublicCall) {
Conversation.ConversationType.ROOM_PUBLIC_CALL ConversationEnums.ConversationType.ROOM_PUBLIC_CALL
} else { } else {
Conversation.ConversationType.ROOM_GROUP_CALL ConversationEnums.ConversationType.ROOM_GROUP_CALL
} }
val userIdsArray = ArrayList(selectedUserIds) val userIdsArray = ArrayList(selectedUserIds)
val groupIdsArray = ArrayList(selectedGroupIds) val groupIdsArray = ArrayList(selectedGroupIds)
@ -415,7 +415,7 @@ class ContactsActivity :
searchView!!.inputType = InputType.TYPE_TEXT_VARIATION_FILTER searchView!!.inputType = InputType.TYPE_TEXT_VARIATION_FILTER
var imeOptions: Int = EditorInfo.IME_ACTION_DONE or EditorInfo.IME_FLAG_NO_FULLSCREEN var imeOptions: Int = EditorInfo.IME_ACTION_DONE or EditorInfo.IME_FLAG_NO_FULLSCREEN
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
appPreferences?.isKeyboardIncognito == true appPreferences.isKeyboardIncognito == true
) { ) {
imeOptions = imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING imeOptions = imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
} }

View File

@ -37,7 +37,7 @@ import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.conversation.viewmodel.ConversationViewModel import com.nextcloud.talk.conversation.viewmodel.ConversationViewModel
import com.nextcloud.talk.databinding.DialogCreateConversationBinding import com.nextcloud.talk.databinding.DialogCreateConversationBinding
import com.nextcloud.talk.jobs.AddParticipantsToConversation 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.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
@ -66,7 +66,7 @@ class CreateConversationDialogFragment : DialogFragment() {
private var emojiPopup: EmojiPopup? = null 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 usersToInvite: ArrayList<String> = ArrayList()
private var groupsToInvite: ArrayList<String> = ArrayList() private var groupsToInvite: ArrayList<String> = ArrayList()
private var emailsToInvite: ArrayList<String> = ArrayList() private var emailsToInvite: ArrayList<String> = ArrayList()

View File

@ -6,7 +6,7 @@
*/ */
package com.nextcloud.talk.conversation.repository 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.conversations.RoomOverall
import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.generic.GenericOverall
import io.reactivex.Observable import io.reactivex.Observable
@ -15,5 +15,8 @@ interface ConversationRepository {
fun renameConversation(roomToken: String, roomNameNew: String): Observable<GenericOverall> 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>
} }

View File

@ -9,7 +9,7 @@ package com.nextcloud.talk.conversation.repository
import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.RetrofitBucket 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.conversations.RoomOverall
import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils
@ -43,11 +43,12 @@ class ConversationRepositoryImpl(private val ncApi: NcApi, currentUserProvider:
override fun createConversation( override fun createConversation(
roomName: String, roomName: String,
conversationType: Conversation.ConversationType? conversationType: ConversationEnums.ConversationType?
): Observable<RoomOverall> { ): Observable<RoomOverall> {
val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1)) val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1))
val retrofitBucket: RetrofitBucket = if (conversationType == Conversation.ConversationType.ROOM_PUBLIC_CALL) { val retrofitBucket: RetrofitBucket =
if (conversationType == ConversationEnums.ConversationType.ROOM_PUBLIC_CALL) {
ApiUtils.getRetrofitBucketForCreateRoom( ApiUtils.getRetrofitBucketForCreateRoom(
apiVersion, apiVersion,
currentUser.baseUrl!!, currentUser.baseUrl!!,

View File

@ -10,7 +10,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.nextcloud.talk.conversation.repository.ConversationRepository 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 com.nextcloud.talk.models.json.conversations.RoomOverall
import io.reactivex.Observer import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
@ -40,7 +40,7 @@ class ConversationViewModel @Inject constructor(private val repository: Conversa
disposable?.dispose() disposable?.dispose()
} }
fun createConversation(roomName: String, conversationType: Conversation.ConversationType?) { fun createConversation(roomName: String, conversationType: ConversationEnums.ConversationType?) {
_viewState.value = CreatingState _viewState.value = CreatingState
repository.createConversation( repository.createConversation(

View File

@ -57,11 +57,9 @@ import com.nextcloud.talk.extensions.loadUserAvatar
import com.nextcloud.talk.jobs.DeleteConversationWorker import com.nextcloud.talk.jobs.DeleteConversationWorker
import com.nextcloud.talk.jobs.LeaveConversationWorker import com.nextcloud.talk.jobs.LeaveConversationWorker
import com.nextcloud.talk.models.domain.ConversationModel 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.domain.converters.DomainEnumNotificationLevelConverter
import com.nextcloud.talk.models.json.capabilities.SpreedCapability 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.converters.EnumActorTypeConverter
import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.models.json.participants.Participant import com.nextcloud.talk.models.json.participants.Participant
@ -350,7 +348,7 @@ class ConversationInfoActivity :
binding.webinarInfoView.webinarSettings.visibility = VISIBLE binding.webinarInfoView.webinarSettings.visibility = VISIBLE
val isLobbyOpenToModeratorsOnly = val isLobbyOpenToModeratorsOnly =
conversation!!.lobbyState == LobbyState.LOBBY_STATE_MODERATORS_ONLY conversation!!.lobbyState == ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY
binding.webinarInfoView.lobbySwitch.isChecked = isLobbyOpenToModeratorsOnly binding.webinarInfoView.lobbySwitch.isChecked = isLobbyOpenToModeratorsOnly
reconfigureLobbyTimerView() reconfigureLobbyTimerView()
@ -386,8 +384,8 @@ class ConversationInfoActivity :
} }
private fun webinaryRoomType(conversation: ConversationModel): Boolean { private fun webinaryRoomType(conversation: ConversationModel): Boolean {
return conversation.type == ConversationType.ROOM_GROUP_CALL || return conversation.type == ConversationEnums.ConversationType.ROOM_GROUP_CALL ||
conversation.type == ConversationType.ROOM_PUBLIC_CALL conversation.type == ConversationEnums.ConversationType.ROOM_PUBLIC_CALL
} }
private fun reconfigureLobbyTimerView(dateTime: Calendar? = null) { private fun reconfigureLobbyTimerView(dateTime: Calendar? = null) {
@ -402,9 +400,9 @@ class ConversationInfoActivity :
} }
conversation!!.lobbyState = if (isChecked) { conversation!!.lobbyState = if (isChecked) {
LobbyState.LOBBY_STATE_MODERATORS_ONLY ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY
} else { } else {
LobbyState.LOBBY_STATE_ALL_PARTICIPANTS ConversationEnums.LobbyState.LOBBY_STATE_ALL_PARTICIPANTS
} }
if ( if (
@ -760,13 +758,13 @@ class ConversationInfoActivity :
binding.deleteConversationAction.visibility = VISIBLE binding.deleteConversationAction.visibility = VISIBLE
} }
if (ConversationType.ROOM_SYSTEM == conversation!!.type) { if (ConversationEnums.ConversationType.ROOM_SYSTEM == conversation!!.type) {
binding.notificationSettingsView.callNotificationsSwitch.visibility = GONE binding.notificationSettingsView.callNotificationsSwitch.visibility = GONE
} }
binding.listBansButton.visibility = binding.listBansButton.visibility =
if (ConversationUtils.canModerate(conversationCopy, spreedCapabilities) && 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 VISIBLE
} else { } else {
@ -922,7 +920,7 @@ class ConversationInfoActivity :
binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown.isEnabled = true binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown.isEnabled = true
binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown.alpha = 1.0f binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown.alpha = 1.0f
if (conversation!!.notificationLevel != NotificationLevel.DEFAULT) { if (conversation!!.notificationLevel != ConversationEnums.NotificationLevel.DEFAULT) {
val stringValue: String = val stringValue: String =
when ( when (
DomainEnumNotificationLevelConverter() DomainEnumNotificationLevelConverter()
@ -952,7 +950,7 @@ class ConversationInfoActivity :
} }
private fun setProperNotificationValue(conversation: ConversationModel?) { 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)) { if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MENTION_FLAG)) {
binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown.setText( binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown.setText(
resources.getString(R.string.nc_notify_me_always) resources.getString(R.string.nc_notify_me_always)
@ -971,7 +969,10 @@ class ConversationInfoActivity :
private fun loadConversationAvatar() { private fun loadConversationAvatar() {
when (conversation!!.type) { 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 { conversation!!.name?.let {
binding.avatarImage.loadUserAvatar( binding.avatarImage.loadUserAvatar(
conversationUser, 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( binding.avatarImage.loadConversationAvatar(
conversationUser, conversationUser,
conversation!!, conversation!!,
@ -991,11 +992,11 @@ class ConversationInfoActivity :
) )
} }
ConversationType.ROOM_SYSTEM -> { ConversationEnums.ConversationType.ROOM_SYSTEM -> {
binding.avatarImage.loadSystemAvatar() binding.avatarImage.loadSystemAvatar()
} }
ConversationType.NOTE_TO_SELF -> { ConversationEnums.ConversationType.NOTE_TO_SELF -> {
binding.avatarImage.loadNoteToSelfAvatar() binding.avatarImage.loadNoteToSelfAvatar()
} }

View File

@ -19,8 +19,8 @@ import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.ActivityConversationInfoBinding import com.nextcloud.talk.databinding.ActivityConversationInfoBinding
import com.nextcloud.talk.databinding.DialogPasswordBinding import com.nextcloud.talk.databinding.DialogPasswordBinding
import com.nextcloud.talk.models.domain.ConversationModel 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.capabilities.SpreedCapability
import com.nextcloud.talk.models.json.conversations.ConversationEnums
import com.nextcloud.talk.repositories.conversations.ConversationsRepository import com.nextcloud.talk.repositories.conversations.ConversationsRepository
import com.nextcloud.talk.utils.ConversationUtils import com.nextcloud.talk.utils.ConversationUtils
import io.reactivex.Observer import io.reactivex.Observer
@ -47,7 +47,7 @@ class GuestAccessHelper(
binding.guestAccessView.guestAccessSettings.visibility = View.GONE 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 binding.guestAccessView.allowGuestsSwitch.isChecked = true
showAllOptions() showAllOptions()
if (conversation.hasPassword) { if (conversation.hasPassword) {

View File

@ -12,7 +12,7 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel 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.data.user.model.User
import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.domain.ConversationModel
import com.nextcloud.talk.models.json.capabilities.SpreedCapability import com.nextcloud.talk.models.json.capabilities.SpreedCapability
@ -26,7 +26,7 @@ import io.reactivex.schedulers.Schedulers
import javax.inject.Inject import javax.inject.Inject
class ConversationInfoViewModel @Inject constructor( class ConversationInfoViewModel @Inject constructor(
private val chatRepository: ChatRepository private val chatNetworkDataSource: ChatNetworkDataSource
) : ViewModel() { ) : ViewModel() {
object LifeCycleObserver : DefaultLifecycleObserver { object LifeCycleObserver : DefaultLifecycleObserver {
@ -92,7 +92,7 @@ class ConversationInfoViewModel @Inject constructor(
fun getRoom(user: User, token: String) { fun getRoom(user: User, token: String) {
_viewState.value = GetRoomStartState _viewState.value = GetRoomStartState
chatRepository.getRoom(user, token) chatNetworkDataSource.getRoom(user, token)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread()) ?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(GetRoomObserver()) ?.subscribe(GetRoomObserver())
@ -104,7 +104,7 @@ class ConversationInfoViewModel @Inject constructor(
if (conversationModel.remoteServer.isNullOrEmpty()) { if (conversationModel.remoteServer.isNullOrEmpty()) {
_getCapabilitiesViewState.value = GetCapabilitiesSuccessState(user.capabilities!!.spreedCapability!!) _getCapabilitiesViewState.value = GetCapabilitiesSuccessState(user.capabilities!!.spreedCapability!!)
} else { } else {
chatRepository.getCapabilities(user, token) chatNetworkDataSource.getCapabilities(user, token)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread()) ?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(object : Observer<SpreedCapability> { ?.subscribe(object : Observer<SpreedCapability> {
@ -130,7 +130,7 @@ class ConversationInfoViewModel @Inject constructor(
fun listBans(user: User, token: String) { fun listBans(user: User, token: String) {
val url = ApiUtils.getUrlForBans(user.baseUrl!!, token) val url = ApiUtils.getUrlForBans(user.baseUrl!!, token)
chatRepository.listBans(user.getCredentials(), url) chatNetworkDataSource.listBans(user.getCredentials(), url)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread()) ?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(object : Observer<List<TalkBan>> { ?.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) { fun banActor(user: User, token: String, actorType: String, actorId: String, internalNote: String) {
val url = ApiUtils.getUrlForBans(user.baseUrl!!, token) 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()) .subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread()) ?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(object : Observer<TalkBan> { ?.subscribe(object : Observer<TalkBan> {
@ -178,7 +178,7 @@ class ConversationInfoViewModel @Inject constructor(
fun unbanActor(user: User, token: String, banId: Int) { fun unbanActor(user: User, token: String, banId: Int) {
val url = ApiUtils.getUrlForUnban(user.baseUrl!!, token, banId) val url = ApiUtils.getUrlForUnban(user.baseUrl!!, token, banId)
chatRepository.unbanActor(user.getCredentials(), url) chatNetworkDataSource.unbanActor(user.getCredentials(), url)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread()) ?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(object : Observer<GenericOverall> { ?.subscribe(object : Observer<GenericOverall> {

View File

@ -34,8 +34,8 @@ import com.nextcloud.talk.extensions.loadConversationAvatar
import com.nextcloud.talk.extensions.loadSystemAvatar import com.nextcloud.talk.extensions.loadSystemAvatar
import com.nextcloud.talk.extensions.loadUserAvatar import com.nextcloud.talk.extensions.loadUserAvatar
import com.nextcloud.talk.models.domain.ConversationModel 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.capabilities.SpreedCapability
import com.nextcloud.talk.models.json.conversations.ConversationEnums
import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.CapabilitiesUtil import com.nextcloud.talk.utils.CapabilitiesUtil
@ -126,10 +126,6 @@ class ConversationInfoEditActivity : BaseActivity() {
initObservers() initObservers()
} }
override fun onResume() {
super.onResume()
}
private fun initObservers() { private fun initObservers() {
conversationInfoEditViewModel.viewState.observe(this) { state -> conversationInfoEditViewModel.viewState.observe(this) { state ->
when (state) { when (state) {
@ -349,15 +345,18 @@ class ConversationInfoEditActivity : BaseActivity() {
setupAvatarOptions() setupAvatarOptions()
when (conversation!!.type) { 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) } 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) binding.avatarImage.loadConversationAvatar(conversationUser, conversation!!, false, viewThemeUtils)
} }
ConversationType.ROOM_SYSTEM -> { ConversationEnums.ConversationType.ROOM_SYSTEM -> {
binding.avatarImage.loadSystemAvatar() binding.avatarImage.loadSystemAvatar()
} }

View File

@ -31,7 +31,7 @@ class ConversationInfoEditRepositoryImpl(private val ncApi: NcApi, currentUserPr
builder.setType(MultipartBody.FORM) builder.setType(MultipartBody.FORM)
builder.addFormDataPart( builder.addFormDataPart(
"file", "file",
file!!.name, file.name,
file.asRequestBody(Mimetype.IMAGE_PREFIX_GENERIC.toMediaTypeOrNull()) file.asRequestBody(Mimetype.IMAGE_PREFIX_GENERIC.toMediaTypeOrNull())
) )
val filePart: MultipartBody.Part = MultipartBody.Part.createFormData( val filePart: MultipartBody.Part = MultipartBody.Part.createFormData(
@ -44,13 +44,13 @@ class ConversationInfoEditRepositoryImpl(private val ncApi: NcApi, currentUserPr
credentials, credentials,
ApiUtils.getUrlForConversationAvatar(1, user.baseUrl!!, roomToken), ApiUtils.getUrlForConversationAvatar(1, user.baseUrl!!, roomToken),
filePart filePart
).map { ConversationModel.mapToConversationModel(it.ocs?.data!!) } ).map { ConversationModel.mapToConversationModel(it.ocs?.data!!, user) }
} }
override fun deleteConversationAvatar(user: User, roomToken: String): Observable<ConversationModel> { override fun deleteConversationAvatar(user: User, roomToken: String): Observable<ConversationModel> {
return ncApi.deleteConversationAvatar( return ncApi.deleteConversationAvatar(
credentials, credentials,
ApiUtils.getUrlForConversationAvatar(1, user.baseUrl!!, roomToken) ApiUtils.getUrlForConversationAvatar(1, user.baseUrl!!, roomToken)
).map { ConversationModel.mapToConversationModel(it.ocs?.data!!) } ).map { ConversationModel.mapToConversationModel(it.ocs?.data!!, user) }
} }
} }

View File

@ -10,7 +10,7 @@ import android.util.Log
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel 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.conversationinfoedit.data.ConversationInfoEditRepository
import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.domain.ConversationModel
@ -22,7 +22,7 @@ import java.io.File
import javax.inject.Inject import javax.inject.Inject
class ConversationInfoEditViewModel @Inject constructor( class ConversationInfoEditViewModel @Inject constructor(
private val repository: ChatRepository, private val repository: ChatNetworkDataSource,
private val conversationInfoEditRepository: ConversationInfoEditRepository private val conversationInfoEditRepository: ConversationInfoEditRepository
) : ViewModel() { ) : ViewModel() {

View File

@ -45,6 +45,7 @@ import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuItemCompat import androidx.core.view.MenuItemCompat
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.work.Data import androidx.work.Data
import androidx.work.OneTimeWorkRequest import androidx.work.OneTimeWorkRequest
@ -91,8 +92,8 @@ import com.nextcloud.talk.jobs.DeleteConversationWorker
import com.nextcloud.talk.jobs.UploadAndShareFilesWorker import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
import com.nextcloud.talk.messagesearch.MessageSearchHelper import com.nextcloud.talk.messagesearch.MessageSearchHelper
import com.nextcloud.talk.messagesearch.MessageSearchHelper.MessageSearchResults import com.nextcloud.talk.messagesearch.MessageSearchHelper.MessageSearchResults
import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.models.domain.ConversationModel
import com.nextcloud.talk.models.json.conversations.RoomsOverall import com.nextcloud.talk.models.json.conversations.ConversationEnums
import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository
import com.nextcloud.talk.settings.SettingsActivity import com.nextcloud.talk.settings.SettingsActivity
import com.nextcloud.talk.ui.dialog.ChooseAccountDialogFragment 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.isUnifiedSearchAvailable
import com.nextcloud.talk.utils.CapabilitiesUtil.isUserStatusAvailable import com.nextcloud.talk.utils.CapabilitiesUtil.isUserStatusAvailable
import com.nextcloud.talk.utils.ClosedInterfaceImpl import com.nextcloud.talk.utils.ClosedInterfaceImpl
import com.nextcloud.talk.utils.ConversationUtils
import com.nextcloud.talk.utils.FileUtils import com.nextcloud.talk.utils.FileUtils
import com.nextcloud.talk.utils.Mimetype import com.nextcloud.talk.utils.Mimetype
import com.nextcloud.talk.utils.ParticipantPermissions import com.nextcloud.talk.utils.ParticipantPermissions
@ -134,6 +136,9 @@ import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import io.reactivex.subjects.BehaviorSubject 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.apache.commons.lang3.builder.CompareToBuilder
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
@ -190,7 +195,7 @@ class ConversationsListActivity :
private var isRefreshing = false private var isRefreshing = false
private var showShareToScreen = false private var showShareToScreen = false
private var filesToShare: ArrayList<String>? = null private var filesToShare: ArrayList<String>? = null
private var selectedConversation: Conversation? = null private var selectedConversation: ConversationModel? = null
private var textToPaste: String? = "" private var textToPaste: String? = ""
private var selectedMessageId: String? = null private var selectedMessageId: String? = null
private var forwardMessage: Boolean = false private var forwardMessage: Boolean = false
@ -259,7 +264,7 @@ class ConversationsListActivity :
if (adapter == null) { if (adapter == null) {
adapter = FlexibleAdapter(conversationItems, this, true) adapter = FlexibleAdapter(conversationItems, this, true)
} else { } else {
binding?.loadingContent?.visibility = View.GONE binding.loadingContent?.visibility = View.GONE
} }
adapter!!.addListener(this) adapter!!.addListener(this)
prepareViews() prepareViews()
@ -334,6 +339,51 @@ class ConversationsListActivity :
else -> {} 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() { fun filterConversation() {
@ -374,7 +424,7 @@ class ConversationsListActivity :
updateFilterConversationButtonColor() updateFilterConversationButtonColor()
} }
private fun filter(conversation: Conversation): Boolean { private fun filter(conversation: ConversationModel): Boolean {
var result = true var result = true
for ((k, v) in filterState) { for ((k, v) in filterState) {
if (v) { if (v) {
@ -383,8 +433,8 @@ class ConversationsListActivity :
( (
result && result &&
( (
conversation.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL || conversation.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL ||
conversation.type == Conversation.ConversationType.FORMER_ONE_TO_ONE conversation.type == ConversationEnums.ConversationType.FORMER_ONE_TO_ONE
) && ) &&
(conversation.unreadMessages > 0) (conversation.unreadMessages > 0)
) )
@ -573,7 +623,7 @@ class ConversationsListActivity :
if (!filterState.containsValue(true)) filterableConversationItems = searchableConversationItems if (!filterState.containsValue(true)) filterableConversationItems = searchableConversationItems
adapter!!.updateDataSet(filterableConversationItems, false) adapter!!.updateDataSet(filterableConversationItems, false)
adapter!!.showAllHeaders() adapter!!.showAllHeaders()
binding?.swipeRefreshLayoutView?.isEnabled = false binding.swipeRefreshLayoutView?.isEnabled = false
searchBehaviorSubject.onNext(true) searchBehaviorSubject.onNext(true)
return true return true
} }
@ -586,10 +636,10 @@ class ConversationsListActivity :
if (searchHelper != null) { if (searchHelper != null) {
// cancel any pending searches // cancel any pending searches
searchHelper!!.cancelSearch() searchHelper!!.cancelSearch()
binding?.swipeRefreshLayoutView?.isRefreshing = false binding.swipeRefreshLayoutView?.isRefreshing = false
searchBehaviorSubject.onNext(false) searchBehaviorSubject.onNext(false)
} }
binding?.swipeRefreshLayoutView?.isEnabled = true binding.swipeRefreshLayoutView?.isEnabled = true
searchView!!.onActionViewCollapsed() searchView!!.onActionViewCollapsed()
binding.conversationListAppbar.stateListAnimator = AnimatorInflater.loadStateListAnimator( binding.conversationListAppbar.stateListAnimator = AnimatorInflater.loadStateListAnimator(
@ -602,7 +652,7 @@ class ConversationsListActivity :
viewThemeUtils.platform.resetStatusBar(this@ConversationsListActivity) viewThemeUtils.platform.resetStatusBar(this@ConversationsListActivity)
} }
val layoutManager = binding?.recyclerView?.layoutManager as SmoothScrollLinearLayoutManager? val layoutManager = binding.recyclerView?.layoutManager as SmoothScrollLinearLayoutManager?
layoutManager?.scrollToPositionWithOffset(0, 0) layoutManager?.scrollToPositionWithOffset(0, 0)
return true return true
} }
@ -681,67 +731,68 @@ class ConversationsListActivity :
} }
fun fetchRooms() { fun fetchRooms() {
val includeStatus = isUserStatusAvailable(userManager.currentUser.blockingGet()) val includeStatus = isUserStatusAvailable(currentUser!!)
conversationsListViewModel.getRooms()
// checks internet connection before fetching rooms // checks internet connection before fetching rooms
if (isNetworkAvailable(context)) { if (isNetworkAvailable(context)) {
Log.d(TAG, "Internet connection available") // Log.d(TAG, "Internet connection available")
dispose(null) // dispose(null)
isRefreshing = true // isRefreshing = true
conversationItems = ArrayList() // conversationItems = ArrayList()
conversationItemsWithHeader = ArrayList() // conversationItemsWithHeader = ArrayList()
val apiVersion = ApiUtils.getConversationApiVersion( // val apiVersion = ApiUtils.getConversationApiVersion(
currentUser!!, // currentUser!!,
intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1) // intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1)
) // )
val startNanoTime = System.nanoTime() // val startNanoTime = System.nanoTime()
Log.d(TAG, "fetchData - getRooms - calling: $startNanoTime") // Log.d(TAG, "fetchData - getRooms - calling: $startNanoTime")
roomsQueryDisposable = ncApi.getRooms( // roomsQueryDisposable = ncApi.getRooms(
credentials, // credentials,
ApiUtils.getUrlForRooms( // ApiUtils.getUrlForRooms(
apiVersion, // apiVersion,
currentUser!!.baseUrl // currentUser!!.baseUrl
), // ),
includeStatus // includeStatus
) // )
.subscribeOn(Schedulers.io()) // .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) // .observeOn(AndroidSchedulers.mainThread())
.subscribe({ (ocs): RoomsOverall -> // .subscribe({ (ocs): RoomsOverall ->
Log.d(TAG, "fetchData - getRooms - got response: $startNanoTime") // Log.d(TAG, "fetchData - getRooms - got response: $startNanoTime")
//
// This is invoked asynchronously, when server returns a response the view might have been // // 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. // // 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? // // FIXME - does it make sense to update internal data structures even when view has been unbound?
// if (view == null) { // // if (view == null) {
// Log.d(TAG, "fetchData - getRooms - view is not bound: $startNanoTime") // // Log.d(TAG, "fetchData - getRooms - view is not bound: $startNanoTime")
// return@subscribe // // 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
// } // }
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 { } else {
Log.d(TAG, "No internet connection detected") Log.d(TAG, "No internet connection detected")
showNetworkErrorDialog() showNetworkErrorDialog()
@ -760,31 +811,31 @@ class ConversationsListActivity :
private fun initOverallLayout(isConversationListNotEmpty: Boolean) { private fun initOverallLayout(isConversationListNotEmpty: Boolean) {
if (isConversationListNotEmpty) { if (isConversationListNotEmpty) {
if (binding?.emptyLayout?.visibility != View.GONE) { if (binding.emptyLayout?.visibility != View.GONE) {
binding?.emptyLayout?.visibility = View.GONE binding.emptyLayout?.visibility = View.GONE
} }
if (binding?.swipeRefreshLayoutView?.visibility != View.VISIBLE) { if (binding.swipeRefreshLayoutView?.visibility != View.VISIBLE) {
binding?.swipeRefreshLayoutView?.visibility = View.VISIBLE binding.swipeRefreshLayoutView?.visibility = View.VISIBLE
} }
} else { } else {
if (binding?.emptyLayout?.visibility != View.VISIBLE) { if (binding.emptyLayout?.visibility != View.VISIBLE) {
binding?.emptyLayout?.visibility = View.VISIBLE binding.emptyLayout?.visibility = View.VISIBLE
} }
if (binding?.swipeRefreshLayoutView?.visibility != View.GONE) { if (binding.swipeRefreshLayoutView?.visibility != View.GONE) {
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 && if (intent.getStringExtra(KEY_FORWARD_HIDE_SOURCE_ROOM) != null &&
intent.getStringExtra(KEY_FORWARD_HIDE_SOURCE_ROOM) == conversation.roomId intent.getStringExtra(KEY_FORWARD_HIDE_SOURCE_ROOM) == conversation.roomId
) { ) {
return return
} }
if (conversation.objectType == Conversation.ObjectType.ROOM && if (conversation.objectType == ConversationEnums.ObjectType.ROOM &&
conversation.lobbyState == Conversation.LobbyState.LOBBY_STATE_MODERATORS_ONLY conversation.lobbyState == ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY
) { ) {
return return
} }
@ -909,35 +960,35 @@ class ConversationsListActivity :
) )
) { ) {
val openConversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList() val openConversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
openConversationsQueryDisposable = ncApi.getOpenConversations( // openConversationsQueryDisposable = ncApi.getOpenConversations(
credentials, // credentials,
ApiUtils.getUrlForOpenConversations(apiVersion, currentUser!!.baseUrl!!) // ApiUtils.getUrlForOpenConversations(apiVersion, currentUser!!.baseUrl!!)
) // )
.subscribeOn(Schedulers.io()) // .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) // .observeOn(AndroidSchedulers.mainThread())
.subscribe({ (ocs): RoomsOverall -> // .subscribe({ (ocs): RoomsOverall ->
for (conversation in ocs!!.data!!) { // for (conversation in ocs!!.data!!) {
val headerTitle = resources!!.getString(R.string.openConversations) // val headerTitle = resources!!.getString(R.string.openConversations)
var genericTextHeaderItem: GenericTextHeaderItem // var genericTextHeaderItem: GenericTextHeaderItem
if (!callHeaderItems.containsKey(headerTitle)) { // if (!callHeaderItems.containsKey(headerTitle)) {
genericTextHeaderItem = GenericTextHeaderItem(headerTitle, viewThemeUtils) // genericTextHeaderItem = GenericTextHeaderItem(headerTitle, viewThemeUtils)
callHeaderItems[headerTitle] = genericTextHeaderItem // callHeaderItems[headerTitle] = genericTextHeaderItem
} // }
val conversationItem = ConversationItem( // val conversationItem = ConversationItem(
conversation, // conversation,
currentUser!!, // currentUser!!,
this, // this,
callHeaderItems[headerTitle], // callHeaderItems[headerTitle],
viewThemeUtils // viewThemeUtils
) // )
openConversationItems.add(conversationItem) // openConversationItems.add(conversationItem)
} // }
searchableConversationItems.addAll(openConversationItems) // searchableConversationItems.addAll(openConversationItems)
}, { throwable: Throwable -> // }, { throwable: Throwable ->
Log.e(TAG, "fetchData - getRooms - ERROR", throwable) // Log.e(TAG, "fetchData - getRooms - ERROR", throwable)
handleHttpExceptions(throwable) // handleHttpExceptions(throwable)
dispose(openConversationsQueryDisposable) // dispose(openConversationsQueryDisposable)
}) { dispose(openConversationsQueryDisposable) } // }) { dispose(openConversationsQueryDisposable) }
} else { } else {
Log.d(TAG, "no open conversations fetched because of missing capability") 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) { if (!isDestroyed) {
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(v.windowToken, 0) imm.hideSoftInputFromWindow(v.windowToken, 0)
} }
false false
} }
binding?.swipeRefreshLayoutView?.setOnRefreshListener { binding.swipeRefreshLayoutView?.setOnRefreshListener {
fetchRooms() fetchRooms()
fetchPendingInvitations() fetchPendingInvitations()
} }
binding?.swipeRefreshLayoutView?.let { viewThemeUtils.androidx.themeSwipeRefreshLayout(it) } binding.swipeRefreshLayoutView?.let { viewThemeUtils.androidx.themeSwipeRefreshLayout(it) }
binding?.emptyLayout?.setOnClickListener { showNewConversationsScreen() } binding.emptyLayout?.setOnClickListener { showNewConversationsScreen() }
binding?.floatingActionButton?.setOnClickListener { binding.floatingActionButton?.setOnClickListener {
run(context) run(context)
showNewConversationsScreen() showNewConversationsScreen()
} }
binding?.floatingActionButton?.let { viewThemeUtils.material.themeFAB(it) } binding.floatingActionButton?.let { viewThemeUtils.material.themeFAB(it) }
binding.switchAccountButton.setOnClickListener { binding.switchAccountButton.setOnClickListener {
if (resources != null && resources!!.getBoolean(R.bool.multiaccount_support)) { if (resources != null && resources!!.getBoolean(R.bool.multiaccount_support)) {
@ -1015,13 +1066,13 @@ class ConversationsListActivity :
newFragment.show(supportFragmentManager, FilterConversationFragment.TAG) newFragment.show(supportFragmentManager, FilterConversationFragment.TAG)
} }
binding?.newMentionPopupBubble?.hide() binding.newMentionPopupBubble?.hide()
binding?.newMentionPopupBubble?.setPopupBubbleListener { binding.newMentionPopupBubble?.setPopupBubbleListener {
binding?.recyclerView?.smoothScrollToPosition( binding.recyclerView?.smoothScrollToPosition(
nextUnreadConversationScrollPosition nextUnreadConversationScrollPosition
) )
} }
binding?.newMentionPopupBubble?.let { viewThemeUtils.material.colorMaterialButtonPrimaryFilled(it) } binding.newMentionPopupBubble?.let { viewThemeUtils.material.colorMaterialButtonPrimaryFilled(it) }
} }
private fun hideLogoForBrandedClients() { private fun hideLogoForBrandedClients() {
@ -1041,17 +1092,17 @@ class ConversationsListActivity :
try { try {
val lastVisibleItem = layoutManager!!.findLastCompletelyVisibleItemPosition() val lastVisibleItem = layoutManager!!.findLastCompletelyVisibleItemPosition()
for (flexItem in conversationItems) { for (flexItem in conversationItems) {
val conversation: Conversation = (flexItem as ConversationItem).model val conversation: ConversationModel = (flexItem as ConversationItem).model
val position = adapter!!.getGlobalPositionOf(flexItem) val position = adapter!!.getGlobalPositionOf(flexItem)
if (hasUnreadItems(conversation) && position > lastVisibleItem) { if (hasUnreadItems(conversation) && position > lastVisibleItem) {
nextUnreadConversationScrollPosition = position nextUnreadConversationScrollPosition = position
if (!binding?.newMentionPopupBubble?.isShown!!) { if (!binding.newMentionPopupBubble?.isShown!!) {
binding?.newMentionPopupBubble?.show() binding.newMentionPopupBubble?.show()
} }
return@subscribe return@subscribe
} }
nextUnreadConversationScrollPosition = 0 nextUnreadConversationScrollPosition = 0
binding?.newMentionPopupBubble?.hide() binding.newMentionPopupBubble?.hide()
} }
} catch (e: NullPointerException) { } catch (e: NullPointerException) {
Log.d( Log.d(
@ -1066,10 +1117,10 @@ class ConversationsListActivity :
} }
} }
private fun hasUnreadItems(conversation: Conversation) = private fun hasUnreadItems(conversation: ConversationModel) =
conversation.unreadMention || conversation.unreadMention ||
conversation.unreadMessages > 0 && 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() { private fun showNewConversationsScreen() {
val intent = Intent(context, ContactsActivityCompose::class.java) val intent = Intent(context, ContactsActivityCompose::class.java)
@ -1157,7 +1208,7 @@ class ConversationsListActivity :
@SuppressLint("CheckResult") // handled by helper @SuppressLint("CheckResult") // handled by helper
private fun startMessageSearch(search: String?) { private fun startMessageSearch(search: String?) {
binding?.swipeRefreshLayoutView?.isRefreshing = true binding.swipeRefreshLayoutView?.isRefreshing = true
searchHelper?.startMessageSearch(search!!) searchHelper?.startMessageSearch(search!!)
?.subscribeOn(Schedulers.io()) ?.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread()) ?.observeOn(AndroidSchedulers.mainThread())
@ -1214,7 +1265,7 @@ class ConversationsListActivity :
} }
@Suppress("Detekt.ComplexMethod") @Suppress("Detekt.ComplexMethod")
private fun handleConversation(conversation: Conversation?) { private fun handleConversation(conversation: ConversationModel?) {
selectedConversation = conversation selectedConversation = conversation
if (selectedConversation != null) { if (selectedConversation != null) {
val hasChatPermission = ParticipantPermissions( val hasChatPermission = ParticipantPermissions(
@ -1244,19 +1295,19 @@ class ConversationsListActivity :
} }
} }
private fun shouldShowLobby(conversation: Conversation): Boolean { private fun shouldShowLobby(conversation: ConversationModel): Boolean {
val participantPermissions = ParticipantPermissions( val participantPermissions = ParticipantPermissions(
currentUser!!.capabilities?.spreedCapability!!, currentUser!!.capabilities?.spreedCapability!!,
conversation selectedConversation!!
) )
return conversation.lobbyState == Conversation.LobbyState.LOBBY_STATE_MODERATORS_ONLY && return conversation.lobbyState == ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY &&
!conversation.canModerate(currentUser!!) && !ConversationUtils.canModerate(conversation, currentUser!!.capabilities!!.spreedCapability!!) &&
!participantPermissions.canIgnoreLobby() !participantPermissions.canIgnoreLobby()
} }
private fun isReadOnlyConversation(conversation: Conversation): Boolean { private fun isReadOnlyConversation(conversation: ConversationModel): Boolean {
return conversation.conversationReadOnlyState === return conversation.conversationReadOnlyState ===
Conversation.ConversationReadOnlyState.CONVERSATION_READ_ONLY ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_ONLY
} }
private fun handleSharedData() { private fun handleSharedData() {
@ -1519,7 +1570,7 @@ class ConversationsListActivity :
}, BOTTOM_SHEET_DELAY) }, BOTTOM_SHEET_DELAY)
} }
fun showDeleteConversationDialog(conversation: Conversation) { fun showDeleteConversationDialog(conversation: ConversationModel) {
binding.floatingActionButton.let { binding.floatingActionButton.let {
val dialogBuilder = MaterialAlertDialogBuilder(it.context) val dialogBuilder = MaterialAlertDialogBuilder(it.context)
.setIcon( .setIcon(
@ -1751,7 +1802,7 @@ class ConversationsListActivity :
} }
} }
private fun deleteConversation(conversation: Conversation) { private fun deleteConversation(conversation: ConversationModel) {
val data = Data.Builder() val data = Data.Builder()
data.putLong( data.putLong(
KEY_INTERNAL_USER_ID, KEY_INTERNAL_USER_ID,
@ -1810,15 +1861,15 @@ class ConversationsListActivity :
} }
// add unified search result at the end of the list // add unified search result at the end of the list
adapter!!.addItems(adapter!!.mainItemCount + adapter!!.scrollableHeaders.size, adapterItems) 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) { private fun onMessageSearchError(throwable: Throwable) {
handleHttpExceptions(throwable) handleHttpExceptions(throwable)
binding?.swipeRefreshLayoutView?.isRefreshing = false binding.swipeRefreshLayoutView?.isRefreshing = false
showErrorDialog() showErrorDialog()
} }

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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>>
}

View File

@ -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()
}
}

View File

@ -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()
}
}
}

View File

@ -10,7 +10,7 @@ import android.util.Log
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel 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.InvitationsModel
import com.nextcloud.talk.invitation.data.InvitationsRepository import com.nextcloud.talk.invitation.data.InvitationsRepository
import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.users.UserManager
@ -18,21 +18,36 @@ import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.onEach
import javax.inject.Inject import javax.inject.Inject
class ConversationsListViewModel @Inject constructor( class ConversationsListViewModel @Inject constructor(
private val conversationsListRepository: ConversationsListRepository private val repository: OfflineConversationsRepository,
var userManager: UserManager
) : ) :
ViewModel() { ViewModel() {
@Inject @Inject
lateinit var invitationsRepository: InvitationsRepository lateinit var invitationsRepository: InvitationsRepository
@Inject
lateinit var userManager: UserManager
sealed interface ViewState 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 GetFederationInvitationsStartState : ViewState
object GetFederationInvitationsErrorState : 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> { inner class FederatedInvitationsObserver : Observer<InvitationsModel> {
override fun onSubscribe(d: Disposable) { override fun onSubscribe(d: Disposable) {
// unused atm // unused atm

View File

@ -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()
}

View File

@ -9,6 +9,8 @@ package com.nextcloud.talk.dagger.modules;
import android.content.Context; 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.data.source.local.TalkDatabase;
import com.nextcloud.talk.utils.preferences.AppPreferences; import com.nextcloud.talk.utils.preferences.AppPreferences;
import com.nextcloud.talk.utils.preferences.AppPreferencesImpl; import com.nextcloud.talk.utils.preferences.AppPreferencesImpl;
@ -44,4 +46,10 @@ public class DatabaseModule {
@NonNull final AppPreferences appPreferences) { @NonNull final AppPreferences appPreferences) {
return TalkDatabase.getInstance(context, appPreferences); return TalkDatabase.getInstance(context, appPreferences);
} }
@Provides
@Singleton
public NetworkMonitor provideNetworkMonitor(@NonNull final Context poContext) {
return new NetworkMonitorImpl(poContext);
}
} }

View File

@ -1,7 +1,7 @@
/* /*
* Nextcloud Talk - Android Client * 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 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com> * SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH * SPDX-FileCopyrightText: 2022 Nextcloud GmbH
@ -10,17 +10,25 @@
package com.nextcloud.talk.dagger.modules package com.nextcloud.talk.dagger.modules
import com.nextcloud.talk.api.NcApi 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.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.ContactsRepository
import com.nextcloud.talk.contacts.ContactsRepositoryImpl import com.nextcloud.talk.contacts.ContactsRepositoryImpl
import com.nextcloud.talk.conversation.repository.ConversationRepository import com.nextcloud.talk.conversation.repository.ConversationRepository
import com.nextcloud.talk.conversation.repository.ConversationRepositoryImpl import com.nextcloud.talk.conversation.repository.ConversationRepositoryImpl
import com.nextcloud.talk.conversationinfoedit.data.ConversationInfoEditRepository import com.nextcloud.talk.conversationinfoedit.data.ConversationInfoEditRepository
import com.nextcloud.talk.conversationinfoedit.data.ConversationInfoEditRepositoryImpl import com.nextcloud.talk.conversationinfoedit.data.ConversationInfoEditRepositoryImpl
import com.nextcloud.talk.conversationlist.data.ConversationsListRepository import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository
import com.nextcloud.talk.conversationlist.data.ConversationsListRepositoryImpl 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.source.local.TalkDatabase
import com.nextcloud.talk.data.storage.ArbitraryStoragesRepository import com.nextcloud.talk.data.storage.ArbitraryStoragesRepository
import com.nextcloud.talk.data.storage.ArbitraryStoragesRepositoryImpl 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.users.UserManager
import com.nextcloud.talk.utils.DateUtils import com.nextcloud.talk.utils.DateUtils
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
import com.nextcloud.talk.utils.preferences.AppPreferences
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -97,8 +106,12 @@ class RepositoryModule {
} }
@Provides @Provides
fun provideReactionsRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): ReactionsRepository { fun provideReactionsRepository(
return ReactionsRepositoryImpl(ncApi, userProvider) ncApi: NcApi,
userProvider: CurrentUserProviderNew,
dao: ChatMessagesDao
): ReactionsRepository {
return ReactionsRepositoryImpl(ncApi, userProvider, dao)
} }
@Provides @Provides
@ -128,13 +141,13 @@ class RepositoryModule {
} }
@Provides @Provides
fun provideConversationsListRepository(ncApi: NcApi): ConversationsListRepository { fun provideChatNetworkDataSource(ncApi: NcApi): ChatNetworkDataSource {
return ConversationsListRepositoryImpl(ncApi) return RetrofitChatNetwork(ncApi)
} }
@Provides @Provides
fun provideChatRepository(ncApi: NcApi): ChatRepository { fun provideConversationsNetworkDataSource(ncApi: NcApi): ConversationsNetworkDataSource {
return NetworkChatRepositoryImpl(ncApi) return RetrofitConversationsNetwork(ncApi)
} }
@Provides @Provides
@ -155,6 +168,34 @@ class RepositoryModule {
return InvitationsRepositoryImpl(ncApi) 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 @Provides
fun provideContactsRepository(ncApiCoroutines: NcApiCoroutines, userManager: UserManager): ContactsRepository { fun provideContactsRepository(ncApiCoroutines: NcApiCoroutines, userManager: UserManager): ContactsRepository {
return ContactsRepositoryImpl(ncApiCoroutines, userManager) return ContactsRepositoryImpl(ncApiCoroutines, userManager)

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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"
)

View File

@ -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
)

View File

@ -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>
}

View File

@ -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
}

View File

@ -1,7 +1,7 @@
/* /*
* Nextcloud Talk - Android Client * 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: 2022 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2017-2020 Mario Danic <mario@lovelyhq.com> * SPDX-FileCopyrightText: 2017-2020 Mario Danic <mario@lovelyhq.com>
* SPDX-License-Identifier: GPL-3.0-or-later * 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.content.Context
import android.util.Log import android.util.Log
import androidx.room.AutoMigration
import androidx.room.Database import androidx.room.Database
import androidx.room.Room import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverters import androidx.room.TypeConverters
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import com.nextcloud.talk.R 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.CapabilitiesConverter
import com.nextcloud.talk.data.source.local.converters.ExternalSignalingServerConverter 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.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.PushConfigurationConverter
import com.nextcloud.talk.data.source.local.converters.ServerVersionConverter import com.nextcloud.talk.data.source.local.converters.ServerVersionConverter
import com.nextcloud.talk.data.source.local.converters.SignalingSettingsConverter 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.SQLiteDatabaseHook
import net.sqlcipher.database.SupportFactory import net.sqlcipher.database.SupportFactory
import java.util.Locale import java.util.Locale
import androidx.room.AutoMigration
@Database( @Database(
entities = [UserEntity::class, ArbitraryStorageEntity::class], entities = [
UserEntity::class,
ArbitraryStorageEntity::class,
ConversationEntity::class,
ChatMessageEntity::class,
ChatBlockEntity::class
],
version = 10, version = 10,
autoMigrations = [ autoMigrations = [
AutoMigration(from = 9, to = 10) AutoMigration(from = 9, to = 10)
@ -47,11 +61,16 @@ import androidx.room.AutoMigration
ServerVersionConverter::class, ServerVersionConverter::class,
ExternalSignalingServerConverter::class, ExternalSignalingServerConverter::class,
SignalingSettingsConverter::class, SignalingSettingsConverter::class,
HashMapHashMapConverter::class HashMapHashMapConverter::class,
LinkedHashMapConverter::class,
ArrayListConverter::class
) )
abstract class TalkDatabase : RoomDatabase() { abstract class TalkDatabase : RoomDatabase() {
abstract fun usersDao(): UsersDao abstract fun usersDao(): UsersDao
abstract fun conversationsDao(): ConversationsDao
abstract fun chatMessagesDao(): ChatMessagesDao
abstract fun chatBlocksDao(): ChatBlocksDao
abstract fun arbitraryStoragesDao(): ArbitraryStoragesDao abstract fun arbitraryStoragesDao(): ArbitraryStoragesDao
companion object { companion object {
@ -89,7 +108,7 @@ abstract class TalkDatabase : RoomDatabase() {
return Room return Room
.databaseBuilder(context.applicationContext, TalkDatabase::class.java, dbName) .databaseBuilder(context.applicationContext, TalkDatabase::class.java, dbName)
// comment out openHelperFactory to view the database entries in Android Studio for debugging // 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) .addMigrations(Migrations.MIGRATION_6_8, Migrations.MIGRATION_7_8, Migrations.MIGRATION_8_9)
.allowMainThreadQueries() .allowMainThreadQueries()
.addCallback( .addCallback(

View File

@ -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>?
}
}

View File

@ -12,7 +12,7 @@ import com.bluelinelabs.logansquare.LoganSquare
class HashMapHashMapConverter { class HashMapHashMapConverter {
@TypeConverter @TypeConverter
fun fromDoubleHashMapToString(map: HashMap<String, HashMap<String, String>>?): String? { fun fromDoubleHashMapToString(map: HashMap<String?, HashMap<String?, String?>>?): String? {
return if (map == null) { return if (map == null) {
LoganSquare.serialize(hashMapOf<String, HashMap<String, String>>()) LoganSquare.serialize(hashMapOf<String, HashMap<String, String>>())
} else { } else {
@ -21,11 +21,11 @@ class HashMapHashMapConverter {
} }
@TypeConverter @TypeConverter
fun fromStringToDoubleHashMap(value: String?): HashMap<String, HashMap<String, String>>? { fun fromStringToDoubleHashMap(value: String?): HashMap<String?, HashMap<String?, String?>>? {
if (value.isNullOrEmpty()) { if (value.isNullOrEmpty()) {
return hashMapOf() 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?>>?
} }
} }

View File

@ -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()
""
}
}
}

View File

@ -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()
}
}
}

View File

@ -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

View File

@ -29,9 +29,9 @@ import coil.transform.RoundedCornersTransformation
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.domain.ConversationModel
import com.nextcloud.talk.models.domain.ConversationType import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.models.json.conversations.Conversation 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.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.DisplayUtils
@ -49,7 +49,7 @@ fun ImageView.loadConversationAvatar(
): io.reactivex.disposables.Disposable { ): io.reactivex.disposables.Disposable {
return loadConversationAvatar( return loadConversationAvatar(
user, user,
ConversationModel.mapToConversationModel(conversation), ConversationModel.mapToConversationModel(conversation, user),
ignoreCache, ignoreCache,
viewThemeUtils viewThemeUtils
) )
@ -72,10 +72,10 @@ fun ImageView.loadConversationAvatar(
if (conversation.avatarVersion.isNullOrEmpty() && viewThemeUtils != null) { if (conversation.avatarVersion.isNullOrEmpty() && viewThemeUtils != null) {
when (conversation.type) { when (conversation.type) {
ConversationType.ROOM_GROUP_CALL -> ConversationEnums.ConversationType.ROOM_GROUP_CALL ->
return loadDefaultGroupCallAvatar(viewThemeUtils) return loadDefaultGroupCallAvatar(viewThemeUtils)
ConversationType.ROOM_PUBLIC_CALL -> ConversationEnums.ConversationType.ROOM_PUBLIC_CALL ->
return loadDefaultPublicCallAvatar(viewThemeUtils) return loadDefaultPublicCallAvatar(viewThemeUtils)
else -> {} 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..) // when no own images are set. (although these default avatars can not be themed for the android app..)
val errorPlaceholder = val errorPlaceholder =
when (conversation.type) { when (conversation.type) {
ConversationType.ROOM_GROUP_CALL -> ConversationEnums.ConversationType.ROOM_GROUP_CALL ->
ContextCompat.getDrawable(context, R.drawable.ic_circular_group) 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) ContextCompat.getDrawable(context, R.drawable.ic_circular_link)
else -> ContextCompat.getDrawable(context, R.drawable.account_circle_96dp) else -> ContextCompat.getDrawable(context, R.drawable.account_circle_96dp)

View File

@ -16,6 +16,9 @@ import com.nextcloud.talk.R;
import com.nextcloud.talk.api.NcApi; import com.nextcloud.talk.api.NcApi;
import com.nextcloud.talk.application.NextcloudTalkApplication; import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager; 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.data.user.model.User;
import com.nextcloud.talk.models.json.generic.GenericMeta; import com.nextcloud.talk.models.json.generic.GenericMeta;
import com.nextcloud.talk.models.json.generic.GenericOverall; import com.nextcloud.talk.models.json.generic.GenericOverall;
@ -46,17 +49,19 @@ import retrofit2.Retrofit;
public class AccountRemovalWorker extends Worker { public class AccountRemovalWorker extends Worker {
public static final String TAG = "AccountRemovalWorker"; public static final String TAG = "AccountRemovalWorker";
@Inject @Inject UserManager userManager;
UserManager userManager;
@Inject @Inject ArbitraryStorageManager arbitraryStorageManager;
ArbitraryStorageManager arbitraryStorageManager;
@Inject @Inject Retrofit retrofit;
Retrofit retrofit;
@Inject @Inject OkHttpClient okHttpClient;
OkHttpClient okHttpClient;
@Inject ChatMessagesDao chatMessagesDao;
@Inject ConversationsDao conversationsDao;
@Inject ChatBlocksDao chatBlocksDao;
NcApi ncApi; NcApi ncApi;
@ -177,6 +182,7 @@ public class AccountRemovalWorker extends Worker {
try { try {
arbitraryStorageManager.deleteAllEntriesForAccountIdentifier(user.getId()); arbitraryStorageManager.deleteAllEntriesForAccountIdentifier(user.getId());
deleteAllUserInfo(user);
deleteUser(user); deleteUser(user);
} catch (Throwable e) { } catch (Throwable e) {
Log.e(TAG, "error while trying to delete All Entries For Account Identifier", 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) { private void deleteUser(User user) {
if (user.getId() != null) { if (user.getId() != null) {
String username = user.getUsername(); String username = user.getUsername();

View File

@ -49,11 +49,11 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager
import com.nextcloud.talk.callnotification.CallNotificationActivity 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.SignatureVerification
import com.nextcloud.talk.models.domain.ConversationModel 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.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.notifications.NotificationOverall
import com.nextcloud.talk.models.json.participants.Participant import com.nextcloud.talk.models.json.participants.Participant
import com.nextcloud.talk.models.json.participants.ParticipantsOverall import com.nextcloud.talk.models.json.participants.ParticipantsOverall
@ -125,7 +125,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
@Inject @Inject
var retrofit: Retrofit? = null var retrofit: Retrofit? = null
var chatRepository: ChatRepository? = null var chatNetworkDataSource: ChatNetworkDataSource? = null
@Inject set @Inject set
@Inject @Inject
@ -231,7 +231,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
bundle.putLong(KEY_INTERNAL_USER_ID, signatureVerification.user!!.id!!) bundle.putLong(KEY_INTERNAL_USER_ID, signatureVerification.user!!.id!!)
bundle.putBoolean(KEY_FROM_NOTIFICATION_START_CALL, true) 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.putBoolean(KEY_ROOM_ONE_TO_ONE, isOneToOneCall) // ggf change in Activity? not necessary????
bundle.putString(BundleKeys.KEY_CONVERSATION_NAME, conversation.name) bundle.putString(BundleKeys.KEY_CONVERSATION_NAME, conversation.name)
@ -300,7 +300,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
checkIfCallIsActive(signatureVerification, conversation) checkIfCallIsActive(signatureVerification, conversation)
} }
chatRepository?.getRoom(userBeingCalled, roomToken = pushMessage.id!!) chatNetworkDataSource?.getRoom(userBeingCalled, roomToken = pushMessage.id!!)
?.subscribeOn(Schedulers.io()) ?.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread()) ?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(object : Observer<ConversationModel> { ?.subscribe(object : Observer<ConversationModel> {

View File

@ -7,17 +7,23 @@
*/ */
package com.nextcloud.talk.models.domain 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.Conversation
import com.nextcloud.talk.models.json.conversations.ConversationEnums
import com.nextcloud.talk.models.json.participants.Participant
class ConversationModel( class ConversationModel(
var internalId: String,
var roomId: String? = null, var roomId: String? = null,
var token: String? = null, var token: String? = null,
var name: String? = null, var name: String? = null,
var displayName: String? = null, var displayName: String? = null,
var description: String? = null, var description: String? = null,
var type: ConversationType? = null, var type: ConversationEnums.ConversationType? = null,
var lastPing: Long = 0, var lastPing: Long = 0,
var participantType: ParticipantType? = null, var participantType: Participant.ParticipantType? = null,
var hasPassword: Boolean = false, var hasPassword: Boolean = false,
var sessionId: String? = null, var sessionId: String? = null,
var actorId: String? = null, var actorId: String? = null,
@ -27,11 +33,12 @@ class ConversationModel(
var lastActivity: Long = 0, var lastActivity: Long = 0,
var unreadMessages: Int = 0, var unreadMessages: Int = 0,
var unreadMention: Boolean = false, var unreadMention: Boolean = false,
// var lastMessage: .....? = null, // var lastMessageViaConversationList: LastMessageJson? = null,
var objectType: ObjectType? = null, var lastMessageViaConversationList: ChatMessageJson? = null,
var notificationLevel: NotificationLevel? = null, var objectType: ConversationEnums.ObjectType? = null,
var conversationReadOnlyState: ConversationReadOnlyState? = null, var notificationLevel: ConversationEnums.NotificationLevel? = null,
var lobbyState: LobbyState? = null, var conversationReadOnlyState: ConversationEnums.ConversationReadOnlyState? = null,
var lobbyState: ConversationEnums.LobbyState? = null,
var lobbyTimer: Long? = null, var lobbyTimer: Long? = null,
var lastReadMessage: Int = 0, var lastReadMessage: Int = 0,
var hasCall: Boolean = false, var hasCall: Boolean = false,
@ -53,20 +60,23 @@ class ConversationModel(
var callStartTime: Long? = null, var callStartTime: Long? = null,
var recordingConsentRequired: Int = 0, var recordingConsentRequired: Int = 0,
var remoteServer: String? = null, 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 { companion object {
fun mapToConversationModel(conversation: Conversation): ConversationModel { fun mapToConversationModel(conversation: Conversation, user: User): ConversationModel {
return ConversationModel( return ConversationModel(
internalId = user.id!!.toString() + "@" + conversation.token,
roomId = conversation.roomId, roomId = conversation.roomId,
token = conversation.token, token = conversation.token,
name = conversation.name, name = conversation.name,
displayName = conversation.displayName, displayName = conversation.displayName,
description = conversation.description, description = conversation.description,
type = conversation.type?.let { ConversationType.valueOf(it.name) }, type = conversation.type?.let { ConversationEnums.ConversationType.valueOf(it.name) },
lastPing = conversation.lastPing, lastPing = conversation.lastPing,
participantType = conversation.participantType?.let { ParticipantType.valueOf(it.name) }, participantType = conversation.participantType?.let { Participant.ParticipantType.valueOf(it.name) },
hasPassword = conversation.hasPassword, hasPassword = conversation.hasPassword,
sessionId = conversation.sessionId, sessionId = conversation.sessionId,
actorId = conversation.actorId, actorId = conversation.actorId,
@ -77,18 +87,18 @@ class ConversationModel(
unreadMessages = conversation.unreadMessages, unreadMessages = conversation.unreadMessages,
unreadMention = conversation.unreadMention, unreadMention = conversation.unreadMention,
// lastMessage = conversation.lastMessage, to do... // 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 = conversation.notificationLevel?.let {
NotificationLevel.valueOf( ConversationEnums.NotificationLevel.valueOf(
it.name it.name
) )
}, },
conversationReadOnlyState = conversation.conversationReadOnlyState?.let { conversationReadOnlyState = conversation.conversationReadOnlyState?.let {
ConversationReadOnlyState.valueOf( ConversationEnums.ConversationReadOnlyState.valueOf(
it.name it.name
) )
}, },
lobbyState = conversation.lobbyState?.let { LobbyState.valueOf(it.name) }, lobbyState = conversation.lobbyState?.let { ConversationEnums.LobbyState.valueOf(it.name) },
lobbyTimer = conversation.lobbyTimer, lobbyTimer = conversation.lobbyTimer,
lastReadMessage = conversation.lastReadMessage, lastReadMessage = conversation.lastReadMessage,
hasCall = conversation.hasCall, hasCall = conversation.hasCall,
@ -116,46 +126,46 @@ class ConversationModel(
} }
} }
enum class ConversationType { // enum class ConversationType {
DUMMY, // DUMMY,
ROOM_TYPE_ONE_TO_ONE_CALL, // ROOM_TYPE_ONE_TO_ONE_CALL,
ROOM_GROUP_CALL, // ROOM_GROUP_CALL,
ROOM_PUBLIC_CALL, // ROOM_PUBLIC_CALL,
ROOM_SYSTEM, // ROOM_SYSTEM,
FORMER_ONE_TO_ONE, // FORMER_ONE_TO_ONE,
NOTE_TO_SELF // NOTE_TO_SELF
} // }
//
enum class ParticipantType { // enum class ParticipantType {
DUMMY, // DUMMY,
OWNER, // OWNER,
MODERATOR, // MODERATOR,
USER, // USER,
GUEST, // GUEST,
USER_FOLLOWING_LINK, // USER_FOLLOWING_LINK,
GUEST_MODERATOR // GUEST_MODERATOR
} // }
//
enum class ObjectType { // enum class ObjectType {
DEFAULT, // DEFAULT,
SHARE_PASSWORD, // SHARE_PASSWORD,
FILE, // FILE,
ROOM // ROOM
} // }
//
enum class NotificationLevel { // enum class NotificationLevel {
DEFAULT, // DEFAULT,
ALWAYS, // ALWAYS,
MENTION, // MENTION,
NEVER // NEVER
} // }
//
enum class ConversationReadOnlyState { // enum class ConversationReadOnlyState {
CONVERSATION_READ_WRITE, // CONVERSATION_READ_WRITE,
CONVERSATION_READ_ONLY // CONVERSATION_READ_ONLY
} // }
//
enum class LobbyState { // enum class LobbyState {
LOBBY_STATE_ALL_PARTICIPANTS, // LOBBY_STATE_ALL_PARTICIPANTS,
LOBBY_STATE_MODERATORS_ONLY // LOBBY_STATE_MODERATORS_ONLY
} // }

View File

@ -6,7 +6,7 @@
*/ */
package com.nextcloud.talk.models.domain 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( data class ReactionAddedModel(
var chatMessage: ChatMessage, var chatMessage: ChatMessage,

View File

@ -6,7 +6,7 @@
*/ */
package com.nextcloud.talk.models.domain 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( data class ReactionDeletedModel(
var chatMessage: ChatMessage, var chatMessage: ChatMessage,

View File

@ -9,25 +9,25 @@
package com.nextcloud.talk.models.domain.converters package com.nextcloud.talk.models.domain.converters
import com.bluelinelabs.logansquare.typeconverters.IntBasedTypeConverter 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>() { class DomainEnumNotificationLevelConverter : IntBasedTypeConverter<ConversationEnums.NotificationLevel>() {
override fun getFromInt(i: Int): NotificationLevel { override fun getFromInt(i: Int): ConversationEnums.NotificationLevel {
return when (i) { return when (i) {
DEFAULT -> NotificationLevel.DEFAULT DEFAULT -> ConversationEnums.NotificationLevel.DEFAULT
ALWAYS -> NotificationLevel.ALWAYS ALWAYS -> ConversationEnums.NotificationLevel.ALWAYS
MENTION -> NotificationLevel.MENTION MENTION -> ConversationEnums.NotificationLevel.MENTION
NEVER -> NotificationLevel.NEVER NEVER -> ConversationEnums.NotificationLevel.NEVER
else -> NotificationLevel.DEFAULT else -> ConversationEnums.NotificationLevel.DEFAULT
} }
} }
override fun convertToInt(`object`: NotificationLevel): Int { override fun convertToInt(`object`: ConversationEnums.NotificationLevel): Int {
return when (`object`) { return when (`object`) {
NotificationLevel.DEFAULT -> DEFAULT ConversationEnums.NotificationLevel.DEFAULT -> DEFAULT
NotificationLevel.ALWAYS -> ALWAYS ConversationEnums.NotificationLevel.ALWAYS -> ALWAYS
NotificationLevel.MENTION -> MENTION ConversationEnums.NotificationLevel.MENTION -> MENTION
NotificationLevel.NEVER -> NEVER ConversationEnums.NotificationLevel.NEVER -> NEVER
else -> DEFAULT else -> DEFAULT
} }
} }

View File

@ -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

View File

@ -19,7 +19,7 @@ data class ChatOCS(
@JsonField(name = ["meta"]) @JsonField(name = ["meta"])
var meta: GenericMeta?, var meta: GenericMeta?,
@JsonField(name = ["data"]) @JsonField(name = ["data"])
var data: List<ChatMessage>? = null var data: List<ChatMessageJson>? = null
) : Parcelable { ) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null, null) constructor() : this(null, null)

View File

@ -19,7 +19,7 @@ data class ChatOCSSingleMessage(
@JsonField(name = ["meta"]) @JsonField(name = ["meta"])
var meta: GenericMeta?, var meta: GenericMeta?,
@JsonField(name = ["data"]) @JsonField(name = ["data"])
var data: ChatMessage? = null var data: ChatMessageJson? = null
) : Parcelable { ) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null, null) constructor() : this(null, null)

View File

@ -10,14 +10,13 @@ package com.nextcloud.talk.models.json.chat
import android.os.Parcelable import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject import com.bluelinelabs.logansquare.annotation.JsonObject
import java.util.HashMap
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
@JsonObject @JsonObject
data class ChatShareOCS( data class ChatShareOCS(
@JsonField(name = ["data"]) @JsonField(name = ["data"])
var data: HashMap<String, ChatMessage>? = null var data: HashMap<String, ChatMessageJson>? = null
) : Parcelable { ) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null) constructor() : this(null)

View File

@ -33,7 +33,7 @@ class ChatUtils {
resultMessage?.replace("{$key}", "@" + individualHashMap["name"]) resultMessage?.replace("{$key}", "@" + individualHashMap["name"])
} else if (type == "geo-location") { } else if (type == "geo-location") {
individualHashMap["name"] individualHashMap["name"]
} else if (individualHashMap?.containsKey("link") == true) { } else if (individualHashMap.containsKey("link") == true) {
if (type == "file") { if (type == "file") {
resultMessage?.replace("{$key}", individualHashMap["name"].toString()) resultMessage?.replace("{$key}", individualHashMap["name"].toString())
} else { } else {

View File

@ -12,9 +12,10 @@ package com.nextcloud.talk.models.json.conversations
import android.os.Parcelable import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject 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.data.user.model.User
import com.nextcloud.talk.models.domain.ConversationModel 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.ConversationObjectTypeConverter
import com.nextcloud.talk.models.json.converters.EnumLobbyStateConverter import com.nextcloud.talk.models.json.converters.EnumLobbyStateConverter
import com.nextcloud.talk.models.json.converters.EnumNotificationLevelConverter import com.nextcloud.talk.models.json.converters.EnumNotificationLevelConverter
@ -39,7 +40,7 @@ data class Conversation(
@JsonField(name = ["description"]) @JsonField(name = ["description"])
var description: String? = null, var description: String? = null,
@JsonField(name = ["type"], typeConverter = EnumRoomTypeConverter::class) @JsonField(name = ["type"], typeConverter = EnumRoomTypeConverter::class)
var type: ConversationType? = null, var type: ConversationEnums.ConversationType? = null,
@JsonField(name = ["lastPing"]) @JsonField(name = ["lastPing"])
var lastPing: Long = 0, var lastPing: Long = 0,
@JsonField(name = ["participantType"], typeConverter = EnumParticipantTypeConverter::class) @JsonField(name = ["participantType"], typeConverter = EnumParticipantTypeConverter::class)
@ -67,20 +68,21 @@ data class Conversation(
@JsonField(name = ["unreadMention"]) @JsonField(name = ["unreadMention"])
var unreadMention: Boolean = false, var unreadMention: Boolean = false,
// TODO get this from Json -> map to ChatMessage and fix error
@JsonField(name = ["lastMessage"]) @JsonField(name = ["lastMessage"])
var lastMessage: ChatMessage? = null, var lastMessage: ChatMessageJson? = null,
@JsonField(name = ["objectType"], typeConverter = ConversationObjectTypeConverter::class) @JsonField(name = ["objectType"], typeConverter = ConversationObjectTypeConverter::class)
var objectType: ObjectType? = null, var objectType: ConversationEnums.ObjectType? = null,
@JsonField(name = ["notificationLevel"], typeConverter = EnumNotificationLevelConverter::class) @JsonField(name = ["notificationLevel"], typeConverter = EnumNotificationLevelConverter::class)
var notificationLevel: NotificationLevel? = null, var notificationLevel: ConversationEnums.NotificationLevel? = null,
@JsonField(name = ["readOnly"], typeConverter = EnumReadOnlyConversationConverter::class) @JsonField(name = ["readOnly"], typeConverter = EnumReadOnlyConversationConverter::class)
var conversationReadOnlyState: ConversationReadOnlyState? = null, var conversationReadOnlyState: ConversationEnums.ConversationReadOnlyState? = null,
@JsonField(name = ["lobbyState"], typeConverter = EnumLobbyStateConverter::class) @JsonField(name = ["lobbyState"], typeConverter = EnumLobbyStateConverter::class)
var lobbyState: LobbyState? = null, var lobbyState: ConversationEnums.LobbyState? = null,
@JsonField(name = ["lobbyTimer"]) @JsonField(name = ["lobbyTimer"])
var lobbyTimer: Long? = null, var lobbyTimer: Long? = null,
@ -149,15 +151,15 @@ data class Conversation(
var remoteServer: String? = null, var remoteServer: String? = null,
@JsonField(name = ["remoteToken"]) @JsonField(name = ["remoteToken"])
var remoteToken: String? = null var remoteToken: String? = null,
) : Parcelable { override var id: Long = 0,
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' override var markedForDeletion: Boolean = false
constructor() : this(null, null)
) : Parcelable, SyncableModel {
@Deprecated("Use ConversationUtil") @Deprecated("Use ConversationUtil")
val isPublic: Boolean val isPublic: Boolean
get() = ConversationType.ROOM_PUBLIC_CALL == type get() = ConversationEnums.ConversationType.ROOM_PUBLIC_CALL == type
@Deprecated("Use ConversationUtil") @Deprecated("Use ConversationUtil")
val isGuest: Boolean val isGuest: Boolean
@ -175,22 +177,27 @@ data class Conversation(
fun canModerate(conversationUser: User): Boolean { fun canModerate(conversationUser: User): Boolean {
return isParticipantOwnerOrModerator && return isParticipantOwnerOrModerator &&
!ConversationUtils.isLockedOneToOne( !ConversationUtils.isLockedOneToOne(
ConversationModel.mapToConversationModel(this), ConversationModel.mapToConversationModel(this, conversationUser),
conversationUser.capabilities?.spreedCapability!! conversationUser.capabilities?.spreedCapability!!
) && ) &&
type != ConversationType.FORMER_ONE_TO_ONE && type != ConversationEnums.ConversationType.FORMER_ONE_TO_ONE &&
!ConversationUtils.isNoteToSelfConversation(ConversationModel.mapToConversationModel(this)) !ConversationUtils.isNoteToSelfConversation(
ConversationModel.mapToConversationModel(this, conversationUser)
)
} }
@Deprecated("Use ConversationUtil") @Deprecated("Use ConversationUtil")
fun isLobbyViewApplicable(conversationUser: User): Boolean { fun isLobbyViewApplicable(conversationUser: User): Boolean {
return !canModerate(conversationUser) && 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") @Deprecated("Use ConversationUtil")
fun isNameEditable(conversationUser: User): Boolean { 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") @Deprecated("Use ConversationUtil")
@ -216,41 +223,6 @@ data class Conversation(
@Deprecated("Use ConversationUtil") @Deprecated("Use ConversationUtil")
fun isNoteToSelfConversation(): Boolean { fun isNoteToSelfConversation(): Boolean {
return type == ConversationType.NOTE_TO_SELF return type == ConversationEnums.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
} }
} }

View File

@ -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
}
}

View File

@ -7,27 +7,27 @@
package com.nextcloud.talk.models.json.converters package com.nextcloud.talk.models.json.converters
import com.bluelinelabs.logansquare.typeconverters.StringBasedTypeConverter 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>() { class ConversationObjectTypeConverter : StringBasedTypeConverter<ConversationEnums.ObjectType>() {
override fun getFromString(string: String?): Conversation.ObjectType { override fun getFromString(string: String?): ConversationEnums.ObjectType {
return when (string) { return when (string) {
"share:password" -> Conversation.ObjectType.SHARE_PASSWORD "share:password" -> ConversationEnums.ObjectType.SHARE_PASSWORD
"room" -> Conversation.ObjectType.ROOM "room" -> ConversationEnums.ObjectType.ROOM
"file" -> Conversation.ObjectType.FILE "file" -> ConversationEnums.ObjectType.FILE
else -> Conversation.ObjectType.DEFAULT else -> ConversationEnums.ObjectType.DEFAULT
} }
} }
override fun convertToString(`object`: Conversation.ObjectType?): String { override fun convertToString(`object`: ConversationEnums.ObjectType?): String {
if (`object` == null) { if (`object` == null) {
return "" return ""
} }
return when (`object`) { return when (`object`) {
Conversation.ObjectType.SHARE_PASSWORD -> "share:password" ConversationEnums.ObjectType.SHARE_PASSWORD -> "share:password"
Conversation.ObjectType.ROOM -> "room" ConversationEnums.ObjectType.ROOM -> "room"
Conversation.ObjectType.FILE -> "file" ConversationEnums.ObjectType.FILE -> "file"
else -> "" else -> ""
} }
} }

View File

@ -8,22 +8,23 @@ package com.nextcloud.talk.models.json.converters;
import com.bluelinelabs.logansquare.typeconverters.IntBasedTypeConverter; import com.bluelinelabs.logansquare.typeconverters.IntBasedTypeConverter;
import com.nextcloud.talk.models.json.conversations.Conversation; 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 @Override
public Conversation.LobbyState getFromInt(int i) { public ConversationEnums.LobbyState getFromInt(int i) {
switch (i) { switch (i) {
case 0: case 0:
return Conversation.LobbyState.LOBBY_STATE_ALL_PARTICIPANTS; return ConversationEnums.LobbyState.LOBBY_STATE_ALL_PARTICIPANTS;
case 1: case 1:
return Conversation.LobbyState.LOBBY_STATE_MODERATORS_ONLY; return ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY;
default: default:
return Conversation.LobbyState.LOBBY_STATE_ALL_PARTICIPANTS; return ConversationEnums.LobbyState.LOBBY_STATE_ALL_PARTICIPANTS;
} }
} }
@Override @Override
public int convertToInt(Conversation.LobbyState object) { public int convertToInt(ConversationEnums.LobbyState object) {
switch (object) { switch (object) {
case LOBBY_STATE_ALL_PARTICIPANTS: case LOBBY_STATE_ALL_PARTICIPANTS:
return 0; return 0;

View File

@ -8,26 +8,27 @@ package com.nextcloud.talk.models.json.converters;
import com.bluelinelabs.logansquare.typeconverters.IntBasedTypeConverter; import com.bluelinelabs.logansquare.typeconverters.IntBasedTypeConverter;
import com.nextcloud.talk.models.json.conversations.Conversation; 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 @Override
public Conversation.NotificationLevel getFromInt(int i) { public ConversationEnums.NotificationLevel getFromInt(int i) {
switch (i) { switch (i) {
case 0: case 0:
return Conversation.NotificationLevel.DEFAULT; return ConversationEnums.NotificationLevel.DEFAULT;
case 1: case 1:
return Conversation.NotificationLevel.ALWAYS; return ConversationEnums.NotificationLevel.ALWAYS;
case 2: case 2:
return Conversation.NotificationLevel.MENTION; return ConversationEnums.NotificationLevel.MENTION;
case 3: case 3:
return Conversation.NotificationLevel.NEVER; return ConversationEnums.NotificationLevel.NEVER;
default: default:
return Conversation.NotificationLevel.DEFAULT; return ConversationEnums.NotificationLevel.DEFAULT;
} }
} }
@Override @Override
public int convertToInt(Conversation.NotificationLevel object) { public int convertToInt(ConversationEnums.NotificationLevel object) {
switch (object) { switch (object) {
case DEFAULT: case DEFAULT:
return 0; return 0;

View File

@ -8,22 +8,23 @@ package com.nextcloud.talk.models.json.converters;
import com.bluelinelabs.logansquare.typeconverters.IntBasedTypeConverter; import com.bluelinelabs.logansquare.typeconverters.IntBasedTypeConverter;
import com.nextcloud.talk.models.json.conversations.Conversation; 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 @Override
public Conversation.ConversationReadOnlyState getFromInt(int i) { public ConversationEnums.ConversationReadOnlyState getFromInt(int i) {
switch (i) { switch (i) {
case 0: case 0:
return Conversation.ConversationReadOnlyState.CONVERSATION_READ_WRITE; return ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_WRITE;
case 1: case 1:
return Conversation.ConversationReadOnlyState.CONVERSATION_READ_ONLY; return ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_ONLY;
default: default:
return Conversation.ConversationReadOnlyState.CONVERSATION_READ_WRITE; return ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_WRITE;
} }
} }
@Override @Override
public int convertToInt(Conversation.ConversationReadOnlyState object) { public int convertToInt(ConversationEnums.ConversationReadOnlyState object) {
switch (object) { switch (object) {
case CONVERSATION_READ_WRITE: case CONVERSATION_READ_WRITE:
return 0; return 0;

View File

@ -7,31 +7,31 @@
package com.nextcloud.talk.models.json.converters; package com.nextcloud.talk.models.json.converters;
import com.bluelinelabs.logansquare.typeconverters.IntBasedTypeConverter; 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 @Override
public Conversation.ConversationType getFromInt(int i) { public ConversationEnums.ConversationType getFromInt(int i) {
switch (i) { switch (i) {
case 1: case 1:
return Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL; return ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL;
case 2: case 2:
return Conversation.ConversationType.ROOM_GROUP_CALL; return ConversationEnums.ConversationType.ROOM_GROUP_CALL;
case 3: case 3:
return Conversation.ConversationType.ROOM_PUBLIC_CALL; return ConversationEnums.ConversationType.ROOM_PUBLIC_CALL;
case 4: case 4:
return Conversation.ConversationType.ROOM_SYSTEM; return ConversationEnums.ConversationType.ROOM_SYSTEM;
case 5: case 5:
return Conversation.ConversationType.FORMER_ONE_TO_ONE; return ConversationEnums.ConversationType.FORMER_ONE_TO_ONE;
case 6: case 6:
return Conversation.ConversationType.NOTE_TO_SELF; return ConversationEnums.ConversationType.NOTE_TO_SELF;
default: default:
return Conversation.ConversationType.DUMMY; return ConversationEnums.ConversationType.DUMMY;
} }
} }
@Override @Override
public int convertToInt(Conversation.ConversationType object) { public int convertToInt(ConversationEnums.ConversationType object) {
switch (object) { switch (object) {
case DUMMY: case DUMMY:
return 0; return 0;

View File

@ -9,66 +9,66 @@
package com.nextcloud.talk.models.json.converters package com.nextcloud.talk.models.json.converters
import com.bluelinelabs.logansquare.typeconverters.StringBasedTypeConverter import com.bluelinelabs.logansquare.typeconverters.StringBasedTypeConverter
import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.AUDIO_RECORDING_STARTED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.AUDIO_RECORDING_STARTED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.AUDIO_RECORDING_STOPPED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.AUDIO_RECORDING_STOPPED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.AVATAR_REMOVED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.AVATAR_REMOVED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.AVATAR_SET import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.AVATAR_SET
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.BREAKOUT_ROOMS_STARTED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.BREAKOUT_ROOMS_STARTED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.BREAKOUT_ROOMS_STOPPED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.BREAKOUT_ROOMS_STOPPED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CALL_ENDED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CALL_ENDED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CALL_JOINED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CALL_JOINED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CALL_LEFT import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CALL_LEFT
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CALL_MISSED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CALL_MISSED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CALL_STARTED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CALL_STARTED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CALL_TRIED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CALL_TRIED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CIRCLE_ADDED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CIRCLE_ADDED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CIRCLE_REMOVED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CIRCLE_REMOVED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CLEARED_CHAT import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CLEARED_CHAT
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CONVERSATION_CREATED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CONVERSATION_CREATED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CONVERSATION_RENAMED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CONVERSATION_RENAMED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.DESCRIPTION_REMOVED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.DESCRIPTION_REMOVED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.DESCRIPTION_SET import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.DESCRIPTION_SET
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.DUMMY import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.DUMMY
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.FILE_SHARED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.FILE_SHARED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.GROUP_ADDED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.GROUP_ADDED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.GROUP_REMOVED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.GROUP_REMOVED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.GUESTS_ALLOWED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.GUESTS_ALLOWED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.GUESTS_DISALLOWED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.GUESTS_DISALLOWED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.GUEST_MODERATOR_DEMOTED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.GUEST_MODERATOR_DEMOTED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.GUEST_MODERATOR_PROMOTED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.GUEST_MODERATOR_PROMOTED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.LISTABLE_ALL import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.LISTABLE_ALL
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.LISTABLE_NONE import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.LISTABLE_NONE
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.LISTABLE_USERS import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.LISTABLE_USERS
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.LOBBY_NONE import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.LOBBY_NONE
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.LOBBY_NON_MODERATORS import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.LOBBY_NON_MODERATORS
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.LOBBY_OPEN_TO_EVERYONE import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.LOBBY_OPEN_TO_EVERYONE
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.MATTERBRIDGE_CONFIG_ADDED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MATTERBRIDGE_CONFIG_ADDED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.MATTERBRIDGE_CONFIG_DISABLED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MATTERBRIDGE_CONFIG_DISABLED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.MATTERBRIDGE_CONFIG_EDITED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MATTERBRIDGE_CONFIG_EDITED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.MATTERBRIDGE_CONFIG_ENABLED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MATTERBRIDGE_CONFIG_ENABLED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.MATTERBRIDGE_CONFIG_REMOVED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MATTERBRIDGE_CONFIG_REMOVED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.MESSAGE_DELETED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MESSAGE_DELETED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.MESSAGE_EXPIRATION_DISABLED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MESSAGE_EXPIRATION_DISABLED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.MESSAGE_EXPIRATION_ENABLED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MESSAGE_EXPIRATION_ENABLED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.MODERATOR_DEMOTED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MODERATOR_DEMOTED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.MODERATOR_PROMOTED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MODERATOR_PROMOTED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.OBJECT_SHARED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.OBJECT_SHARED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.PASSWORD_REMOVED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.PASSWORD_REMOVED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.PASSWORD_SET import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.PASSWORD_SET
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.POLL_CLOSED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.POLL_CLOSED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.POLL_VOTED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.POLL_VOTED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.REACTION import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.REACTION
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.REACTION_DELETED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.REACTION_DELETED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.REACTION_REVOKED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.REACTION_REVOKED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.READ_ONLY import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.READ_ONLY
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.READ_ONLY_OFF import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.READ_ONLY_OFF
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.RECORDING_FAILED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.RECORDING_FAILED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.RECORDING_STARTED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.RECORDING_STARTED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.RECORDING_STOPPED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.RECORDING_STOPPED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.USER_ADDED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.USER_ADDED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.USER_REMOVED import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.USER_REMOVED
/* /*
* see https://nextcloud-talk.readthedocs.io/en/latest/chat/#system-messages * see https://nextcloud-talk.readthedocs.io/en/latest/chat/#system-messages

View File

@ -10,7 +10,7 @@ package com.nextcloud.talk.models.json.websocket
import android.os.Parcelable import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject 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 com.nextcloud.talk.models.json.converters.EnumRoomTypeConverter
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ -20,7 +20,7 @@ data class RoomPropertiesWebSocketMessage(
@JsonField(name = ["name"]) @JsonField(name = ["name"])
var name: String? = null, var name: String? = null,
@JsonField(name = ["type"], typeConverter = EnumRoomTypeConverter::class) @JsonField(name = ["type"], typeConverter = EnumRoomTypeConverter::class)
var roomType: ConversationType? = null var roomType: ConversationEnums.ConversationType? = null
) : Parcelable { ) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null, null) constructor() : this(null, null)

View File

@ -18,7 +18,10 @@ import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
import io.reactivex.Observable 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 { ConversationsRepository {
private val user: User private val user: User

Some files were not shown because too many files have changed in this diff Show More