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
Родитель b15c1787c2
Коммит 2408d639e4
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: C793F8B59F43CE7B
119 изменённых файлов: 5186 добавлений и 1369 удалений

Просмотреть файл

@ -39,5 +39,6 @@
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="RedundantSemicolon" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="SerializableCtor" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

Просмотреть файл

@ -93,6 +93,12 @@ android {
buildConfigField "String", "PERMISSION_LOCAL_BROADCAST", "\"${localBroadcastPermission}\""
}
testOptions {
unitTests.all {
useJUnitPlatform()
}
}
buildTypes {
release {
minifyEnabled false
@ -146,7 +152,7 @@ ext {
coilKtVersion = "2.7.0"
daggerVersion = "2.52"
emojiVersion = "1.4.0"
fidoVersion = "4.1.0-patch2"
fidoVersion = "4.4.0"
lifecycleVersion = '2.8.4'
okhttpVersion = "4.12.0"
markwonVersion = "4.6.2"
@ -157,6 +163,7 @@ ext {
roomVersion = "2.6.1"
workVersion = "2.9.1"
espressoVersion = "3.6.1"
androidxTestVersion = "1.5.0"
media3_version = "1.4.0"
coroutines_version = "1.8.1"
mockitoKotlinVersion = "5.4.0"
@ -170,10 +177,14 @@ configurations.configureEach {
}
dependencies {
spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.13.0'
spotbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.6.4'
implementation("androidx.compose.runtime:runtime:1.6.8")
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.datastore:datastore-core:1.1.1'
implementation 'androidx.datastore:datastore-preferences:1.1.1'
implementation 'androidx.test.ext:junit-ktx:1.1.5'
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.6")
implementation fileTree(include: ['*'], dir: 'libs')
@ -192,7 +203,6 @@ dependencies {
implementation "androidx.work:work-runtime:${workVersion}"
implementation "androidx.work:work-rxjava2:${workVersion}"
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
androidTestImplementation "androidx.work:work-testing:${workVersion}"
implementation 'com.google.android.flexbox:flexbox:3.0.0'
implementation ('com.github.bitfireAT:dav4jvm:2.1.3', {
exclude group: 'org.ogce', module: 'xpp3' // Android comes with its own XmlPullParser
@ -289,6 +299,12 @@ dependencies {
})
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.activity:activity-ktx:1.9.0'
implementation 'com.github.nextcloud.android-common:ui:0.21.0'
implementation 'com.github.nextcloud-deps:android-talk-webrtc:121.6167.0'
gplayImplementation 'com.google.android.gms:play-services-base:18.4.0'
gplayImplementation "com.google.firebase:firebase-messaging:23.4.1"
//compose
implementation(platform("androidx.compose:compose-bom:2024.06.00"))
@ -305,11 +321,14 @@ dependencies {
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-core:5.12.0'
androidTestImplementation 'org.mockito:mockito-android:5.12.0'
testImplementation 'androidx.arch.core:core-testing:2.2.0'
androidTestImplementation "androidx.test:core:1.6.1"
androidTestImplementation "androidx.test:core:1.5.0"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1"
androidTestImplementation 'androidx.test:core-ktx:1.6.1'
androidTestImplementation 'org.mockito:mockito-android:5.12.0'
androidTestImplementation "androidx.work:work-testing:${workVersion}"
// Espresso core
androidTestImplementation ("androidx.test.espresso:espresso-core:$espressoVersion", {
exclude group: 'com.android.support', module: 'support-annotations'
@ -317,6 +336,9 @@ dependencies {
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
androidTestImplementation "androidx.test.espresso:espresso-web:$espressoVersion"
androidTestImplementation "androidx.test.espresso:espresso-accessibility:$espressoVersion"
androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion"
androidTestImplementation('com.android.support.test.espresso:espresso-intents:3.0.2')
spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.13.0'
@ -325,7 +347,7 @@ dependencies {
gplayImplementation 'com.google.android.gms:play-services-base:18.5.0'
gplayImplementation "com.google.firebase:firebase-messaging:24.0.0"
implementation 'androidx.activity:activity-ktx:1.9.1'
implementation 'androidx.activity:activity-ktx:1.9.1'
implementation 'com.github.nextcloud.android-common:ui:0.23.0'

Просмотреть файл

@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 10,
"identityHash": "1b2dab0ea495c45c9c9ee6e64ba74039",
"identityHash": "93ef64fac7a9a811c4a3c2f5a6406f87",
"entities": [
{
"tableName": "User",
@ -135,12 +135,539 @@
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "Conversations",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `name` TEXT, `displayName` TEXT, `description` TEXT, `type` TEXT, `lastPing` INTEGER NOT NULL, `participantType` TEXT, `hasPassword` INTEGER NOT NULL, `sessionId` TEXT, `actorId` TEXT, `actorType` TEXT, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, `unreadMention` INTEGER NOT NULL, `lastMessageJson` TEXT, `objectType` TEXT, `notificationLevel` TEXT, `readOnly` TEXT, `lobbyState` TEXT, `lobbyTimer` INTEGER, `lastReadMessage` INTEGER NOT NULL, `hasCall` INTEGER NOT NULL, `callFlag` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `canLeaveConversation` INTEGER, `canDeleteConversation` INTEGER, `unreadMentionDirect` INTEGER, `notificationCalls` INTEGER, `permissions` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `status` TEXT, `statusIcon` TEXT, `statusMessage` TEXT, `statusClearAt` INTEGER, `callRecording` INTEGER NOT NULL, `avatarVersion` TEXT, `isCustomAvatar` INTEGER, `callStartTime` INTEGER, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "internalId",
"columnName": "internalId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastPing",
"columnName": "lastPing",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "participantType",
"columnName": "participantType",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "hasPassword",
"columnName": "hasPassword",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sessionId",
"columnName": "sessionId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "actorId",
"columnName": "actorId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "actorType",
"columnName": "actorType",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "favorite",
"columnName": "isFavorite",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastActivity",
"columnName": "lastActivity",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unreadMessages",
"columnName": "unreadMessages",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unreadMention",
"columnName": "unreadMention",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastMessageJson",
"columnName": "lastMessageJson",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "objectType",
"columnName": "objectType",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "notificationLevel",
"columnName": "notificationLevel",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "conversationReadOnlyState",
"columnName": "readOnly",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lobbyState",
"columnName": "lobbyState",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lobbyTimer",
"columnName": "lobbyTimer",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastReadMessage",
"columnName": "lastReadMessage",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasCall",
"columnName": "hasCall",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "callFlag",
"columnName": "callFlag",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canStartCall",
"columnName": "canStartCall",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canLeaveConversation",
"columnName": "canLeaveConversation",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "canDeleteConversation",
"columnName": "canDeleteConversation",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "unreadMentionDirect",
"columnName": "unreadMentionDirect",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "notificationCalls",
"columnName": "notificationCalls",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "permissions",
"columnName": "permissions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "messageExpiration",
"columnName": "messageExpiration",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "statusIcon",
"columnName": "statusIcon",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "statusMessage",
"columnName": "statusMessage",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "statusClearAt",
"columnName": "statusClearAt",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "callRecording",
"columnName": "callRecording",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "avatarVersion",
"columnName": "avatarVersion",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "hasCustomAvatar",
"columnName": "isCustomAvatar",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "callStartTime",
"columnName": "callStartTime",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "recordingConsentRequired",
"columnName": "recordingConsent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "remoteServer",
"columnName": "remoteServer",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "remoteToken",
"columnName": "remoteToken",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"internalId"
]
},
"indices": [
{
"name": "index_Conversations_accountId",
"unique": false,
"columnNames": [
"accountId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_Conversations_accountId` ON `${TABLE_NAME}` (`accountId`)"
}
],
"foreignKeys": [
{
"table": "User",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"accountId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "ChatMessages",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `id` INTEGER NOT NULL, `internalConversationId` TEXT, `actorType` TEXT, `actorId` TEXT, `actorDisplayName` TEXT, `timestamp` INTEGER NOT NULL, `systemMessage` TEXT, `messageType` TEXT, `isReplyable` INTEGER NOT NULL, `message` TEXT, `messageParameters` TEXT, `expirationTimestamp` INTEGER NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `markdown` INTEGER, `lastEditActorType` TEXT, `lastEditActorId` TEXT, `lastEditActorDisplayName` TEXT, `lastEditTimestamp` INTEGER, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "internalId",
"columnName": "internalId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "internalConversationId",
"columnName": "internalConversationId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "actorType",
"columnName": "actorType",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "actorId",
"columnName": "actorId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "actorDisplayName",
"columnName": "actorDisplayName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "systemMessageType",
"columnName": "systemMessage",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "messageType",
"columnName": "messageType",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "replyable",
"columnName": "isReplyable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "messageParameters",
"columnName": "messageParameters",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "expirationTimestamp",
"columnName": "expirationTimestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "parentMessageId",
"columnName": "parent",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "reactions",
"columnName": "reactions",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reactionsSelf",
"columnName": "reactionsSelf",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "renderMarkdown",
"columnName": "markdown",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastEditActorType",
"columnName": "lastEditActorType",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastEditActorId",
"columnName": "lastEditActorId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastEditActorDisplayName",
"columnName": "lastEditActorDisplayName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastEditTimestamp",
"columnName": "lastEditTimestamp",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"internalId"
]
},
"indices": [
{
"name": "index_ChatMessages_internalId",
"unique": true,
"columnNames": [
"internalId"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ChatMessages_internalId` ON `${TABLE_NAME}` (`internalId`)"
},
{
"name": "index_ChatMessages_internalConversationId",
"unique": false,
"columnNames": [
"internalConversationId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_ChatMessages_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)"
}
],
"foreignKeys": [
{
"table": "Conversations",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"internalConversationId"
],
"referencedColumns": [
"internalId"
]
}
]
},
{
"tableName": "ChatBlocks",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `internalConversationId` TEXT NOT NULL, `oldestMessageId` INTEGER NOT NULL, `newestMessageId` INTEGER NOT NULL, `hasHistory` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "internalConversationId",
"columnName": "internalConversationId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "oldestMessageId",
"columnName": "oldestMessageId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "newestMessageId",
"columnName": "newestMessageId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasHistory",
"columnName": "hasHistory",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1b2dab0ea495c45c9c9ee6e64ba74039')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '93ef64fac7a9a811c4a3c2f5a6406f87')"
]
}
}

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -16,7 +16,7 @@ import coil.target.Target
import coil.transform.CircleCropTransformation
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.databinding.CallStartedMessageBinding
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils

Просмотреть файл

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

Просмотреть файл

@ -10,26 +10,35 @@ package com.nextcloud.talk.adapters.messages
import android.annotation.SuppressLint
import android.content.Context
import android.text.TextUtils
import android.util.Log
import android.view.View
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import autodagger.AutoInjector
import coil.load
import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.talk.R
import com.nextcloud.talk.adapters.messages.OutcomingPollMessageViewHolder.Companion
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.databinding.ItemCustomIncomingLinkPreviewMessageBinding
import com.nextcloud.talk.extensions.loadBotsAvatar
import com.nextcloud.talk.extensions.loadChangelogBotAvatar
import com.nextcloud.talk.extensions.loadFederatedUserAvatar
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DateUtils
import com.nextcloud.talk.utils.message.MessageUtils
import com.nextcloud.talk.utils.preferences.AppPreferences
import com.stfalcon.chatkit.messages.MessageHolders
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
@ -168,40 +177,62 @@ class IncomingLinkPreviewMessageViewHolder(incomingView: View, payload: Any) :
}
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (!message.isDeleted && message.parentMessage != null) {
val parentChatMessage = message.parentMessage
parentChatMessage!!.activeUser = message.activeUser
parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) {
addHeader(
"Authorization",
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
if (message.parentMessageId != null && !message.isDeleted) {
CoroutineScope(Dispatchers.Main).launch {
try {
val chatActivity = commonMessageInterface as ChatActivity
val urlForChatting = ApiUtils.getUrlForChat(
chatActivity.chatApiVersion,
chatActivity.conversationUser?.baseUrl,
chatActivity.roomToken
)
val parentChatMessage = withContext(Dispatchers.IO) {
chatActivity.chatViewModel.getMessageById(
urlForChatting,
chatActivity.currentConversation!!,
message.parentMessageId!!
).first()
}
parentChatMessage.activeUser = message.activeUser
parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) {
addHeader(
"Authorization",
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
)
}
} ?: run {
binding.messageQuote.quotedMessageImage.visibility = View.GONE
}
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
?: context.getText(R.string.nc_nick_guest)
binding.messageQuote.quotedMessage.text = messageUtils
.enrichChatReplyMessageText(
binding.messageQuote.quotedMessage.context,
parentChatMessage,
true,
viewThemeUtils
)
binding.messageQuote.quotedMessageAuthor
.setTextColor(ContextCompat.getColor(context, R.color.textColorMaxContrast))
if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) {
viewThemeUtils.platform.colorViewBackground(
binding.messageQuote.quoteColoredView,
ColorRole.PRIMARY
)
} else {
binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast)
}
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
} ?: run {
binding.messageQuote.quotedMessageImage.visibility = View.GONE
}
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
?: context.getText(R.string.nc_nick_guest)
binding.messageQuote.quotedMessage.text = messageUtils
.enrichChatReplyMessageText(
binding.messageQuote.quotedMessage.context,
parentChatMessage,
true,
viewThemeUtils
)
binding.messageQuote.quotedMessageAuthor
.setTextColor(ContextCompat.getColor(context, R.color.textColorMaxContrast))
if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) {
viewThemeUtils.platform.colorViewBackground(binding.messageQuote.quoteColoredView, ColorRole.PRIMARY)
} else {
binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast)
}
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} else {
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
}

Просмотреть файл

@ -20,18 +20,21 @@ import android.view.MotionEvent
import android.view.View
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.lifecycle.lifecycleScope
import autodagger.AutoInjector
import coil.load
import com.google.android.material.snackbar.Snackbar
import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.talk.R
import com.nextcloud.talk.adapters.messages.IncomingPollMessageViewHolder.Companion
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.databinding.ItemCustomIncomingLocationMessageBinding
import com.nextcloud.talk.extensions.loadBotsAvatar
import com.nextcloud.talk.extensions.loadChangelogBotAvatar
import com.nextcloud.talk.extensions.loadFederatedUserAvatar
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DateUtils
@ -39,6 +42,11 @@ import com.nextcloud.talk.utils.UriUtils
import com.nextcloud.talk.utils.message.MessageUtils
import com.nextcloud.talk.utils.preferences.AppPreferences
import com.stfalcon.chatkit.messages.MessageHolders
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.URLEncoder
import javax.inject.Inject
@ -150,40 +158,62 @@ class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) :
}
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (!message.isDeleted && message.parentMessage != null) {
val parentChatMessage = message.parentMessage
parentChatMessage!!.activeUser = message.activeUser
parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) {
addHeader(
"Authorization",
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
if (message.parentMessageId != null && !message.isDeleted) {
CoroutineScope(Dispatchers.Main).launch {
try {
val chatActivity = commonMessageInterface as ChatActivity
val urlForChatting = ApiUtils.getUrlForChat(
chatActivity.chatApiVersion,
chatActivity.conversationUser?.baseUrl,
chatActivity.roomToken
)
val parentChatMessage = withContext(Dispatchers.IO) {
chatActivity.chatViewModel.getMessageById(
urlForChatting,
chatActivity.currentConversation!!,
message.parentMessageId!!
).first()
}
parentChatMessage.activeUser = message.activeUser
parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) {
addHeader(
"Authorization",
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
)
}
} ?: run {
binding.messageQuote.quotedMessageImage.visibility = View.GONE
}
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
?: context.getText(R.string.nc_nick_guest)
binding.messageQuote.quotedMessage.text = messageUtils
.enrichChatReplyMessageText(
binding.messageQuote.quotedMessage.context,
parentChatMessage,
true,
viewThemeUtils
)
binding.messageQuote.quotedMessageAuthor
.setTextColor(context.resources.getColor(R.color.textColorMaxContrast, null))
if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) {
viewThemeUtils.platform.colorViewBackground(
binding.messageQuote.quoteColoredView,
ColorRole.PRIMARY
)
} else {
binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast)
}
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
} ?: run {
binding.messageQuote.quotedMessageImage.visibility = View.GONE
}
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
?: context.getText(R.string.nc_nick_guest)
binding.messageQuote.quotedMessage.text = messageUtils
.enrichChatReplyMessageText(
binding.messageQuote.quotedMessage.context,
parentChatMessage,
true,
viewThemeUtils
)
binding.messageQuote.quotedMessageAuthor
.setTextColor(context.resources.getColor(R.color.textColorMaxContrast, null))
if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) {
viewThemeUtils.platform.colorViewBackground(binding.messageQuote.quoteColoredView, ColorRole.PRIMARY)
} else {
binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast)
}
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} else {
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
}

Просмотреть файл

@ -9,12 +9,15 @@ package com.nextcloud.talk.adapters.messages
import android.annotation.SuppressLint
import android.content.Context
import android.text.TextUtils
import android.util.Log
import android.view.View
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import autodagger.AutoInjector
import coil.load
import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.talk.R
import com.nextcloud.talk.adapters.messages.IncomingTextMessageViewHolder.Companion
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
@ -23,7 +26,7 @@ import com.nextcloud.talk.databinding.ItemCustomIncomingPollMessageBinding
import com.nextcloud.talk.extensions.loadBotsAvatar
import com.nextcloud.talk.extensions.loadChangelogBotAvatar
import com.nextcloud.talk.extensions.loadFederatedUserAvatar
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.polls.ui.PollMainDialogFragment
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.ApiUtils
@ -31,6 +34,11 @@ import com.nextcloud.talk.utils.DateUtils
import com.nextcloud.talk.utils.message.MessageUtils
import com.nextcloud.talk.utils.preferences.AppPreferences
import com.stfalcon.chatkit.messages.MessageHolders
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
@ -176,40 +184,61 @@ class IncomingPollMessageViewHolder(incomingView: View, payload: Any) :
}
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (!message.isDeleted && message.parentMessage != null) {
val parentChatMessage = message.parentMessage
parentChatMessage!!.activeUser = message.activeUser
parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) {
addHeader(
"Authorization",
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
if (message.parentMessageId != null && !message.isDeleted) {
CoroutineScope(Dispatchers.Main).launch {
try {
val chatActivity = commonMessageInterface as ChatActivity
val urlForChatting = ApiUtils.getUrlForChat(
chatActivity.chatApiVersion,
chatActivity.conversationUser?.baseUrl,
chatActivity.roomToken
)
val parentChatMessage = withContext(Dispatchers.IO) {
chatActivity.chatViewModel.getMessageById(
urlForChatting,
chatActivity.currentConversation!!,
message.parentMessageId!!
).first()
}
parentChatMessage.activeUser = message.activeUser
parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) {
addHeader(
"Authorization",
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
)
}
} ?: run {
binding.messageQuote.quotedMessageImage.visibility = View.GONE
}
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
?: context.getText(R.string.nc_nick_guest)
binding.messageQuote.quotedMessage.text = messageUtils
.enrichChatReplyMessageText(
binding.messageQuote.quotedMessage.context,
parentChatMessage,
true,
viewThemeUtils
)
binding.messageQuote.quotedMessageAuthor
.setTextColor(ContextCompat.getColor(context, R.color.textColorMaxContrast))
if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) {
viewThemeUtils.platform.colorViewBackground(
binding.messageQuote.quoteColoredView,
ColorRole.PRIMARY
)
} else {
binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast)
}
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
} ?: run {
binding.messageQuote.quotedMessageImage.visibility = View.GONE
}
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
?: context.getText(R.string.nc_nick_guest)
binding.messageQuote.quotedMessage.text = messageUtils
.enrichChatReplyMessageText(
binding.messageQuote.quotedMessage.context,
parentChatMessage,
true,
viewThemeUtils
)
binding.messageQuote.quotedMessageAuthor
.setTextColor(ContextCompat.getColor(context, R.color.textColorMaxContrast))
if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) {
viewThemeUtils.platform.colorViewBackground(binding.messageQuote.quoteColoredView, ColorRole.PRIMARY)
} else {
binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast)
}
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} else {
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
}

Просмотреть файл

@ -18,7 +18,7 @@ import com.google.android.material.card.MaterialCardView;
import com.nextcloud.talk.R;
import com.nextcloud.talk.databinding.ItemCustomIncomingPreviewMessageBinding;
import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding;
import com.nextcloud.talk.models.json.chat.ChatMessage;
import com.nextcloud.talk.chat.data.model.ChatMessage;
import com.nextcloud.talk.utils.TextMatchers;
import java.util.HashMap;

Просмотреть файл

@ -11,9 +11,11 @@ package com.nextcloud.talk.adapters.messages
import android.content.Context
import android.text.TextUtils
import android.util.Log
import android.util.TypedValue
import android.view.View
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import autodagger.AutoInjector
import coil.load
import com.nextcloud.android.common.ui.theme.utils.ColorRole
@ -25,7 +27,7 @@ import com.nextcloud.talk.databinding.ItemCustomIncomingTextMessageBinding
import com.nextcloud.talk.extensions.loadBotsAvatar
import com.nextcloud.talk.extensions.loadChangelogBotAvatar
import com.nextcloud.talk.extensions.loadFederatedUserAvatar
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DateUtils
@ -33,6 +35,13 @@ import com.nextcloud.talk.utils.TextMatchers
import com.nextcloud.talk.utils.message.MessageUtils
import com.nextcloud.talk.utils.preferences.AppPreferences
import com.stfalcon.chatkit.messages.MessageHolders
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
@ -99,14 +108,14 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
if (message.lastEditTimestamp != 0L && !message.isDeleted) {
binding.messageEditIndicator.visibility = View.VISIBLE
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.lastEditTimestamp)
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.lastEditTimestamp!!)
} else {
binding.messageEditIndicator.visibility = View.GONE
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
}
// parent message handling
if (!message.isDeleted && message.parentMessage != null) {
if (!message.isDeleted && message.parentMessageId != null) {
processParentMessage(message)
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} else {
@ -176,44 +185,73 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
}
private fun processParentMessage(message: ChatMessage) {
val parentChatMessage = message.parentMessage
parentChatMessage!!.activeUser = message.activeUser
parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) {
addHeader(
"Authorization",
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
)
if (message.parentMessageId != null && !message.isDeleted) {
CoroutineScope(Dispatchers.Main).launch {
try {
val chatActivity = commonMessageInterface as ChatActivity
val urlForChatting = ApiUtils.getUrlForChat(
chatActivity.chatApiVersion,
chatActivity.conversationUser?.baseUrl,
chatActivity.roomToken
)
val parentChatMessage = withContext(Dispatchers.IO) {
chatActivity.chatViewModel.getMessageById(
urlForChatting,
chatActivity.currentConversation!!,
message.parentMessageId!!
).first()
}
parentChatMessage.activeUser = message.activeUser
parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) {
addHeader(
"Authorization",
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
)
}
} ?: run {
binding.messageQuote.quotedMessageImage.visibility = View.GONE
}
binding.messageQuote.quotedMessageAuthor.text =
if (parentChatMessage.actorDisplayName.isNullOrEmpty()) {
context.getText(R.string.nc_nick_guest)
} else {
parentChatMessage.actorDisplayName
}
binding.messageQuote.quotedMessage.text = messageUtils
.enrichChatReplyMessageText(
binding.messageQuote.quotedMessage.context,
parentChatMessage,
true,
viewThemeUtils
)
if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) {
viewThemeUtils.platform.colorViewBackground(
binding.messageQuote.quoteColoredView,
ColorRole.PRIMARY
)
} else {
binding.messageQuote.quoteColoredView.setBackgroundColor(
ContextCompat.getColor(
binding.messageQuote.quoteColoredView.context,
R.color.high_emphasis_text
)
)
}
binding.messageQuote.quotedChatMessageView.setOnClickListener {
val chatActivity = commonMessageInterface as ChatActivity
chatActivity.jumpToQuotedMessage(parentChatMessage)
}
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
}
} ?: run {
binding.messageQuote.quotedMessageImage.visibility = View.GONE
}
binding.messageQuote.quotedMessageAuthor.text = if (parentChatMessage.actorDisplayName.isNullOrEmpty()) {
context.getText(R.string.nc_nick_guest)
} else {
parentChatMessage.actorDisplayName
}
binding.messageQuote.quotedMessage.text = messageUtils
.enrichChatReplyMessageText(
binding.messageQuote.quotedMessage.context,
parentChatMessage,
true,
viewThemeUtils
)
if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) {
viewThemeUtils.platform.colorViewBackground(binding.messageQuote.quoteColoredView, ColorRole.PRIMARY)
} else {
binding.messageQuote.quoteColoredView.setBackgroundColor(
ContextCompat.getColor(binding.messageQuote.quoteColoredView.context, R.color.high_emphasis_text)
)
}
binding.messageQuote.quotedChatMessageView.setOnClickListener {
val chatActivity = commonMessageInterface as ChatActivity
chatActivity.jumpToQuotedMessage(parentChatMessage)
}
}
@ -234,5 +272,6 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
companion object {
const val TEXT_SIZE_MULTIPLIER = 2.5
private val TAG = IncomingTextMessageViewHolder::class.java.simpleName
}
}

Просмотреть файл

@ -24,17 +24,23 @@ import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.databinding.ItemCustomIncomingVoiceMessageBinding
import com.nextcloud.talk.extensions.loadBotsAvatar
import com.nextcloud.talk.extensions.loadChangelogBotAvatar
import com.nextcloud.talk.extensions.loadFederatedUserAvatar
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DateUtils
import com.nextcloud.talk.utils.message.MessageUtils
import com.nextcloud.talk.utils.preferences.AppPreferences
import com.stfalcon.chatkit.messages.MessageHolders
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.ExecutionException
import javax.inject.Inject
@ -203,14 +209,17 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) :
Log.d(TAG, "WorkInfo.State.RUNNING in ViewHolder")
showVoiceMessageLoading()
}
WorkInfo.State.SUCCEEDED -> {
Log.d(TAG, "WorkInfo.State.SUCCEEDED in ViewHolder")
showPlayButton()
}
WorkInfo.State.FAILED -> {
Log.d(TAG, "WorkInfo.State.FAILED in ViewHolder")
showPlayButton()
}
else -> {
}
}
@ -269,40 +278,62 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) :
}
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (!message.isDeleted && message.parentMessage != null) {
val parentChatMessage = message.parentMessage
parentChatMessage!!.activeUser = message.activeUser
parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) {
addHeader(
"Authorization",
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
if (message.parentMessageId != null && !message.isDeleted) {
CoroutineScope(Dispatchers.Main).launch {
try {
val chatActivity = commonMessageInterface as ChatActivity
val urlForChatting = ApiUtils.getUrlForChat(
chatActivity.chatApiVersion,
chatActivity.conversationUser?.baseUrl,
chatActivity.roomToken
)
val parentChatMessage = withContext(Dispatchers.IO) {
chatActivity.chatViewModel.getMessageById(
urlForChatting,
chatActivity.currentConversation!!,
message.parentMessageId!!
).first()
}
parentChatMessage.activeUser = message.activeUser
parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) {
addHeader(
"Authorization",
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
)
}
} ?: run {
binding.messageQuote.quotedMessageImage.visibility = View.GONE
}
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
?: context!!.getText(R.string.nc_nick_guest)
binding.messageQuote.quotedMessage.text = messageUtils
.enrichChatReplyMessageText(
binding.messageQuote.quotedMessage.context,
parentChatMessage,
true,
viewThemeUtils
)
binding.messageQuote.quotedMessageAuthor
.setTextColor(ContextCompat.getColor(context!!, R.color.textColorMaxContrast))
if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) {
viewThemeUtils.platform.colorViewBackground(
binding.messageQuote.quoteColoredView,
ColorRole.PRIMARY
)
} else {
binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast)
}
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
} ?: run {
binding.messageQuote.quotedMessageImage.visibility = View.GONE
}
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
?: context!!.getText(R.string.nc_nick_guest)
binding.messageQuote.quotedMessage.text = messageUtils
.enrichChatReplyMessageText(
binding.messageQuote.quotedMessage.context,
parentChatMessage,
true,
viewThemeUtils
)
binding.messageQuote.quotedMessageAuthor
.setTextColor(ContextCompat.getColor(context!!, R.color.textColorMaxContrast))
if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) {
viewThemeUtils.platform.colorViewBackground(binding.messageQuote.quoteColoredView, ColorRole.PRIMARY)
} else {
binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast)
}
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} else {
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
}

Просмотреть файл

@ -14,7 +14,7 @@ import android.view.View
import coil.load
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.databinding.ReferenceInsideMessageBinding
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.models.json.opengraph.OpenGraphOverall
import com.nextcloud.talk.utils.ApiUtils
import io.reactivex.Observer

Просмотреть файл

@ -9,17 +9,21 @@ package com.nextcloud.talk.adapters.messages
import android.annotation.SuppressLint
import android.content.Context
import android.util.Log
import android.view.View
import androidx.appcompat.content.res.AppCompatResources
import androidx.lifecycle.lifecycleScope
import autodagger.AutoInjector
import coil.load
import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.talk.R
import com.nextcloud.talk.adapters.messages.OutcomingPollMessageViewHolder.Companion
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.databinding.ItemCustomOutcomingLinkPreviewMessageBinding
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.models.json.chat.ReadStatus
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.ApiUtils
@ -27,6 +31,11 @@ import com.nextcloud.talk.utils.DateUtils
import com.nextcloud.talk.utils.message.MessageUtils
import com.nextcloud.talk.utils.preferences.AppPreferences
import com.stfalcon.chatkit.messages.MessageHolders
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
@ -138,34 +147,53 @@ class OutcomingLinkPreviewMessageViewHolder(outcomingView: View, payload: Any) :
}
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (!message.isDeleted && message.parentMessage != null) {
val parentChatMessage = message.parentMessage
parentChatMessage!!.activeUser = message.activeUser
parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) {
addHeader(
"Authorization",
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
if (message.parentMessageId != null && !message.isDeleted) {
CoroutineScope(Dispatchers.Main).launch {
try {
val chatActivity = commonMessageInterface as ChatActivity
val urlForChatting = ApiUtils.getUrlForChat(
chatActivity.chatApiVersion,
chatActivity.conversationUser?.baseUrl,
chatActivity.roomToken
)
}
} ?: run {
binding.messageQuote.quotedMessageImage.visibility = View.GONE
}
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
?: context.getText(R.string.nc_nick_guest)
binding.messageQuote.quotedMessage.text = messageUtils
.enrichChatReplyMessageText(
binding.messageQuote.quotedMessage.context,
parentChatMessage,
false,
viewThemeUtils
)
viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage)
viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor)
viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView)
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
val parentChatMessage = withContext(Dispatchers.IO) {
chatActivity.chatViewModel.getMessageById(
urlForChatting,
chatActivity.currentConversation!!,
message.parentMessageId!!
).first()
}
parentChatMessage.activeUser = message.activeUser
parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) {
addHeader(
"Authorization",
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
)
}
} ?: run {
binding.messageQuote.quotedMessageImage.visibility = View.GONE
}
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
?: context.getText(R.string.nc_nick_guest)
binding.messageQuote.quotedMessage.text = messageUtils
.enrichChatReplyMessageText(
binding.messageQuote.quotedMessage.context,
parentChatMessage,
false,
viewThemeUtils
)
viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage)
viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor)
viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView)
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
}
} else {
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
}

Просмотреть файл

@ -18,16 +18,19 @@ import android.view.View
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.content.res.AppCompatResources
import androidx.lifecycle.lifecycleScope
import autodagger.AutoInjector
import coil.load
import com.google.android.flexbox.FlexboxLayout
import com.google.android.material.snackbar.Snackbar
import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.talk.R
import com.nextcloud.talk.adapters.messages.IncomingPollMessageViewHolder.Companion
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.databinding.ItemCustomOutcomingLocationMessageBinding
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.models.json.chat.ReadStatus
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.ApiUtils
@ -35,6 +38,11 @@ import com.nextcloud.talk.utils.DateUtils
import com.nextcloud.talk.utils.UriUtils
import com.nextcloud.talk.utils.message.MessageUtils
import com.stfalcon.chatkit.messages.MessageHolders
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.URLEncoder
import javax.inject.Inject
import kotlin.math.roundToInt
@ -190,34 +198,53 @@ class OutcomingLocationMessageViewHolder(incomingView: View) :
}
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (!message.isDeleted && message.parentMessage != null) {
val parentChatMessage = message.parentMessage
parentChatMessage!!.activeUser = message.activeUser
parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) {
addHeader(
"Authorization",
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
if (message.parentMessageId != null && !message.isDeleted) {
CoroutineScope(Dispatchers.Main).launch {
try {
val chatActivity = commonMessageInterface as ChatActivity
val urlForChatting = ApiUtils.getUrlForChat(
chatActivity.chatApiVersion,
chatActivity.conversationUser?.baseUrl,
chatActivity.roomToken
)
}
} ?: run {
binding.messageQuote.quotedMessageImage.visibility = View.GONE
}
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
?: context.getText(R.string.nc_nick_guest)
binding.messageQuote.quotedMessage.text = messageUtils
.enrichChatReplyMessageText(
binding.messageQuote.quotedMessage.context,
parentChatMessage,
false,
viewThemeUtils
)
viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage)
viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor)
viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView)
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
val parentChatMessage = withContext(Dispatchers.IO) {
chatActivity.chatViewModel.getMessageById(
urlForChatting,
chatActivity.currentConversation!!,
message.parentMessageId!!
).first()
}
parentChatMessage.activeUser = message.activeUser
parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) {
addHeader(
"Authorization",
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
)
}
} ?: run {
binding.messageQuote.quotedMessageImage.visibility = View.GONE
}
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
?: context.getText(R.string.nc_nick_guest)
binding.messageQuote.quotedMessage.text = messageUtils
.enrichChatReplyMessageText(
binding.messageQuote.quotedMessage.context,
parentChatMessage,
false,
viewThemeUtils
)
viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage)
viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor)
viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView)
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
}
} else {
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
}

Просмотреть файл

@ -9,18 +9,21 @@ package com.nextcloud.talk.adapters.messages
import android.annotation.SuppressLint
import android.content.Context
import android.util.Log
import android.view.View
import androidx.appcompat.content.res.AppCompatResources
import androidx.lifecycle.lifecycleScope
import autodagger.AutoInjector
import coil.load
import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.talk.R
import com.nextcloud.talk.adapters.messages.IncomingPollMessageViewHolder.Companion
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.databinding.ItemCustomOutcomingPollMessageBinding
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.models.json.chat.ReadStatus
import com.nextcloud.talk.polls.ui.PollMainDialogFragment
import com.nextcloud.talk.ui.theme.ViewThemeUtils
@ -29,6 +32,11 @@ import com.nextcloud.talk.utils.DateUtils
import com.nextcloud.talk.utils.message.MessageUtils
import com.nextcloud.talk.utils.preferences.AppPreferences
import com.stfalcon.chatkit.messages.MessageHolders
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
@ -153,34 +161,53 @@ class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) :
}
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (!message.isDeleted && message.parentMessage != null) {
val parentChatMessage = message.parentMessage
parentChatMessage!!.activeUser = message.activeUser
parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) {
addHeader(
"Authorization",
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
if (message.parentMessageId != null && !message.isDeleted) {
CoroutineScope(Dispatchers.Main).launch {
try {
val chatActivity = commonMessageInterface as ChatActivity
val urlForChatting = ApiUtils.getUrlForChat(
chatActivity.chatApiVersion,
chatActivity.conversationUser?.baseUrl,
chatActivity.roomToken
)
}
} ?: run {
binding.messageQuote.quotedMessageImage.visibility = View.GONE
}
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
?: context.getText(R.string.nc_nick_guest)
binding.messageQuote.quotedMessage.text = messageUtils
.enrichChatReplyMessageText(
binding.messageQuote.quotedMessage.context,
parentChatMessage,
false,
viewThemeUtils
)
viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage)
viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor)
viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView)
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
val parentChatMessage = withContext(Dispatchers.IO) {
chatActivity.chatViewModel.getMessageById(
urlForChatting,
chatActivity.currentConversation!!,
message.parentMessageId!!
).first()
}
parentChatMessage.activeUser = message.activeUser
parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) {
addHeader(
"Authorization",
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
)
}
} ?: run {
binding.messageQuote.quotedMessageImage.visibility = View.GONE
}
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
?: context.getText(R.string.nc_nick_guest)
binding.messageQuote.quotedMessage.text = messageUtils
.enrichChatReplyMessageText(
binding.messageQuote.quotedMessage.context,
parentChatMessage,
false,
viewThemeUtils
)
viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage)
viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor)
viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView)
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
}
} else {
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
}

Просмотреть файл

@ -17,7 +17,7 @@ import com.google.android.material.card.MaterialCardView;
import com.nextcloud.talk.R;
import com.nextcloud.talk.databinding.ItemCustomOutcomingPreviewMessageBinding;
import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding;
import com.nextcloud.talk.models.json.chat.ChatMessage;
import com.nextcloud.talk.chat.data.model.ChatMessage;
import com.nextcloud.talk.utils.TextMatchers;
import java.util.HashMap;

Просмотреть файл

@ -9,6 +9,7 @@
package com.nextcloud.talk.adapters.messages
import android.content.Context
import android.util.Log
import android.util.TypedValue
import android.view.View
import androidx.core.content.res.ResourcesCompat
@ -20,8 +21,8 @@ import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.databinding.ItemCustomOutcomingTextMessageBinding
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.models.json.chat.ReadStatus
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.ApiUtils
@ -29,6 +30,11 @@ import com.nextcloud.talk.utils.DateUtils
import com.nextcloud.talk.utils.TextMatchers
import com.nextcloud.talk.utils.message.MessageUtils
import com.stfalcon.chatkit.messages.MessageHolders.OutcomingTextMessageViewHolder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
@ -91,14 +97,14 @@ class OutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessageViewH
if (message.lastEditTimestamp != 0L && !message.isDeleted) {
binding.messageEditIndicator.visibility = View.VISIBLE
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.lastEditTimestamp)
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.lastEditTimestamp!!)
} else {
binding.messageEditIndicator.visibility = View.GONE
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
}
// parent message handling
if (!message.isDeleted && message.parentMessage != null) {
if (!message.isDeleted && message.parentMessageId != null) {
processParentMessage(message)
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} else {
@ -148,36 +154,58 @@ class OutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessageViewH
}
private fun processParentMessage(message: ChatMessage) {
val parentChatMessage = message.parentMessage
parentChatMessage!!.activeUser = message.activeUser
parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) {
addHeader(
"Authorization",
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
)
if (message.parentMessageId != null && !message.isDeleted) {
CoroutineScope(Dispatchers.Main).launch {
try {
val chatActivity = commonMessageInterface as ChatActivity
val urlForChatting = ApiUtils.getUrlForChat(
chatActivity.chatApiVersion,
chatActivity.conversationUser?.baseUrl,
chatActivity.roomToken
)
val parentChatMessage = withContext(Dispatchers.IO) {
chatActivity.chatViewModel.getMessageById(
urlForChatting,
chatActivity.currentConversation!!,
message.parentMessageId!!
).first()
}
parentChatMessage!!.activeUser = message.activeUser
parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) {
addHeader(
"Authorization",
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
)
}
} ?: run {
binding.messageQuote.quotedMessageImage.visibility = View.GONE
}
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
?: context.getText(R.string.nc_nick_guest)
binding.messageQuote.quotedMessage.text = messageUtils
.enrichChatReplyMessageText(
binding.messageQuote.quotedMessage.context,
parentChatMessage,
false,
viewThemeUtils
)
viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage)
viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor)
viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView)
binding.messageQuote.quotedChatMessageView.setOnClickListener {
val chatActivity = commonMessageInterface as ChatActivity
chatActivity.jumpToQuotedMessage(parentChatMessage)
}
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
}
} ?: run {
binding.messageQuote.quotedMessageImage.visibility = View.GONE
}
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
?: context.getText(R.string.nc_nick_guest)
binding.messageQuote.quotedMessage.text = messageUtils
.enrichChatReplyMessageText(
binding.messageQuote.quotedMessage.context,
parentChatMessage,
false,
viewThemeUtils
)
viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage)
viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor)
viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView)
binding.messageQuote.quotedChatMessageView.setOnClickListener {
val chatActivity = commonMessageInterface as ChatActivity
chatActivity.jumpToQuotedMessage(parentChatMessage)
}
}
@ -191,5 +219,6 @@ class OutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessageViewH
companion object {
const val TEXT_SIZE_MULTIPLIER = 2.5
private val TAG = OutcomingTextMessageViewHolder::class.java.simpleName
}
}

Просмотреть файл

@ -17,16 +17,19 @@ import android.view.View
import android.widget.SeekBar
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.work.WorkInfo
import androidx.work.WorkManager
import autodagger.AutoInjector
import coil.load
import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.talk.R
import com.nextcloud.talk.adapters.messages.IncomingPollMessageViewHolder.Companion
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.databinding.ItemCustomOutcomingVoiceMessageBinding
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.models.json.chat.ReadStatus
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.ApiUtils
@ -34,6 +37,11 @@ import com.nextcloud.talk.utils.DateUtils
import com.nextcloud.talk.utils.message.MessageUtils
import com.nextcloud.talk.utils.preferences.AppPreferences
import com.stfalcon.chatkit.messages.MessageHolders
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.ExecutionException
import javax.inject.Inject
@ -238,14 +246,17 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) :
Log.d(TAG, "WorkInfo.State.RUNNING in ViewHolder")
showVoiceMessageLoading()
}
WorkInfo.State.SUCCEEDED -> {
Log.d(TAG, "WorkInfo.State.SUCCEEDED in ViewHolder")
showPlayButton()
}
WorkInfo.State.FAILED -> {
Log.d(TAG, "WorkInfo.State.FAILED in ViewHolder")
showPlayButton()
}
else -> {
Log.d(TAG, "WorkInfo.State unused in ViewHolder")
}
@ -264,34 +275,53 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) :
}
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (!message.isDeleted && message.parentMessage != null) {
val parentChatMessage = message.parentMessage
parentChatMessage!!.activeUser = message.activeUser
parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) {
addHeader(
"Authorization",
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
if (message.parentMessageId != null && !message.isDeleted) {
CoroutineScope(Dispatchers.Main).launch {
try {
val chatActivity = commonMessageInterface as ChatActivity
val urlForChatting = ApiUtils.getUrlForChat(
chatActivity.chatApiVersion,
chatActivity.conversationUser?.baseUrl,
chatActivity.roomToken
)
}
} ?: run {
binding.messageQuote.quotedMessageImage.visibility = View.GONE
}
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
?: context!!.getText(R.string.nc_nick_guest)
binding.messageQuote.quotedMessage.text = messageUtils
.enrichChatReplyMessageText(
binding.messageQuote.quotedMessage.context,
parentChatMessage,
false,
viewThemeUtils
)
viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage)
viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor)
viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView)
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
val parentChatMessage = withContext(Dispatchers.IO) {
chatActivity.chatViewModel.getMessageById(
urlForChatting,
chatActivity.currentConversation!!,
message.parentMessageId!!
).first()
}
parentChatMessage.activeUser = message.activeUser
parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) {
addHeader(
"Authorization",
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
)
}
} ?: run {
binding.messageQuote.quotedMessageImage.visibility = View.GONE
}
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
?: context!!.getText(R.string.nc_nick_guest)
binding.messageQuote.quotedMessage.text = messageUtils
.enrichChatReplyMessageText(
binding.messageQuote.quotedMessage.context,
parentChatMessage,
false,
viewThemeUtils
)
viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage)
viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor)
viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView)
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
}
} else {
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
}

Просмотреть файл

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

Просмотреть файл

@ -34,7 +34,7 @@ import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding
import com.nextcloud.talk.extensions.loadChangelogBotAvatar
import com.nextcloud.talk.extensions.loadFederatedUserAvatar
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.DateUtils

Просмотреть файл

@ -12,7 +12,7 @@ import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.DisplayUtils
import com.vanniktech.emoji.EmojiTextView

Просмотреть файл

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

Просмотреть файл

@ -19,7 +19,7 @@ import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.databinding.ItemSystemMessageBinding
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.utils.DateUtils
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.preferences.AppPreferences

Просмотреть файл

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

Просмотреть файл

@ -9,7 +9,7 @@ package com.nextcloud.talk.adapters.messages;
import android.view.View;
import com.nextcloud.talk.models.json.chat.ChatMessage;
import com.nextcloud.talk.chat.data.model.ChatMessage;
import com.stfalcon.chatkit.messages.MessageHolders;
public class UnreadNoticeMessageViewHolder extends MessageHolders.SystemMessageViewHolder<ChatMessage> {

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -27,6 +27,7 @@ import android.view.View
import android.view.ViewGroup
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.animation.Animation.AnimationListener
import android.view.animation.LinearInterpolator
import android.widget.ImageButton
import android.widget.ImageView
@ -40,6 +41,7 @@ import androidx.core.content.ContextCompat
import androidx.core.widget.doAfterTextChanged
import androidx.emoji2.widget.EmojiTextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import autodagger.AutoInjector
import coil.load
import com.google.android.flexbox.FlexboxLayout
@ -50,10 +52,11 @@ import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.callbacks.MentionAutocompleteCallback
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.chat.viewmodels.ChatViewModel
import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.databinding.FragmentMessageInputBinding
import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.models.json.mention.Mention
import com.nextcloud.talk.models.json.signaling.NCSignalingMessage
import com.nextcloud.talk.presenters.MentionAutocompletePresenter
@ -70,6 +73,9 @@ import com.nextcloud.talk.utils.text.Spans
import com.otaliastudios.autocomplete.Autocomplete
import com.stfalcon.chatkit.commons.models.IMessage
import com.vanniktech.emoji.EmojiPopup
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import java.util.Objects
import javax.inject.Inject
@ -101,6 +107,9 @@ class MessageInputFragment : Fragment() {
@Inject
lateinit var userManager: UserManager
@Inject
lateinit var networkMonitor: NetworkMonitor
lateinit var binding: FragmentMessageInputBinding
private var typedWhileTypingTimerIsRunning: Boolean = false
private var typingTimer: CountDownTimer? = null
@ -158,6 +167,76 @@ class MessageInputFragment : Fragment() {
else -> {}
}
}
viewLifecycleOwner.lifecycleScope.launch {
var wasOnline = true
networkMonitor.isOnline.onEach { isOnline ->
val connectionGained = (!wasOnline && isOnline)
wasOnline = !binding.fragmentMessageInputView.isShown
Log.d(TAG, "isOnline: $isOnline\nwasOnline: $wasOnline\nconnectionGained: $connectionGained")
handleMessageQueue(isOnline)
handleUI(isOnline, connectionGained)
}.collect()
}
}
private fun handleUI(isOnline: Boolean, connectionGained: Boolean) {
if (isOnline) {
if (connectionGained) {
val animation: Animation = AlphaAnimation(1.0f, 0.0f)
animation.duration = 3000
animation.interpolator = LinearInterpolator()
binding.fragmentConnectionLost.setBackgroundColor(resources.getColor(R.color.hwSecurityGreen))
binding.fragmentConnectionLost.text = getString(R.string.connection_gained)
binding.fragmentConnectionLost.startAnimation(animation)
binding.fragmentConnectionLost.animation.setAnimationListener(object : AnimationListener {
override fun onAnimationStart(animation: Animation?) {
// unused atm
}
override fun onAnimationEnd(animation: Animation?) {
binding.fragmentConnectionLost.visibility = View.GONE
binding.fragmentConnectionLost.setBackgroundColor(resources.getColor(R.color.hwSecurityRed))
binding.fragmentConnectionLost.text =
getString(R.string.connection_lost_sent_messages_are_queued)
}
override fun onAnimationRepeat(animation: Animation?) {
// unused atm
}
})
}
binding.fragmentMessageInputView.attachmentButton.isEnabled = true
binding.fragmentMessageInputView.recordAudioButton.isEnabled = true
} else {
binding.fragmentConnectionLost.clearAnimation()
binding.fragmentConnectionLost.visibility = View.GONE
binding.fragmentConnectionLost.setBackgroundColor(resources.getColor(R.color.hwSecurityRed))
binding.fragmentConnectionLost.text =
getString(R.string.connection_lost_sent_messages_are_queued)
binding.fragmentConnectionLost.visibility = View.VISIBLE
binding.fragmentMessageInputView.attachmentButton.isEnabled = false
binding.fragmentMessageInputView.recordAudioButton.isEnabled = false
}
}
private fun handleMessageQueue(isOnline: Boolean) {
if (isOnline) {
chatActivity.messageInputViewModel.switchToMessageQueue(false)
chatActivity.messageInputViewModel.sendAndEmptyMessageQueue(
chatActivity.roomToken,
chatActivity.conversationUser!!.getCredentials(),
ApiUtils.getUrlForChat(
chatActivity.chatApiVersion,
chatActivity.conversationUser!!.baseUrl!!,
chatActivity.roomToken
)
)
} else {
chatActivity.messageInputViewModel.switchToMessageQueue(true)
}
}
private fun restoreState() {
@ -694,6 +773,7 @@ class MessageInputFragment : Fragment() {
private fun sendMessage(message: CharSequence, replyTo: Int?, sendWithoutNotification: Boolean) {
chatActivity.messageInputViewModel.sendChatMessage(
chatActivity.roomToken,
chatActivity.conversationUser!!.getCredentials(),
ApiUtils.getUrlForChat(
chatActivity.chatApiVersion,

Просмотреть файл

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

Просмотреть файл

@ -2,120 +2,88 @@
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
* SPDX-FileCopyrightText: 2022-2024 Marcel Hibbe <dev@mhibbe.de>
* SPDX-FileCopyrightText: 2021 Tim Krüger <t@timkrueger.me>
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.models.json.chat
package com.nextcloud.talk.chat.data.model
import android.os.Parcelable
import android.text.TextUtils
import android.util.Log
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonIgnore
import com.bluelinelabs.logansquare.annotation.JsonObject
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.json.chat.ChatUtils.Companion.getParsedMessage
import com.nextcloud.talk.models.json.chat.ReadStatus
import com.nextcloud.talk.models.json.converters.EnumSystemMessageTypeConverter
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.CapabilitiesUtil
import com.stfalcon.chatkit.commons.models.IUser
import com.stfalcon.chatkit.commons.models.MessageContentType
import kotlinx.parcelize.Parcelize
import java.security.MessageDigest
import java.util.Date
@Parcelize
@JsonObject
data class ChatMessage(
@JsonIgnore
var isGrouped: Boolean = false,
@JsonIgnore
var isOneToOneConversation: Boolean = false,
@JsonIgnore
var isFormerOneToOneConversation: Boolean = false,
@JsonIgnore
var activeUser: User? = null,
@JsonIgnore
var selectedIndividualHashMap: Map<String?, String?>? = null,
@JsonIgnore
var isDeleted: Boolean = false,
@JsonField(name = ["id"])
var jsonMessageId: Int = 0,
@JsonIgnore
var previousMessageId: Int = -1,
@JsonField(name = ["token"])
var token: String? = null,
// guests or users
@JsonField(name = ["actorType"])
var actorType: String? = null,
@JsonField(name = ["actorId"])
var actorId: String? = null,
// send when crafting a message
@JsonField(name = ["actorDisplayName"])
var actorDisplayName: String? = null,
@JsonField(name = ["timestamp"])
var timestamp: Long = 0,
// send when crafting a message, max 1000 lines
@JsonField(name = ["message"])
var message: String? = null,
@JsonField(name = ["messageParameters"])
var messageParameters: HashMap<String?, HashMap<String?, String?>>? = null,
@JsonField(name = ["systemMessage"], typeConverter = EnumSystemMessageTypeConverter::class)
var systemMessageType: SystemMessageType? = null,
@JsonField(name = ["isReplyable"])
var replyable: Boolean = false,
@JsonField(name = ["parent"])
var parentMessage: ChatMessage? = null,
var parentMessageId: Long? = null,
var readStatus: Enum<ReadStatus> = ReadStatus.NONE,
@JsonField(name = ["messageType"])
var messageType: String? = null,
@JsonField(name = ["reactions"])
var reactions: LinkedHashMap<String, Int>? = null,
@JsonField(name = ["reactionsSelf"])
var reactionsSelf: ArrayList<String>? = null,
@JsonField(name = ["expirationTimestamp"])
var expirationTimestamp: Int = 0,
@JsonField(name = ["markdown"])
var renderMarkdown: Boolean? = null,
@JsonField(name = ["lastEditActorDisplayName"])
var lastEditActorDisplayName: String? = null,
@JsonField(name = ["lastEditActorId"])
var lastEditActorId: String? = null,
@JsonField(name = ["lastEditActorType"])
var lastEditActorType: String? = null,
@JsonField(name = ["lastEditTimestamp"])
var lastEditTimestamp: Long = 0,
var lastEditTimestamp: Long? = 0,
var isDownloadingVoiceMessage: Boolean = false,
@ -145,7 +113,7 @@ data class ChatMessage(
var openWhenDownloaded: Boolean = true
) : Parcelable, MessageContentType, MessageContentType.Image {
) : MessageContentType, MessageContentType.Image {
var extractedUrlToPreview: String? = null
@ -282,95 +250,7 @@ data class ChatMessage(
}
}
val lastMessageDisplayText: String
get() {
if (getCalculateMessageType() == MessageType.REGULAR_TEXT_MESSAGE ||
getCalculateMessageType() == MessageType.SYSTEM_MESSAGE ||
getCalculateMessageType() == MessageType.SINGLE_LINK_MESSAGE
) {
return text
} else {
if (MessageType.SINGLE_LINK_GIPHY_MESSAGE == getCalculateMessageType() ||
MessageType.SINGLE_LINK_TENOR_MESSAGE == getCalculateMessageType() ||
MessageType.SINGLE_LINK_GIF_MESSAGE == getCalculateMessageType()
) {
return if (actorId == activeUser!!.userId) {
sharedApplication!!.getString(R.string.nc_sent_a_gif_you)
} else {
String.format(
sharedApplication!!.resources.getString(R.string.nc_sent_a_gif),
getNullsafeActorDisplayName()
)
}
} else if (MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == getCalculateMessageType()) {
return if (actorId == activeUser!!.userId) {
sharedApplication!!.getString(R.string.nc_sent_an_attachment_you)
} else {
String.format(
sharedApplication!!.resources.getString(R.string.nc_sent_an_attachment),
getNullsafeActorDisplayName()
)
}
} else if (MessageType.SINGLE_NC_GEOLOCATION_MESSAGE == getCalculateMessageType()) {
return if (actorId == activeUser!!.userId) {
sharedApplication!!.getString(R.string.nc_sent_location_you)
} else {
String.format(
sharedApplication!!.resources.getString(R.string.nc_sent_location),
getNullsafeActorDisplayName()
)
}
} else if (MessageType.VOICE_MESSAGE == getCalculateMessageType()) {
return if (actorId == activeUser!!.userId) {
sharedApplication!!.getString(R.string.nc_sent_voice_you)
} else {
String.format(
sharedApplication!!.resources.getString(R.string.nc_sent_voice),
getNullsafeActorDisplayName()
)
}
} else if (MessageType.SINGLE_LINK_AUDIO_MESSAGE == getCalculateMessageType()) {
return if (actorId == activeUser!!.userId) {
sharedApplication!!.getString(R.string.nc_sent_an_audio_you)
} else {
String.format(
sharedApplication!!.resources.getString(R.string.nc_sent_an_audio),
getNullsafeActorDisplayName()
)
}
} else if (MessageType.SINGLE_LINK_VIDEO_MESSAGE == getCalculateMessageType()) {
return if (actorId == activeUser!!.userId) {
sharedApplication!!.getString(R.string.nc_sent_a_video_you)
} else {
String.format(
sharedApplication!!.resources.getString(R.string.nc_sent_a_video),
getNullsafeActorDisplayName()
)
}
} else if (MessageType.SINGLE_LINK_IMAGE_MESSAGE == getCalculateMessageType()) {
return if (actorId == activeUser!!.userId) {
sharedApplication!!.getString(R.string.nc_sent_an_image_you)
} else {
String.format(
sharedApplication!!.resources.getString(R.string.nc_sent_an_image),
getNullsafeActorDisplayName()
)
}
} else if (MessageType.POLL_MESSAGE == getCalculateMessageType()) {
return if (actorId == activeUser!!.userId) {
sharedApplication!!.getString(R.string.nc_sent_poll_you)
} else {
String.format(
sharedApplication!!.resources.getString(R.string.nc_sent_poll),
getNullsafeActorDisplayName()
)
}
}
}
return ""
}
private fun getNullsafeActorDisplayName() =
fun getNullsafeActorDisplayName() =
if (!TextUtils.isEmpty(actorDisplayName)) {
actorDisplayName
} else {

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -6,7 +6,7 @@
*/
package com.nextcloud.talk.conversation.repository
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.models.json.conversations.ConversationEnums
import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.models.json.generic.GenericOverall
import io.reactivex.Observable
@ -15,5 +15,8 @@ interface ConversationRepository {
fun renameConversation(roomToken: String, roomNameNew: String): Observable<GenericOverall>
fun createConversation(roomName: String, conversationType: Conversation.ConversationType?): Observable<RoomOverall>
fun createConversation(
roomName: String,
conversationType: ConversationEnums.ConversationType?
): Observable<RoomOverall>
}

Просмотреть файл

@ -9,7 +9,7 @@ package com.nextcloud.talk.conversation.repository
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.RetrofitBucket
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.models.json.conversations.ConversationEnums
import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.utils.ApiUtils
@ -43,29 +43,30 @@ class ConversationRepositoryImpl(private val ncApi: NcApi, currentUserProvider:
override fun createConversation(
roomName: String,
conversationType: Conversation.ConversationType?
conversationType: ConversationEnums.ConversationType?
): Observable<RoomOverall> {
val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1))
val retrofitBucket: RetrofitBucket = if (conversationType == Conversation.ConversationType.ROOM_PUBLIC_CALL) {
ApiUtils.getRetrofitBucketForCreateRoom(
apiVersion,
currentUser.baseUrl!!,
ROOM_TYPE_PUBLIC,
null,
null,
roomName
)
} else {
ApiUtils.getRetrofitBucketForCreateRoom(
apiVersion,
currentUser.baseUrl!!,
ROOM_TYPE_GROUP,
null,
null,
roomName
)
}
val retrofitBucket: RetrofitBucket =
if (conversationType == ConversationEnums.ConversationType.ROOM_PUBLIC_CALL) {
ApiUtils.getRetrofitBucketForCreateRoom(
apiVersion,
currentUser.baseUrl!!,
ROOM_TYPE_PUBLIC,
null,
null,
roomName
)
} else {
ApiUtils.getRetrofitBucketForCreateRoom(
apiVersion,
currentUser.baseUrl!!,
ROOM_TYPE_GROUP,
null,
null,
roomName
)
}
return ncApi.createRoom(credentials, retrofitBucket.url, retrofitBucket.queryMap)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -34,8 +34,8 @@ import com.nextcloud.talk.extensions.loadConversationAvatar
import com.nextcloud.talk.extensions.loadSystemAvatar
import com.nextcloud.talk.extensions.loadUserAvatar
import com.nextcloud.talk.models.domain.ConversationModel
import com.nextcloud.talk.models.domain.ConversationType
import com.nextcloud.talk.models.json.capabilities.SpreedCapability
import com.nextcloud.talk.models.json.conversations.ConversationEnums
import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.CapabilitiesUtil
@ -126,10 +126,6 @@ class ConversationInfoEditActivity : BaseActivity() {
initObservers()
}
override fun onResume() {
super.onResume()
}
private fun initObservers() {
conversationInfoEditViewModel.viewState.observe(this) { state ->
when (state) {
@ -349,15 +345,18 @@ class ConversationInfoEditActivity : BaseActivity() {
setupAvatarOptions()
when (conversation!!.type) {
ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> if (!TextUtils.isEmpty(conversation!!.name)) {
ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> if (!TextUtils.isEmpty(
conversation!!.name
)
) {
conversation!!.name?.let { binding.avatarImage.loadUserAvatar(conversationUser, it, true, false) }
}
ConversationType.ROOM_GROUP_CALL, ConversationType.ROOM_PUBLIC_CALL -> {
ConversationEnums.ConversationType.ROOM_GROUP_CALL, ConversationEnums.ConversationType.ROOM_PUBLIC_CALL -> {
binding.avatarImage.loadConversationAvatar(conversationUser, conversation!!, false, viewThemeUtils)
}
ConversationType.ROOM_SYSTEM -> {
ConversationEnums.ConversationType.ROOM_SYSTEM -> {
binding.avatarImage.loadSystemAvatar()
}

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -10,7 +10,7 @@ import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.nextcloud.talk.conversationlist.data.ConversationsListRepository
import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository
import com.nextcloud.talk.invitation.data.InvitationsModel
import com.nextcloud.talk.invitation.data.InvitationsRepository
import com.nextcloud.talk.users.UserManager
@ -18,21 +18,36 @@ import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.onEach
import javax.inject.Inject
class ConversationsListViewModel @Inject constructor(
private val conversationsListRepository: ConversationsListRepository
private val repository: OfflineConversationsRepository,
var userManager: UserManager
) :
ViewModel() {
@Inject
lateinit var invitationsRepository: InvitationsRepository
@Inject
lateinit var userManager: UserManager
sealed interface ViewState
object GetRoomsStartState : ViewState
object GetRoomsErrorState : ViewState
open class GetRoomsSuccessState(val listIsNotEmpty: Boolean) : ViewState
private val _getRoomsViewState: MutableLiveData<ViewState> = MutableLiveData(GetRoomsStartState)
val getRoomsViewState: LiveData<ViewState>
get() = _getRoomsViewState
val getRoomsFlow = repository.roomListFlow
.onEach { list ->
_getRoomsViewState.value = GetRoomsSuccessState(list.isNotEmpty())
}.catch {
_getRoomsViewState.value = GetRoomsErrorState
}
object GetFederationInvitationsStartState : ViewState
object GetFederationInvitationsErrorState : ViewState
@ -63,6 +78,12 @@ class ConversationsListViewModel @Inject constructor(
}
}
fun getRooms() {
val startNanoTime = System.nanoTime()
Log.d(TAG, "fetchData - getRooms - calling: $startNanoTime")
repository.getRooms()
}
inner class FederatedInvitationsObserver : Observer<InvitationsModel> {
override fun onSubscribe(d: Disposable) {
// unused atm

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -1,7 +1,7 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
* SPDX-FileCopyrightText: 2022-2024 Marcel Hibbe <dev@mhibbe.de>
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
@ -10,17 +10,25 @@
package com.nextcloud.talk.dagger.modules
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.chat.data.ChatMessageRepository
import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource
import com.nextcloud.talk.chat.data.network.OfflineFirstChatRepository
import com.nextcloud.talk.chat.data.network.RetrofitChatNetwork
import com.nextcloud.talk.api.NcApiCoroutines
import com.nextcloud.talk.chat.data.ChatRepository
import com.nextcloud.talk.chat.data.network.NetworkChatRepositoryImpl
import com.nextcloud.talk.contacts.ContactsRepository
import com.nextcloud.talk.contacts.ContactsRepositoryImpl
import com.nextcloud.talk.conversation.repository.ConversationRepository
import com.nextcloud.talk.conversation.repository.ConversationRepositoryImpl
import com.nextcloud.talk.conversationinfoedit.data.ConversationInfoEditRepository
import com.nextcloud.talk.conversationinfoedit.data.ConversationInfoEditRepositoryImpl
import com.nextcloud.talk.conversationlist.data.ConversationsListRepository
import com.nextcloud.talk.conversationlist.data.ConversationsListRepositoryImpl
import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository
import com.nextcloud.talk.conversationlist.data.network.ConversationsNetworkDataSource
import com.nextcloud.talk.conversationlist.data.network.OfflineFirstConversationsRepository
import com.nextcloud.talk.conversationlist.data.network.RetrofitConversationsNetwork
import com.nextcloud.talk.data.database.dao.ChatBlocksDao
import com.nextcloud.talk.data.database.dao.ChatMessagesDao
import com.nextcloud.talk.data.database.dao.ConversationsDao
import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.data.source.local.TalkDatabase
import com.nextcloud.talk.data.storage.ArbitraryStoragesRepository
import com.nextcloud.talk.data.storage.ArbitraryStoragesRepositoryImpl
@ -51,6 +59,7 @@ import com.nextcloud.talk.translate.repositories.TranslateRepositoryImpl
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.DateUtils
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
import com.nextcloud.talk.utils.preferences.AppPreferences
import dagger.Module
import dagger.Provides
import okhttp3.OkHttpClient
@ -97,8 +106,12 @@ class RepositoryModule {
}
@Provides
fun provideReactionsRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): ReactionsRepository {
return ReactionsRepositoryImpl(ncApi, userProvider)
fun provideReactionsRepository(
ncApi: NcApi,
userProvider: CurrentUserProviderNew,
dao: ChatMessagesDao
): ReactionsRepository {
return ReactionsRepositoryImpl(ncApi, userProvider, dao)
}
@Provides
@ -128,13 +141,13 @@ class RepositoryModule {
}
@Provides
fun provideConversationsListRepository(ncApi: NcApi): ConversationsListRepository {
return ConversationsListRepositoryImpl(ncApi)
fun provideChatNetworkDataSource(ncApi: NcApi): ChatNetworkDataSource {
return RetrofitChatNetwork(ncApi)
}
@Provides
fun provideChatRepository(ncApi: NcApi): ChatRepository {
return NetworkChatRepositoryImpl(ncApi)
fun provideConversationsNetworkDataSource(ncApi: NcApi): ConversationsNetworkDataSource {
return RetrofitConversationsNetwork(ncApi)
}
@Provides
@ -155,6 +168,34 @@ class RepositoryModule {
return InvitationsRepositoryImpl(ncApi)
}
@Provides
fun provideOfflineFirstChatRepository(
chatMessagesDao: ChatMessagesDao,
chatBlocksDao: ChatBlocksDao,
dataSource: ChatNetworkDataSource,
appPreferences: AppPreferences,
networkMonitor: NetworkMonitor,
userProvider: CurrentUserProviderNew
): ChatMessageRepository {
return OfflineFirstChatRepository(
chatMessagesDao,
chatBlocksDao,
dataSource,
appPreferences,
networkMonitor,
userProvider
)
}
@Provides
fun provideOfflineFirstConversationsRepository(
dao: ConversationsDao,
dataSource: ConversationsNetworkDataSource,
currentUserProviderNew: CurrentUserProviderNew
): OfflineConversationsRepository {
return OfflineFirstConversationsRepository(dao, dataSource, currentUserProviderNew)
}
@Provides
fun provideContactsRepository(ncApiCoroutines: NcApiCoroutines, userManager: UserManager): ContactsRepository {
return ContactsRepositoryImpl(ncApiCoroutines, userManager)

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -6,7 +6,7 @@
*/
package com.nextcloud.talk.models.domain
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.chat.data.model.ChatMessage
data class ReactionAddedModel(
var chatMessage: ChatMessage,

Просмотреть файл

@ -6,7 +6,7 @@
*/
package com.nextcloud.talk.models.domain
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.chat.data.model.ChatMessage
data class ReactionDeletedModel(
var chatMessage: ChatMessage,

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -18,7 +18,10 @@ import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
import io.reactivex.Observable
class ConversationsRepositoryImpl(private val api: NcApi, private val userProvider: CurrentUserProviderNew) :
class ConversationsRepositoryImpl(
private val api: NcApi,
private val userProvider: CurrentUserProviderNew
) :
ConversationsRepository {
private val user: User

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше