Initial import of f7cloud-mobile native Android app.
Kotlin/Compose client for F7cloud (mail, files, talk, calendar, contacts, tasks, support). Current version: 0.5.113 (build 121).
This commit is contained in:
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2017-2019 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExampleInstrumentedTest {
|
||||
@Test
|
||||
public void useAppContext() throws Exception {
|
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getTargetContext();
|
||||
|
||||
assertNotNull(appContext.getPackageName());
|
||||
assertTrue("The package name must start with 'com.nextcloud.talk2'", appContext.getPackageName().startsWith("com.nextcloud.talk2"));
|
||||
}
|
||||
}
|
||||
Vendored
+48
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2021 Tobias Kaminsky <tobias@kaminsky.me>
|
||||
* SPDX-FileCopyrightText: 2021 F7cloud
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.activities
|
||||
|
||||
import androidx.test.espresso.intent.rule.IntentsTestRule
|
||||
import ru.f7cloud.talk.users.UserManager
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class MainActivityTest {
|
||||
@get:Rule
|
||||
val activityRule: IntentsTestRule<MainActivity> = IntentsTestRule(
|
||||
MainActivity::class.java,
|
||||
true,
|
||||
false
|
||||
)
|
||||
|
||||
@Test
|
||||
fun login() {
|
||||
val sut = activityRule.launchActivity(null)
|
||||
|
||||
val user = sut.userManager.storeProfile(
|
||||
"test",
|
||||
UserManager.UserAttributes(
|
||||
null,
|
||||
serverUrl = "http://server/nc",
|
||||
currentUser = true,
|
||||
userId = "test",
|
||||
token = "test",
|
||||
displayName = "Test Name",
|
||||
pushConfigurationState = null,
|
||||
capabilities = null,
|
||||
serverVersion = null,
|
||||
certificateAlias = null,
|
||||
externalSignalingServer = null
|
||||
)
|
||||
).blockingGet()
|
||||
|
||||
assertNotNull("Error creating user", user)
|
||||
}
|
||||
}
|
||||
Vendored
+393
@@ -0,0 +1,393 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package ru.f7cloud.talk.data.database.dao
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import ru.f7cloud.talk.data.database.model.ChatBlockEntity
|
||||
import ru.f7cloud.talk.data.database.model.ConversationEntity
|
||||
import ru.f7cloud.talk.data.source.local.TalkDatabase
|
||||
import ru.f7cloud.talk.data.user.UsersDao
|
||||
import ru.f7cloud.talk.data.user.model.UserEntity
|
||||
import ru.f7cloud.talk.models.json.conversations.ConversationEnums
|
||||
import ru.f7cloud.talk.models.json.participants.Participant
|
||||
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
|
||||
import java.lang.Boolean
|
||||
import kotlin.Long
|
||||
import kotlin.String
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ChatBlocksDaoTest {
|
||||
private lateinit var usersDao: UsersDao
|
||||
private lateinit var conversationsDao: ConversationsDao
|
||||
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()
|
||||
usersDao = db.usersDao()
|
||||
conversationsDao = db.conversationsDao()
|
||||
chatBlocksDao = db.chatBlocksDao()
|
||||
}
|
||||
|
||||
@After
|
||||
fun closeDb() = db.close()
|
||||
|
||||
@Test
|
||||
fun testGetChatBlocksContainingMessageId() =
|
||||
runTest {
|
||||
val user = createUserEntity("account1", "Account 1")
|
||||
usersDao.saveUser(user)
|
||||
val account1 = usersDao.getUserWithUserId("account1").blockingGet()
|
||||
|
||||
conversationsDao.upsertConversations(
|
||||
accountId = user.id,
|
||||
listOf(
|
||||
createConversationEntity(
|
||||
accountId = account1.id,
|
||||
"abc",
|
||||
roomName = "Conversation One"
|
||||
),
|
||||
createConversationEntity(
|
||||
accountId = account1.id,
|
||||
"def",
|
||||
roomName = "Conversation Two"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val conversation1 = conversationsDao.getConversationsForUser(account1.id).first()[0]
|
||||
|
||||
val chatBlock1 = ChatBlockEntity(
|
||||
internalConversationId = conversation1.internalId,
|
||||
accountId = conversation1.accountId,
|
||||
token = conversation1.token,
|
||||
threadId = 123,
|
||||
oldestMessageId = 50,
|
||||
newestMessageId = 60,
|
||||
hasHistory = true
|
||||
)
|
||||
|
||||
val chatBlock2 = ChatBlockEntity(
|
||||
internalConversationId = conversation1.internalId,
|
||||
accountId = conversation1.accountId,
|
||||
token = conversation1.token,
|
||||
threadId = 123,
|
||||
oldestMessageId = 10,
|
||||
newestMessageId = 20,
|
||||
hasHistory = true
|
||||
)
|
||||
|
||||
val chatBlock3 = ChatBlockEntity(
|
||||
internalConversationId = conversation1.internalId,
|
||||
accountId = conversation1.accountId,
|
||||
token = conversation1.token,
|
||||
threadId = null,
|
||||
oldestMessageId = 50,
|
||||
newestMessageId = 60,
|
||||
hasHistory = true
|
||||
)
|
||||
|
||||
chatBlocksDao.upsertChatBlock(chatBlock1)
|
||||
chatBlocksDao.upsertChatBlock(chatBlock2)
|
||||
chatBlocksDao.upsertChatBlock(chatBlock3)
|
||||
|
||||
val chatBlocksOfThread = chatBlocksDao.getChatBlocksContainingMessageId(
|
||||
internalConversationId = conversation1.internalId,
|
||||
threadId = 123,
|
||||
messageId = 55
|
||||
)
|
||||
|
||||
assertEquals(1, chatBlocksOfThread.first().size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetConnectedChatBlocks() =
|
||||
runTest {
|
||||
val user = createUserEntity("account1", "Account 1")
|
||||
usersDao.saveUser(user)
|
||||
val account1 = usersDao.getUserWithUserId("account1").blockingGet()
|
||||
|
||||
conversationsDao.upsertConversations(
|
||||
account1.id,
|
||||
listOf(
|
||||
createConversationEntity(
|
||||
accountId = account1.id,
|
||||
"abc",
|
||||
roomName = "Conversation One"
|
||||
),
|
||||
createConversationEntity(
|
||||
accountId = account1.id,
|
||||
"def",
|
||||
roomName = "Conversation Two"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val conversation1 = conversationsDao.getConversationsForUser(account1.id).first()[0]
|
||||
val conversation2 = conversationsDao.getConversationsForUser(account1.id).first()[1]
|
||||
|
||||
val searchedChatBlock = ChatBlockEntity(
|
||||
internalConversationId = conversation1.internalId,
|
||||
accountId = conversation1.accountId,
|
||||
token = conversation1.token,
|
||||
threadId = null,
|
||||
oldestMessageId = 50,
|
||||
newestMessageId = 60,
|
||||
hasHistory = true
|
||||
)
|
||||
|
||||
val chatBlockTooOld = ChatBlockEntity(
|
||||
internalConversationId = conversation1.internalId,
|
||||
accountId = conversation1.accountId,
|
||||
token = conversation1.token,
|
||||
threadId = null,
|
||||
oldestMessageId = 10,
|
||||
newestMessageId = 20,
|
||||
hasHistory = true
|
||||
)
|
||||
|
||||
val chatBlockOverlap1 = ChatBlockEntity(
|
||||
internalConversationId = conversation1.internalId,
|
||||
accountId = conversation1.accountId,
|
||||
token = conversation1.token,
|
||||
threadId = null,
|
||||
oldestMessageId = 45,
|
||||
newestMessageId = 55,
|
||||
hasHistory = true
|
||||
)
|
||||
|
||||
val chatBlockWithin = ChatBlockEntity(
|
||||
internalConversationId = conversation1.internalId,
|
||||
accountId = conversation1.accountId,
|
||||
token = conversation1.token,
|
||||
threadId = null,
|
||||
oldestMessageId = 52,
|
||||
newestMessageId = 58,
|
||||
hasHistory = true
|
||||
)
|
||||
|
||||
val chatBlockOverall = ChatBlockEntity(
|
||||
internalConversationId = conversation1.internalId,
|
||||
accountId = conversation1.accountId,
|
||||
token = conversation1.token,
|
||||
threadId = null,
|
||||
oldestMessageId = 1,
|
||||
newestMessageId = 99,
|
||||
hasHistory = true
|
||||
)
|
||||
|
||||
val chatBlockOverlap2 = ChatBlockEntity(
|
||||
internalConversationId = conversation1.internalId,
|
||||
accountId = conversation1.accountId,
|
||||
token = conversation1.token,
|
||||
threadId = null,
|
||||
oldestMessageId = 59,
|
||||
newestMessageId = 70,
|
||||
hasHistory = true
|
||||
)
|
||||
|
||||
val chatBlockTooNew = ChatBlockEntity(
|
||||
internalConversationId = conversation1.internalId,
|
||||
accountId = conversation1.accountId,
|
||||
token = conversation1.token,
|
||||
threadId = null,
|
||||
oldestMessageId = 80,
|
||||
newestMessageId = 90,
|
||||
hasHistory = true
|
||||
)
|
||||
|
||||
val chatBlockWithinButOtherConversation = ChatBlockEntity(
|
||||
internalConversationId = conversation2.internalId,
|
||||
accountId = conversation2.accountId,
|
||||
token = conversation2.token,
|
||||
threadId = null,
|
||||
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(
|
||||
internalConversationId = conversation1.internalId,
|
||||
threadId = null,
|
||||
oldestMessageId = searchedChatBlock.oldestMessageId,
|
||||
newestMessageId = searchedChatBlock.newestMessageId
|
||||
)
|
||||
|
||||
assertEquals(5, results.first().size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetConnectedChatBlocksWithThreadsScenario() =
|
||||
runTest {
|
||||
val user = createUserEntity("account1", "Account 1")
|
||||
usersDao.saveUser(user)
|
||||
val account1 = usersDao.getUserWithUserId("account1").blockingGet()
|
||||
|
||||
conversationsDao.upsertConversations(
|
||||
account1.id,
|
||||
listOf(
|
||||
createConversationEntity(
|
||||
accountId = account1.id,
|
||||
"abc",
|
||||
roomName = "Conversation One"
|
||||
),
|
||||
createConversationEntity(
|
||||
accountId = account1.id,
|
||||
"def",
|
||||
roomName = "Conversation Two"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val conversation1 = conversationsDao.getConversationsForUser(account1.id).first()[0]
|
||||
|
||||
val searchedChatBlock = ChatBlockEntity(
|
||||
internalConversationId = conversation1.internalId,
|
||||
accountId = conversation1.accountId,
|
||||
token = conversation1.token,
|
||||
threadId = 123,
|
||||
oldestMessageId = 50,
|
||||
newestMessageId = 60,
|
||||
hasHistory = true
|
||||
)
|
||||
|
||||
val chatBlockOverlap1 = ChatBlockEntity(
|
||||
internalConversationId = conversation1.internalId,
|
||||
accountId = conversation1.accountId,
|
||||
token = conversation1.token,
|
||||
threadId = null,
|
||||
oldestMessageId = 45,
|
||||
newestMessageId = 55,
|
||||
hasHistory = true
|
||||
)
|
||||
|
||||
val chatBlockOverlap2 = ChatBlockEntity(
|
||||
internalConversationId = conversation1.internalId,
|
||||
accountId = conversation1.accountId,
|
||||
token = conversation1.token,
|
||||
threadId = 123,
|
||||
oldestMessageId = 59,
|
||||
newestMessageId = 70,
|
||||
hasHistory = true
|
||||
)
|
||||
|
||||
chatBlocksDao.upsertChatBlock(searchedChatBlock)
|
||||
|
||||
chatBlocksDao.upsertChatBlock(chatBlockOverlap1)
|
||||
chatBlocksDao.upsertChatBlock(chatBlockOverlap2)
|
||||
|
||||
val resultsForThreadIdNull = chatBlocksDao.getConnectedChatBlocks(
|
||||
internalConversationId = conversation1.internalId,
|
||||
threadId = null,
|
||||
oldestMessageId = searchedChatBlock.oldestMessageId,
|
||||
newestMessageId = searchedChatBlock.newestMessageId
|
||||
)
|
||||
|
||||
assertEquals(1, resultsForThreadIdNull.first().size)
|
||||
|
||||
val resultsForThreadId123 = chatBlocksDao.getConnectedChatBlocks(
|
||||
internalConversationId = conversation1.internalId,
|
||||
threadId = 123,
|
||||
oldestMessageId = searchedChatBlock.oldestMessageId,
|
||||
newestMessageId = searchedChatBlock.newestMessageId
|
||||
)
|
||||
|
||||
assertEquals(2, resultsForThreadId123.first().size)
|
||||
}
|
||||
|
||||
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 = Boolean.FALSE,
|
||||
scheduledForDeletion = Boolean.FALSE
|
||||
)
|
||||
|
||||
private fun createConversationEntity(accountId: Long, token: String, roomName: String) =
|
||||
ConversationEntity(
|
||||
internalId = "$accountId@$token",
|
||||
accountId = accountId,
|
||||
token = token,
|
||||
name = roomName,
|
||||
actorId = "",
|
||||
actorType = "",
|
||||
messageExpiration = 0,
|
||||
unreadMessages = 0,
|
||||
statusMessage = null,
|
||||
lastMessage = null,
|
||||
canDeleteConversation = false,
|
||||
canLeaveConversation = false,
|
||||
lastCommonReadMessage = 0,
|
||||
lastReadMessage = 0,
|
||||
type = ConversationEnums.ConversationType.DUMMY,
|
||||
status = "",
|
||||
callFlag = 1,
|
||||
favorite = false,
|
||||
lastPing = 0,
|
||||
hasCall = false,
|
||||
sessionId = "",
|
||||
canStartCall = false,
|
||||
lastActivity = 0,
|
||||
remoteServer = "",
|
||||
avatarVersion = "",
|
||||
unreadMentionDirect = false,
|
||||
callRecording = 1,
|
||||
callStartTime = 0,
|
||||
statusClearAt = 0,
|
||||
unreadMention = false,
|
||||
lobbyState = ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY,
|
||||
lobbyTimer = 0,
|
||||
objectType = ConversationEnums.ObjectType.FILE,
|
||||
objectId = "",
|
||||
statusIcon = null,
|
||||
description = "",
|
||||
displayName = "",
|
||||
hasPassword = false,
|
||||
permissions = 0,
|
||||
notificationCalls = 0,
|
||||
remoteToken = "",
|
||||
notificationLevel = ConversationEnums.NotificationLevel.ALWAYS,
|
||||
conversationReadOnlyState = ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_ONLY,
|
||||
hasCustomAvatar = false,
|
||||
participantType = Participant.ParticipantType.DUMMY,
|
||||
recordingConsentRequired = 1
|
||||
)
|
||||
}
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package ru.f7cloud.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 ru.f7cloud.talk.chat.data.model.ChatMessage
|
||||
import ru.f7cloud.talk.data.database.model.ChatMessageEntity
|
||||
import ru.f7cloud.talk.data.database.model.ConversationEntity
|
||||
import ru.f7cloud.talk.data.source.local.TalkDatabase
|
||||
import ru.f7cloud.talk.data.user.UsersDao
|
||||
import ru.f7cloud.talk.data.user.model.UserEntity
|
||||
import ru.f7cloud.talk.models.json.conversations.ConversationEnums
|
||||
import ru.f7cloud.talk.models.json.participants.Participant
|
||||
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(
|
||||
account1.id,
|
||||
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,
|
||||
null
|
||||
)
|
||||
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,
|
||||
null
|
||||
)
|
||||
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,
|
||||
actorId = "",
|
||||
actorType = "",
|
||||
messageExpiration = 0,
|
||||
unreadMessages = 0,
|
||||
statusMessage = null,
|
||||
lastMessage = null,
|
||||
canDeleteConversation = false,
|
||||
canLeaveConversation = false,
|
||||
lastCommonReadMessage = 0,
|
||||
lastReadMessage = 0,
|
||||
type = ConversationEnums.ConversationType.DUMMY,
|
||||
status = "",
|
||||
callFlag = 1,
|
||||
favorite = false,
|
||||
lastPing = 0,
|
||||
hasCall = false,
|
||||
sessionId = "",
|
||||
canStartCall = false,
|
||||
lastActivity = 0,
|
||||
remoteServer = "",
|
||||
avatarVersion = "",
|
||||
unreadMentionDirect = false,
|
||||
callRecording = 1,
|
||||
callStartTime = 0,
|
||||
statusClearAt = 0,
|
||||
unreadMention = false,
|
||||
lobbyState = ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY,
|
||||
lobbyTimer = 0,
|
||||
objectType = ConversationEnums.ObjectType.FILE,
|
||||
objectId = "",
|
||||
statusIcon = null,
|
||||
description = "",
|
||||
displayName = "",
|
||||
hasPassword = false,
|
||||
permissions = 0,
|
||||
notificationCalls = 0,
|
||||
remoteToken = "",
|
||||
notificationLevel = ConversationEnums.NotificationLevel.ALWAYS,
|
||||
conversationReadOnlyState = ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_ONLY,
|
||||
hasCustomAvatar = false,
|
||||
participantType = Participant.ParticipantType.DUMMY,
|
||||
recordingConsentRequired = 1
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
deleted = false,
|
||||
token = "",
|
||||
actorId = "",
|
||||
actorType = "",
|
||||
accountId = 1,
|
||||
messageParameters = null,
|
||||
messageType = "",
|
||||
parentMessageId = null,
|
||||
systemMessageType = ChatMessage.SystemMessageType.DUMMY,
|
||||
replyable = false,
|
||||
timestamp = 0,
|
||||
expirationTimestamp = 0,
|
||||
actorDisplayName = "",
|
||||
lastEditActorType = null,
|
||||
lastEditTimestamp = null,
|
||||
renderMarkdown = true,
|
||||
lastEditActorId = "",
|
||||
lastEditActorDisplayName = ""
|
||||
)
|
||||
return entity
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@email.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package ru.f7cloud.talk.data.database.migrations
|
||||
|
||||
import androidx.room.Room
|
||||
import androidx.room.testing.MigrationTestHelper
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import ru.f7cloud.talk.data.source.local.Migrations
|
||||
import ru.f7cloud.talk.data.source.local.TalkDatabase
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.io.IOException
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MigrationsTest {
|
||||
companion object {
|
||||
private const val TEST_DB = "migration-test"
|
||||
private const val INIT_VERSION = 10 // last version before update to offline first
|
||||
private val TAG = MigrationsTest::class.java.simpleName
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val helper: MigrationTestHelper = MigrationTestHelper(
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
TalkDatabase::class.java
|
||||
)
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
@Suppress("SpreadOperator")
|
||||
fun migrateAll() {
|
||||
helper.createDatabase(TEST_DB, INIT_VERSION).apply {
|
||||
close()
|
||||
}
|
||||
|
||||
Room.databaseBuilder(
|
||||
InstrumentationRegistry.getInstrumentation().targetContext,
|
||||
TalkDatabase::class.java,
|
||||
TEST_DB
|
||||
).addMigrations(*TalkDatabase.MIGRATIONS).build().apply {
|
||||
openHelper.writableDatabase.close()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun migrate10To11() {
|
||||
helper.createDatabase(TEST_DB, 10).apply {
|
||||
close()
|
||||
}
|
||||
helper.runMigrationsAndValidate(TEST_DB, 11, true, Migrations.MIGRATION_10_11)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun migrate11To12() {
|
||||
helper.createDatabase(TEST_DB, 11).apply {
|
||||
close()
|
||||
}
|
||||
helper.runMigrationsAndValidate(TEST_DB, 12, true, Migrations.MIGRATION_11_12)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun migrate12To13() {
|
||||
helper.createDatabase(TEST_DB, 12).apply {
|
||||
close()
|
||||
}
|
||||
helper.runMigrationsAndValidate(TEST_DB, 13, true, Migrations.MIGRATION_12_13)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun migrate13To14() {
|
||||
helper.createDatabase(TEST_DB, 13).apply {
|
||||
close()
|
||||
}
|
||||
helper.runMigrationsAndValidate(TEST_DB, 14, true, Migrations.MIGRATION_13_14)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun migrate14To15() {
|
||||
helper.createDatabase(TEST_DB, 14).apply {
|
||||
close()
|
||||
}
|
||||
helper.runMigrationsAndValidate(TEST_DB, 15, true, Migrations.MIGRATION_14_15)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun migrate15To16() {
|
||||
helper.createDatabase(TEST_DB, 15).apply {
|
||||
close()
|
||||
}
|
||||
helper.runMigrationsAndValidate(TEST_DB, 16, true, Migrations.MIGRATION_15_16)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun migrate17To19() {
|
||||
helper.createDatabase(TEST_DB, 17).apply {
|
||||
close()
|
||||
}
|
||||
helper.runMigrationsAndValidate(TEST_DB, 19, true, Migrations.MIGRATION_17_19)
|
||||
}
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2021 Tim Krüger <t@timkrueger.me>
|
||||
* SPDX-FileCopyrightText: 2020 Tobias Kaminsky <tobias@kaminsky.me>
|
||||
* SPDX-FileCopyrightText: 2020 F7cloud
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.ui;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import ru.f7cloud.talk.R;
|
||||
import ru.f7cloud.talk.activities.MainActivity;
|
||||
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.test.core.app.ActivityScenario;
|
||||
import androidx.test.espresso.NoMatchingViewException;
|
||||
import androidx.test.espresso.web.webdriver.DriverAtoms;
|
||||
import androidx.test.espresso.web.webdriver.Locator;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.action.ViewActions.typeText;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static androidx.test.espresso.web.sugar.Web.onWebView;
|
||||
import static androidx.test.espresso.web.webdriver.DriverAtoms.findElement;
|
||||
import static androidx.test.espresso.web.webdriver.DriverAtoms.webClick;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
|
||||
//@LargeTest
|
||||
@Ignore("This test is ignored because it constantly fails on CI")
|
||||
public class LoginIT {
|
||||
|
||||
@Test
|
||||
public void login() throws InterruptedException {
|
||||
|
||||
ActivityScenario<MainActivity> activityScenario = ActivityScenario.launch(MainActivity.class);
|
||||
|
||||
Bundle arguments = androidx.test.platform.app.InstrumentationRegistry.getArguments();
|
||||
|
||||
String baseUrl = arguments.getString("TEST_SERVER_URL");
|
||||
String loginName = arguments.getString("TEST_SERVER_USERNAME");
|
||||
String password = arguments.getString("TEST_SERVER_PASSWORD");
|
||||
|
||||
Thread.sleep(2000);
|
||||
|
||||
try {
|
||||
onView(withId(R.id.serverEntryTextInputEditText)).check(matches(isDisplayed()));
|
||||
} catch (NoMatchingViewException e) {
|
||||
try {
|
||||
// can happen that an invalid account from previous tests is existing
|
||||
onView(withText(R.string.nc_settings_remove_account)).perform(click());
|
||||
Thread.sleep(2000);
|
||||
} catch (NoMatchingViewException ie) {
|
||||
// is OK if the dialog is not shown
|
||||
}
|
||||
|
||||
try {
|
||||
// Delete account if exists
|
||||
onView(withId(R.id.switch_account_button)).perform(click());
|
||||
onView(withId(R.id.settings_remove_account)).perform(click());
|
||||
onView(withText(R.string.nc_settings_remove)).perform(click());
|
||||
// The remove button must be clicked two times
|
||||
onView(withId(R.id.settings_remove_account)).perform(click());
|
||||
// And yes: The button must be clicked two times
|
||||
onView(withText(R.string.nc_settings_remove)).perform(click());
|
||||
onView(withText(R.string.nc_settings_remove)).perform(click());
|
||||
} catch (Exception ie2) {
|
||||
// ignore
|
||||
} finally {
|
||||
Thread.sleep(2000);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onView(withId(R.id.serverEntryTextInputEditText)).perform(typeText(baseUrl));
|
||||
// Click on EditText's drawable right
|
||||
onView(withContentDescription(R.string.nc_server_connect)).perform(click());
|
||||
|
||||
Thread.sleep(4000);
|
||||
|
||||
onWebView().forceJavascriptEnabled();
|
||||
|
||||
// click on login
|
||||
onWebView()
|
||||
.withElement(findElement(Locator.XPATH, "//p[@id='redirect-link']/a"))
|
||||
.perform(webClick());
|
||||
|
||||
// username
|
||||
onWebView()
|
||||
.withElement(findElement(Locator.XPATH, "//input[@id='user']"))
|
||||
.perform(DriverAtoms.webKeys(loginName));
|
||||
|
||||
// password
|
||||
onWebView()
|
||||
.withElement(findElement(Locator.XPATH, "//input[@id='password']"))
|
||||
.perform(DriverAtoms.webKeys(password));
|
||||
|
||||
// click login
|
||||
onWebView()
|
||||
.withElement(findElement(Locator.XPATH, "//input[@id='submit-form']"))
|
||||
.perform(webClick());
|
||||
|
||||
Thread.sleep(2000);
|
||||
|
||||
// grant access
|
||||
onWebView()
|
||||
.withElement(findElement(Locator.XPATH, "//input[@type='submit']"))
|
||||
.perform(webClick());
|
||||
|
||||
Thread.sleep(5 * 1000);
|
||||
|
||||
onView(withId(R.id.switch_account_button)).perform(click());
|
||||
onView(withId(R.id.user_name)).check(matches(withText("User One")));
|
||||
|
||||
activityScenario.onActivity(activity -> {
|
||||
assertEquals(loginName,
|
||||
Objects.requireNonNull(activity.currentUserProviderOld.getCurrentUser().blockingGet()).getUserId());
|
||||
});
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 F7cloud and F7cloud contributors
|
||||
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package ru.f7cloud.talk.utils
|
||||
|
||||
import android.graphics.Color
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
class ColorGeneratorTest {
|
||||
|
||||
@Test
|
||||
fun testUsernameToColor() {
|
||||
usernameToColorHexHelper("", "#0082c9")
|
||||
usernameToColorHexHelper(",", "#1e78c1")
|
||||
usernameToColorHexHelper(".", "#c98879")
|
||||
usernameToColorHexHelper("admin", "#d09e6d")
|
||||
usernameToColorHexHelper("123e4567-e89b-12d3-a456-426614174000", "#bc5c91")
|
||||
usernameToColorHexHelper("Akeel Robertson", "#9750a4")
|
||||
usernameToColorHexHelper("Brayden Truong", "#d09e6d")
|
||||
usernameToColorHexHelper("Daphne Roy", "#9750a4")
|
||||
usernameToColorHexHelper("Ellena Wright Frederic Conway", "#c37285")
|
||||
usernameToColorHexHelper("Gianluca Hills", "#d6b461")
|
||||
usernameToColorHexHelper("Haseeb Stephens", "#d6b461")
|
||||
usernameToColorHexHelper("Idris Mac", "#9750a4")
|
||||
usernameToColorHexHelper("Kristi Fisher", "#0082c9")
|
||||
usernameToColorHexHelper("Lillian Wall", "#bc5c91")
|
||||
usernameToColorHexHelper("Lorelai Taylor", "#ddcb55")
|
||||
usernameToColorHexHelper("Madina Knight", "#9750a4")
|
||||
usernameToColorHexHelper("Meeting", "#c98879")
|
||||
usernameToColorHexHelper("Private Circle", "#c37285")
|
||||
usernameToColorHexHelper("Rae Hope", "#795aab")
|
||||
usernameToColorHexHelper("Santiago Singleton", "#bc5c91")
|
||||
usernameToColorHexHelper("Sid Combs", "#d09e6d")
|
||||
usernameToColorHexHelper("TestCircle", "#499aa2")
|
||||
usernameToColorHexHelper("Tom Mörtel", "#248eb5")
|
||||
usernameToColorHexHelper("Vivienne Jacobs", "#1e78c1")
|
||||
usernameToColorHexHelper("Zaki Cortes", "#6ea68f")
|
||||
usernameToColorHexHelper("a user", "#5b64b3")
|
||||
usernameToColorHexHelper("admin@cloud.example.com", "#9750a4")
|
||||
usernameToColorHexHelper("another user", "#ddcb55")
|
||||
usernameToColorHexHelper("asd", "#248eb5")
|
||||
usernameToColorHexHelper("bar", "#0082c9")
|
||||
usernameToColorHexHelper("foo", "#d09e6d")
|
||||
usernameToColorHexHelper("wasd", "#b6469d")
|
||||
usernameToColorHexHelper("مرحبا بالعالم", "#c98879")
|
||||
usernameToColorHexHelper("🙈", "#b6469d")
|
||||
}
|
||||
|
||||
private fun usernameToColorHexHelper(username: String, expectedHexColor: String) {
|
||||
val userColorInt = ColorGenerator.usernameToColor(username) // returns Int
|
||||
val userHexColor = intToHex(userColorInt)
|
||||
|
||||
Assert.assertEquals(expectedHexColor.lowercase(), userHexColor.lowercase())
|
||||
}
|
||||
|
||||
private fun intToHex(colorInt: Int): String {
|
||||
val r = Color.red(colorInt)
|
||||
val g = Color.green(colorInt)
|
||||
val b = Color.blue(colorInt)
|
||||
return String.format("#%02x%02x%02x", r, g, b)
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2021 Tobias Kaminsky <tobias@kaminsky.me>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.utils
|
||||
|
||||
import at.bitfire.dav4jvm.HttpUtils
|
||||
import org.apache.commons.lang3.time.DateUtils
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
@Ignore("Test fails on CI server. See issue https://github.com/nextcloud/talk-android/issues/1737")
|
||||
class ShareUtilsIT {
|
||||
@Test
|
||||
fun date() {
|
||||
assertEquals(TEST_DATE_IN_MILLIS, parseDate2("Mon, 09 Apr 2008 23:55:38 GMT").time)
|
||||
assertEquals(TEST_DATE_IN_MILLIS, HttpUtils.parseDate("Mon, 09 Apr 2008 23:55:38 GMT")?.time)
|
||||
}
|
||||
|
||||
private fun parseDate2(dateStr: String): Date =
|
||||
DateUtils.parseDate(
|
||||
dateStr, Locale.US,
|
||||
HttpUtils.httpDateFormatStr,
|
||||
// RFC 822, updated by RFC 1123 with any TZ
|
||||
"EEE, dd MMM yyyy HH:mm:ss zzz",
|
||||
// RFC 850, obsoleted by RFC 1036 with any TZ.
|
||||
"EEEE, dd-MMM-yy HH:mm:ss zzz",
|
||||
// ANSI C's asctime() format
|
||||
"EEE MMM d HH:mm:ss yyyy",
|
||||
// Alternative formats.
|
||||
"EEE, dd-MMM-yyyy HH:mm:ss z",
|
||||
"EEE, dd-MMM-yyyy HH-mm-ss z",
|
||||
"EEE, dd MMM yy HH:mm:ss z",
|
||||
"EEE dd-MMM-yyyy HH:mm:ss z",
|
||||
"EEE dd MMM yyyy HH:mm:ss z",
|
||||
"EEE dd-MMM-yyyy HH-mm-ss z",
|
||||
"EEE dd-MMM-yy HH:mm:ss z",
|
||||
"EEE dd MMM yy HH:mm:ss z",
|
||||
"EEE,dd-MMM-yy HH:mm:ss z",
|
||||
"EEE,dd-MMM-yyyy HH:mm:ss z",
|
||||
"EEE, dd-MM-yyyy HH:mm:ss z",
|
||||
// RI bug 6641315 claims a cookie of this format was once served by www.yahoo.com
|
||||
"EEE MMM d yyyy HH:mm:ss z"
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val TEST_DATE_IN_MILLIS = 1207778138000
|
||||
}
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Samanwith KSN <samanwith21@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.utils
|
||||
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.assertFalse
|
||||
import junit.framework.TestCase.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class UriUtilsIT {
|
||||
|
||||
@Test
|
||||
fun testHasHttpProtocolPrefixed() {
|
||||
val uriHttp = "http://www.example.com"
|
||||
val resultHttp = UriUtils.hasHttpProtocolPrefixed(uriHttp)
|
||||
assertTrue(resultHttp)
|
||||
|
||||
val uriHttps = "https://www.example.com"
|
||||
val resultHttps = UriUtils.hasHttpProtocolPrefixed(uriHttps)
|
||||
assertTrue(resultHttps)
|
||||
|
||||
val uriWithoutPrefix = "www.example.com"
|
||||
val resultWithoutPrefix = UriUtils.hasHttpProtocolPrefixed(uriWithoutPrefix)
|
||||
assertFalse(resultWithoutPrefix)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExtractInstanceInternalFileFileId() {
|
||||
assertEquals(
|
||||
"42",
|
||||
UriUtils.extractInstanceInternalFileFileId(
|
||||
"https://cloud.f7cloud.com/apps/files/?dir=/Engineering&fileid=42"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExtractInstanceInternalFileShareFileId() {
|
||||
assertEquals(
|
||||
"42",
|
||||
UriUtils.extractInstanceInternalFileShareFileId("https://cloud.f7cloud.com/f/42")
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIsInstanceInternalFileShareUrl() {
|
||||
assertTrue(
|
||||
UriUtils.isInstanceInternalFileShareUrl(
|
||||
"https://cloud.f7cloud.com",
|
||||
"https://cloud.f7cloud.com/f/42"
|
||||
)
|
||||
)
|
||||
|
||||
assertFalse(
|
||||
UriUtils.isInstanceInternalFileShareUrl(
|
||||
"https://f7cloud.f7cloud.com",
|
||||
"https://cloud.f7cloud.com/f/42"
|
||||
)
|
||||
)
|
||||
|
||||
assertFalse(
|
||||
UriUtils.isInstanceInternalFileShareUrl(
|
||||
"https://f7cloud.f7cloud.com",
|
||||
"https://cloud.f7cloud.com/f/"
|
||||
)
|
||||
)
|
||||
|
||||
assertFalse(
|
||||
UriUtils.isInstanceInternalFileShareUrl(
|
||||
"https://f7cloud.f7cloud.com",
|
||||
"https://cloud.f7cloud.com/f/test123"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIsInstanceInternalFileUrl() {
|
||||
assertTrue(
|
||||
UriUtils.isInstanceInternalFileUrl(
|
||||
"https://cloud.f7cloud.com",
|
||||
"https://cloud.f7cloud.com/apps/files/?dir=/Engineering&fileid=41"
|
||||
)
|
||||
)
|
||||
|
||||
assertFalse(
|
||||
UriUtils.isInstanceInternalFileUrl(
|
||||
"https://f7cloud.f7cloud.com",
|
||||
"https://cloud.f7cloud.com/apps/files/?dir=/Engineering&fileid=41"
|
||||
)
|
||||
)
|
||||
|
||||
assertFalse(
|
||||
UriUtils.isInstanceInternalFileUrl(
|
||||
"https://f7cloud.f7cloud.com",
|
||||
"https://cloud.f7cloud.com/apps/files/?dir=/Engineering&fileid=test123"
|
||||
)
|
||||
)
|
||||
|
||||
assertFalse(
|
||||
UriUtils.isInstanceInternalFileUrl(
|
||||
"https://f7cloud.f7cloud.com",
|
||||
"https://cloud.f7cloud.com/apps/files/?dir=/Engineering&fileid="
|
||||
)
|
||||
)
|
||||
|
||||
assertFalse(
|
||||
UriUtils.isInstanceInternalFileUrl(
|
||||
"https://cloud.f7cloud.com",
|
||||
"https://cloud.f7cloud.com/apps/files/?dir=/Engineering"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIsInstanceInternalFileUrlNew() {
|
||||
assertTrue(
|
||||
UriUtils.isInstanceInternalFileUrlNew(
|
||||
"https://cloud.f7cloud.com",
|
||||
"https://cloud.f7cloud.com/apps/files/files/41?dir=/"
|
||||
)
|
||||
)
|
||||
|
||||
assertFalse(
|
||||
UriUtils.isInstanceInternalFileUrlNew(
|
||||
"https://f7cloud.f7cloud.com",
|
||||
"https://cloud.f7cloud.com/apps/files/files/41?dir=/"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExtractInstanceInternalFileFileIdNew() {
|
||||
assertEquals(
|
||||
"42",
|
||||
UriUtils.extractInstanceInternalFileFileIdNew("https://cloud.f7cloud.com/apps/files/files/42?dir=/")
|
||||
)
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Samanwith KSN <samanwith21@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.junit.MockitoJUnitRunner
|
||||
|
||||
@RunWith(MockitoJUnitRunner::class)
|
||||
class VibrationUtilsTest {
|
||||
|
||||
@Mock
|
||||
private lateinit var mockContext: Context
|
||||
|
||||
@Mock
|
||||
private lateinit var mockVibrator: Vibrator
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
Mockito.`when`(mockContext.getSystemService(Context.VIBRATOR_SERVICE)).thenReturn(mockVibrator)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testVibrateShort() {
|
||||
VibrationUtils.vibrateShort(mockContext)
|
||||
Mockito.verify(mockVibrator)
|
||||
.vibrate(
|
||||
VibrationEffect
|
||||
.createOneShot(
|
||||
VibrationUtils.SHORT_VIBRATE,
|
||||
VibrationEffect.DEFAULT_AMPLITUDE
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<application>
|
||||
<activity
|
||||
android:name="ru.f7cloud.talk.activities.MainActivity"
|
||||
tools:node="remove" />
|
||||
</application>
|
||||
</manifest>
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* F7cloud embed — sync F7cloud AuthSession into talk-android User DB.
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.f7cloud
|
||||
|
||||
import android.util.Log
|
||||
import androidx.work.Data
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import autodagger.AutoInjector
|
||||
import com.bluelinelabs.logansquare.LoganSquare
|
||||
import ru.f7cloud.talk.api.NcApi
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication
|
||||
import ru.f7cloud.talk.jobs.SignalingSettingsWorker
|
||||
import ru.f7cloud.talk.users.UserManager
|
||||
import ru.f7cloud.talk.utils.ApiUtils
|
||||
import ru.f7cloud.talk.utils.bundle.BundleKeys
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(F7cloudTalkApplication::class)
|
||||
class F7TalkUserSync {
|
||||
@Inject lateinit var userManager: UserManager
|
||||
@Inject lateinit var ncApi: NcApi
|
||||
|
||||
fun ensureUser(
|
||||
serverUrl: String,
|
||||
username: String,
|
||||
appPassword: String,
|
||||
davUserId: String?,
|
||||
): Long? {
|
||||
val baseUrl = serverUrl.trimEnd('/')
|
||||
val credentials = ApiUtils.getCredentials(username, appPassword)
|
||||
if (credentials.isNullOrBlank()) {
|
||||
Log.w(TAG, "Missing credentials for $username")
|
||||
return null
|
||||
}
|
||||
|
||||
val existingId = findExistingUserId(username, baseUrl)
|
||||
if (existingId != null && hasCapabilities(existingId)) {
|
||||
refreshTokenIfNeeded(existingId, appPassword)
|
||||
userManager.setUserAsActive(userManager.getUserWithId(existingId).blockingGet()).blockingGet()
|
||||
enqueueSignalingSettings(existingId)
|
||||
Log.d(TAG, "User already synced: $username@$baseUrl")
|
||||
return existingId
|
||||
}
|
||||
|
||||
val capabilities = ncApi
|
||||
.getCapabilities(credentials, ApiUtils.getUrlForCapabilities(baseUrl))
|
||||
.blockingFirst()
|
||||
val profile = ncApi
|
||||
.getUserProfile(credentials, ApiUtils.getUrlForUserProfile(baseUrl))
|
||||
.blockingFirst()
|
||||
|
||||
val profileData = profile.ocs?.data
|
||||
val capsData = capabilities.ocs?.data
|
||||
if (capsData?.capabilities == null || capsData.serverVersion == null) {
|
||||
Log.w(TAG, "Capabilities response incomplete for $baseUrl")
|
||||
return null
|
||||
}
|
||||
|
||||
val displayName = profileData?.displayName?.takeIf { it.isNotBlank() }
|
||||
?: profileData?.displayNameAlt?.takeIf { it.isNotBlank() }
|
||||
?: username
|
||||
val userId = profileData?.userId?.takeIf { it.isNotBlank() }
|
||||
?: davUserId?.takeIf { it.isNotBlank() }
|
||||
?: username
|
||||
|
||||
val user = userManager.storeProfile(
|
||||
username,
|
||||
UserManager.UserAttributes(
|
||||
id = existingId,
|
||||
serverUrl = baseUrl,
|
||||
currentUser = true,
|
||||
userId = userId,
|
||||
token = appPassword,
|
||||
displayName = displayName,
|
||||
pushConfigurationState = null,
|
||||
capabilities = LoganSquare.serialize(capsData.capabilities),
|
||||
serverVersion = LoganSquare.serialize(capsData.serverVersion),
|
||||
certificateAlias = null,
|
||||
externalSignalingServer = null,
|
||||
),
|
||||
).blockingGet()
|
||||
|
||||
val internalId = user.id
|
||||
if (internalId != null) {
|
||||
enqueueSignalingSettings(internalId)
|
||||
}
|
||||
Log.d(TAG, "Synced talk user id=$internalId for $username@$baseUrl")
|
||||
return internalId
|
||||
}
|
||||
|
||||
private fun enqueueSignalingSettings(internalUserId: Long) {
|
||||
val app = F7cloudTalkApplication.sharedApplication ?: return
|
||||
val data = Data.Builder()
|
||||
.putLong(BundleKeys.KEY_INTERNAL_USER_ID, internalUserId)
|
||||
.build()
|
||||
val work = OneTimeWorkRequest.Builder(SignalingSettingsWorker::class.java)
|
||||
.setInputData(data)
|
||||
.build()
|
||||
WorkManager.getInstance(app).enqueue(work)
|
||||
Log.d(TAG, "Enqueued SignalingSettingsWorker for user $internalUserId (HPB)")
|
||||
}
|
||||
|
||||
private fun findExistingUserId(username: String, baseUrl: String): Long? =
|
||||
userManager.users.blockingGet()
|
||||
.firstOrNull { it.username == username && it.baseUrl == baseUrl && !it.scheduledForDeletion }
|
||||
?.id
|
||||
|
||||
private fun hasCapabilities(userId: Long): Boolean {
|
||||
val user = userManager.getUserWithId(userId).blockingGet()
|
||||
return user.capabilities?.spreedCapability != null
|
||||
}
|
||||
|
||||
private fun refreshTokenIfNeeded(userId: Long, token: String) {
|
||||
val user = userManager.getUserWithId(userId).blockingGet()
|
||||
if (user.token != token) {
|
||||
user.token = token
|
||||
userManager.saveUser(user).blockingGet()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "F7TalkUserSync"
|
||||
|
||||
@Volatile
|
||||
private var instance: F7TalkUserSync? = null
|
||||
|
||||
fun get(): F7TalkUserSync {
|
||||
val app = F7cloudTalkApplication.sharedApplication
|
||||
?: error("F7cloudTalkApplication not initialized")
|
||||
return instance ?: synchronized(this) {
|
||||
instance ?: F7TalkUserSync().also {
|
||||
app.componentApplication.inject(it)
|
||||
instance = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- F7cloud embed: branding (signaling/HPB comes from server capabilities). -->
|
||||
<resources>
|
||||
<string name="nc_app_name">F7cloud Talk</string>
|
||||
<string name="nc_app_product_name">F7cloud Talk</string>
|
||||
<string name="nc_server_product_name">F7cloud</string>
|
||||
<string name="nc_talk_login_scheme" translatable="false">f7</string>
|
||||
<string name="nc_import_accounts_from" translatable="false"></string>
|
||||
<string name="nc_import_account_type" translatable="false">f7cloud</string>
|
||||
<string name="osm_geocoder_contact" translatable="false">android@f7cloud.ru</string>
|
||||
<string name="nc_privacy_url" translatable="false">https://f7cloud.ru/</string>
|
||||
<bool name="hide_provider">true</bool>
|
||||
<bool name="multiaccount_support">false</bool>
|
||||
</resources>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<application>
|
||||
<activity
|
||||
android:name="ru.f7cloud.talk.activities.MainActivity"
|
||||
tools:node="remove" />
|
||||
</application>
|
||||
</manifest>
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
Birebir ya da grup ile ses ya da görüntü çağrıları yapmak, web konferansları oluşturmak ve sohbet iletileri göndermek için Nextcloud Talk uygulamasını kullanabilisiniz. Tüm bilgiler tamamen şifrelenmiş olarak sizin sunucunuz üzerinden iletilerek olabilecek en yüksek gizlilik düzeyi sağlanır.
|
||||
|
||||
Nextcloud Talk uygulamasının kullanımı kolaydır ve herhangi bir ücret ödemeniz gerekmez!
|
||||
|
||||
Nextcloud Talk şu özellikleri destekler:
|
||||
* HD (H.265) ses ve görüntü çağrıları
|
||||
* Grup ve birebir çağrılar
|
||||
* Webinar ve herkese açık web toplantıları
|
||||
* Bireysel ve grup sohbetleri
|
||||
* Kolay ekran paylaşımı
|
||||
* Android ve iOS uygulamaları
|
||||
* Mobil çağrı ve anında sohbet bildirimleri
|
||||
* Nextcloud Files ve Nextcloud Groupware ile bütünleşme
|
||||
* Kendi veri merkezinizde, %100 açık kaynaklı
|
||||
* Uçtan uca şifrelenmiş çağrılar
|
||||
* Milyonlarca kullanıcıya göre
|
||||
* SIP geçidi üzerinden telefon ile arama
|
||||
|
||||
Nextcloud Talk uygulamasının kullanılabilmesi için Nextcloud Talk sunucusu gereklidir. Nextcloud verilerinizin kontrolunu yeniden size veren, size özel, kendi sunucularınızda barındırabileceğiniz bir dosya eşitleme ve iletişim platformudur. Evinizde, hizmet sağlayıcınızda ya da kurumunuzda bulunan bir sunucu üzerinde kullanarak, belgelerinize, takvimlerinize, kişilerinize, e-postalarınıza ve diğer verilerinize erişmenizi sağlar. Verilerinizi farklı Nextcloud sunucuları kullanan kişiler ile paylaşarak dosyalar üzerinde birlikte çalışabilirsiniz. Nextcloud tamamen açık kaynaklı olduğundan özelliklerini gereksinimlerinize göre genişletebilir, geliştirilmesine katkıda bulunabilir ya da vaatlerimizin doğruluğunu görebilirsiniz.
|
||||
|
||||
|
||||
Dünya üzerinde milyonlarca kullanıcı ev ya da iş yerlerinde günlük işlemleri için Nextcloud kullanıyor. Kurumsal kullanıcılar Nextcloud GmbH tarafından sunulan destek hizmetleri ile tamamen kendi BT bölümlerinin denetimindeki kurumsal üretim ve iş birliği platformu için eksiksiz destek alıyorlar.
|
||||
|
||||
|
||||
Ayrıntılı bilgi almak için https://nextcloud.com/talk
|
||||
|
||||
Nextcloud web sitesi https://nextcloud.com
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.utils;
|
||||
|
||||
|
||||
import ru.f7cloud.talk.interfaces.ClosedInterface;
|
||||
|
||||
public class ClosedInterfaceImpl implements ClosedInterface {
|
||||
@Override
|
||||
public void providerInstallerInstallIfNeededAsync() {
|
||||
// does absolutely nothing :)
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGooglePlayServicesAvailable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUpPushTokenRegistration() {
|
||||
// no push notifications for generic build variant
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- F7cloud embed: strip Talk standalone launcher; host app is F7cloud Mobile. -->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<application>
|
||||
<activity
|
||||
android:name="ru.f7cloud.talk.activities.MainActivity"
|
||||
tools:node="remove" />
|
||||
</application>
|
||||
</manifest>
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.jobs
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.work.Data
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import autodagger.AutoInjector
|
||||
import com.google.android.gms.tasks.OnCompleteListener
|
||||
import com.google.firebase.messaging.FirebaseMessaging
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication
|
||||
import ru.f7cloud.talk.utils.preferences.AppPreferences
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(F7cloudTalkApplication::class)
|
||||
class GetFirebasePushTokenWorker(val context: Context, workerParameters: WorkerParameters) :
|
||||
Worker(context, workerParameters) {
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
@SuppressLint("LongLogTag")
|
||||
override fun doWork(): Result {
|
||||
F7cloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
|
||||
|
||||
FirebaseMessaging.getInstance().token.addOnCompleteListener(
|
||||
OnCompleteListener { task ->
|
||||
if (!task.isSuccessful) {
|
||||
Log.w(TAG, "Fetching FCM registration token failed", task.exception)
|
||||
return@OnCompleteListener
|
||||
}
|
||||
|
||||
val pushToken = task.result
|
||||
Log.d(TAG, "Fetched firebase push token is: $pushToken")
|
||||
|
||||
appPreferences.pushToken = pushToken
|
||||
appPreferences.pushTokenLatestFetch = System.currentTimeMillis()
|
||||
|
||||
val data: Data =
|
||||
Data.Builder().putString(PushRegistrationWorker.ORIGIN, "GetFirebasePushTokenWorker").build()
|
||||
val pushRegistrationWork = OneTimeWorkRequest.Builder(PushRegistrationWorker::class.java)
|
||||
.setInputData(data)
|
||||
.build()
|
||||
WorkManager.getInstance(context).enqueue(pushRegistrationWork)
|
||||
}
|
||||
)
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = GetFirebasePushTokenWorker::class.simpleName
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Tim Krüger <t@timkrueger.me>
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2017-2019 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.services.firebase
|
||||
|
||||
import android.util.Log
|
||||
import androidx.work.Data
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import autodagger.AutoInjector
|
||||
import com.google.firebase.messaging.FirebaseMessagingService
|
||||
import com.google.firebase.messaging.RemoteMessage
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication.Companion.sharedApplication
|
||||
import ru.f7cloud.talk.jobs.NotificationWorker
|
||||
import ru.f7cloud.talk.jobs.PushRegistrationWorker
|
||||
import ru.f7cloud.talk.utils.bundle.BundleKeys
|
||||
import ru.f7cloud.talk.utils.preferences.AppPreferences
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(F7cloudTalkApplication::class)
|
||||
class NCFirebaseMessagingService : FirebaseMessagingService() {
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
override fun onCreate() {
|
||||
Log.d(TAG, "onCreate")
|
||||
super.onCreate()
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
}
|
||||
|
||||
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
||||
Log.d(TAG, "onMessageReceived")
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
|
||||
Log.d(TAG, "remoteMessage.priority: " + remoteMessage.priority)
|
||||
Log.d(TAG, "remoteMessage.originalPriority: " + remoteMessage.originalPriority)
|
||||
|
||||
val data = remoteMessage.data
|
||||
val subject = data[KEY_NOTIFICATION_SUBJECT]
|
||||
val signature = data[KEY_NOTIFICATION_SIGNATURE]
|
||||
|
||||
if (!subject.isNullOrEmpty() && !signature.isNullOrEmpty()) {
|
||||
val messageData = Data.Builder()
|
||||
.putString(BundleKeys.KEY_NOTIFICATION_SUBJECT, subject)
|
||||
.putString(BundleKeys.KEY_NOTIFICATION_SIGNATURE, signature)
|
||||
.build()
|
||||
val notificationWork =
|
||||
OneTimeWorkRequest.Builder(NotificationWorker::class.java).setInputData(messageData)
|
||||
.build()
|
||||
WorkManager.getInstance().enqueue(notificationWork)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewToken(token: String) {
|
||||
super.onNewToken(token)
|
||||
Log.d(TAG, "onNewToken. token = $token")
|
||||
|
||||
appPreferences.pushToken = token
|
||||
appPreferences.pushTokenLatestGeneration = System.currentTimeMillis()
|
||||
|
||||
val data: Data =
|
||||
Data.Builder().putString(PushRegistrationWorker.ORIGIN, "NCFirebaseMessagingService#onNewToken").build()
|
||||
val pushRegistrationWork = OneTimeWorkRequest.Builder(PushRegistrationWorker::class.java)
|
||||
.setInputData(data)
|
||||
.build()
|
||||
WorkManager.getInstance().enqueue(pushRegistrationWork)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = NCFirebaseMessagingService::class.simpleName
|
||||
const val KEY_NOTIFICATION_SUBJECT = "subject"
|
||||
const val KEY_NOTIFICATION_SIGNATURE = "signature"
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2017-2019 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.utils
|
||||
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.PeriodicWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import autodagger.AutoInjector
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
import com.google.android.gms.security.ProviderInstaller
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication
|
||||
import ru.f7cloud.talk.interfaces.ClosedInterface
|
||||
import ru.f7cloud.talk.jobs.GetFirebasePushTokenWorker
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@AutoInjector(F7cloudTalkApplication::class)
|
||||
class ClosedInterfaceImpl :
|
||||
ClosedInterface,
|
||||
ProviderInstaller.ProviderInstallListener {
|
||||
|
||||
override val isGooglePlayServicesAvailable: Boolean = isGPlayServicesAvailable()
|
||||
|
||||
override fun providerInstallerInstallIfNeededAsync() {
|
||||
F7cloudTalkApplication.sharedApplication?.let {
|
||||
ProviderInstaller.installIfNeededAsync(
|
||||
it.applicationContext,
|
||||
this
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onProviderInstalled() {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onProviderInstallFailed(p0: Int, p1: Intent?) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
private fun isGPlayServicesAvailable(): Boolean {
|
||||
val api = GoogleApiAvailability.getInstance()
|
||||
val code =
|
||||
F7cloudTalkApplication.sharedApplication?.let {
|
||||
api.isGooglePlayServicesAvailable(it.applicationContext)
|
||||
}
|
||||
return if (code == ConnectionResult.SUCCESS) {
|
||||
true
|
||||
} else {
|
||||
Log.w(TAG, "GooglePlayServices are not available. Code:$code")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun setUpPushTokenRegistration() {
|
||||
val firebasePushTokenWorker = OneTimeWorkRequest.Builder(GetFirebasePushTokenWorker::class.java).build()
|
||||
WorkManager.getInstance().enqueue(firebasePushTokenWorker)
|
||||
|
||||
setUpPeriodicTokenRefreshFromFCM()
|
||||
}
|
||||
|
||||
private fun setUpPeriodicTokenRefreshFromFCM() {
|
||||
val periodicTokenRefreshFromFCM = PeriodicWorkRequest.Builder(
|
||||
GetFirebasePushTokenWorker::class.java,
|
||||
DAILY,
|
||||
TimeUnit.HOURS,
|
||||
FLEX_INTERVAL,
|
||||
TimeUnit.HOURS
|
||||
).build()
|
||||
|
||||
WorkManager.getInstance()
|
||||
.enqueueUniquePeriodicWork(
|
||||
"periodicTokenRefreshFromFCM",
|
||||
ExistingPeriodicWorkPolicy.UPDATE,
|
||||
periodicTokenRefreshFromFCM
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = ClosedInterfaceImpl::class.java.simpleName
|
||||
const val DAILY: Long = 24
|
||||
const val FLEX_INTERVAL: Long = 10
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
<!--
|
||||
~ F7cloud Talk - Android Client
|
||||
~
|
||||
~ SPDX-FileCopyrightText: 2017-2024 F7cloud and F7cloud contributors
|
||||
~ SPDX-FileCopyrightText: 2021-2023 Marcel Hibbe <dev@mhibbe.de>
|
||||
~ SPDX-FileCopyrightText: 2017-2019 Mario Danic <mario@lovelyhq.com>
|
||||
~ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera.autofocus"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:glEsVersion="0x00020000"
|
||||
android:required="true" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.AUTHENTICATE_ACCOUNTS"
|
||||
android:maxSdkVersion="22" />
|
||||
<uses-permission
|
||||
android:name="android.permission.BLUETOOTH"
|
||||
android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission
|
||||
android:name="android.permission.GET_ACCOUNTS"
|
||||
android:maxSdkVersion="22" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
|
||||
<uses-permission android:name="android.permission.READ_PROFILE" />
|
||||
|
||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.USE_CREDENTIALS"
|
||||
android:maxSdkVersion="22" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="29"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<!-- This permission is deprecated in Android P -->
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<permission
|
||||
android:name="${applicationId}.${broadcastPermission}"
|
||||
android:protectionLevel="signature" />
|
||||
<uses-permission android:name="${applicationId}.${broadcastPermission}" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_config"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/nc_app_name"
|
||||
android:largeHeap="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme.Launcher"
|
||||
android:taskAffinity=""
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.car.application"
|
||||
android:resource="@xml/automotive_app_desc"/>
|
||||
|
||||
<meta-data
|
||||
android:name="android.max_aspect"
|
||||
android:value="10" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.MainActivity"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat" />
|
||||
<data android:scheme="content" />
|
||||
<data android:scheme="file" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".account.ServerSelectionActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".account.BrowserLoginActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity android:name=".contacts.ContactsActivity"
|
||||
android:theme="@style/AppTheme"/>
|
||||
|
||||
<activity android:name=".conversationcreation.ConversationCreationActivity"
|
||||
android:theme="@style/AppTheme"/>
|
||||
|
||||
<activity
|
||||
android:name=".account.AccountVerificationActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".account.SwitchAccountActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".chat.ScheduledMessagesActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".conversationlist.ConversationsListActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".chat.ChatActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.CallActivity"
|
||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTask"
|
||||
android:showOnLockScreen="true"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:taskAffinity=".call"
|
||||
android:theme="@style/AppTheme.CallLauncher" />
|
||||
|
||||
<activity
|
||||
android:name=".callnotification.CallNotificationActivity"
|
||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTask"
|
||||
android:showOnLockScreen="true"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:taskAffinity=".call"
|
||||
android:theme="@style/AppTheme.CallLauncher" />
|
||||
|
||||
<activity
|
||||
android:name=".fullscreenfile.FullScreenImageActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:theme="@style/FullScreenImageTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".fullscreenfile.FullScreenMediaActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:theme="@style/FullScreenMediaTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".fullscreenfile.FullScreenTextViewerActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:theme="@style/FullScreenTextTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.TakePhotoActivity"
|
||||
android:theme="@style/TakePhotoTheme"
|
||||
android:windowSoftInputMode="stateHidden" />
|
||||
|
||||
<activity
|
||||
android:name=".shareditems.activities.SharedItemsActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".remotefilebrowser.activities.RemoteFileBrowserActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".messagesearch.MessageSearchActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".location.LocationPickerActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".location.GeocodingActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".translate.ui.TranslateActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".profile.ProfileActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".diagnose.DiagnoseActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".conversationinfo.ConversationInfoActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".conversationinfoedit.ConversationInfoEditActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".openconversations.ListOpenConversationsActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".invitation.InvitationsActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".lock.LockedActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".threadsoverview.ThreadsOverviewActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<receiver
|
||||
android:name=".receivers.PackageReplacedReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".receivers.DirectReplyReceiver" />
|
||||
<receiver android:name=".receivers.MarkAsReadReceiver" />
|
||||
<receiver android:name=".receivers.DismissRecordingAvailableReceiver" />
|
||||
<receiver android:name=".receivers.ShareRecordingToChatReceiver" />
|
||||
|
||||
<service
|
||||
android:name=".utils.SyncService"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/syncadapter" />
|
||||
<meta-data
|
||||
android:name="android.provider.CONTACTS_STRUCTURE"
|
||||
android:resource="@xml/contacts" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".utils.AuthenticatorService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.accounts.AccountAuthenticator" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.accounts.AccountAuthenticator"
|
||||
android:resource="@xml/auth" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".services.CallForegroundService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="microphone|camera" />
|
||||
|
||||
<provider
|
||||
android:name="ru.f7cloud.talk.utils.TalkFileProvider"
|
||||
android:authorities="${applicationId}.talk.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_provider_paths" />
|
||||
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.media.action.VIDEO_CAPTURE" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv='content-Type' content='text/html; charset=UTF-8' />
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
|
||||
<style>
|
||||
html, body, #map {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="map" ></div>
|
||||
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
|
||||
<script>
|
||||
var queryString = window.location.search;
|
||||
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
var locationLat = urlParams.get('locationLat')
|
||||
var locationLon = urlParams.get('locationLon')
|
||||
var locationGeoLink = urlParams.get('locationGeoLink')
|
||||
var mapProviderUrl = urlParams.get('mapProviderUrl')
|
||||
var mapProviderAttribution = urlParams.get('mapProviderAttribution')
|
||||
|
||||
var map = L.map('map', {
|
||||
zoomControl: false,
|
||||
scrollWheelZoom: false
|
||||
}).setView([locationLat, locationLon], 13);
|
||||
map.dragging.disable();
|
||||
|
||||
L.tileLayer(mapProviderUrl, {
|
||||
attribution: '© ' + mapProviderAttribution
|
||||
}).addTo(map);
|
||||
|
||||
L.marker([locationLat, locationLon]).addTo(map);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,2 @@
|
||||
SPDX-FileCopyrightText: 2021 Marcel Hibbe <dev@mhibbe.de>
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Vendored
+296
@@ -0,0 +1,296 @@
|
||||
/*
|
||||
* Nominatim Java API client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2010 - 2014 Dudie
|
||||
* SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
*/
|
||||
package fr.dudie.nominatim.client;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.github.filosganga.geogson.gson.GeometryAdapterFactory;
|
||||
import com.github.filosganga.geogson.jts.JtsAdapterFactory;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import fr.dudie.nominatim.client.request.NominatimLookupRequest;
|
||||
import fr.dudie.nominatim.client.request.NominatimReverseRequest;
|
||||
import fr.dudie.nominatim.client.request.NominatimSearchRequest;
|
||||
import fr.dudie.nominatim.client.request.paramhelper.OsmType;
|
||||
import fr.dudie.nominatim.gson.ArrayOfAddressElementsDeserializer;
|
||||
import fr.dudie.nominatim.gson.ArrayOfPolygonPointsDeserializer;
|
||||
import fr.dudie.nominatim.gson.BoundingBoxDeserializer;
|
||||
import fr.dudie.nominatim.gson.PolygonPointDeserializer;
|
||||
import fr.dudie.nominatim.model.Address;
|
||||
import fr.dudie.nominatim.model.BoundingBox;
|
||||
import fr.dudie.nominatim.model.Element;
|
||||
import fr.dudie.nominatim.model.PolygonPoint;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
/**
|
||||
* An implementation of the Nominatim Api Service.
|
||||
*
|
||||
* @author Jérémie Huchet
|
||||
* @author Sunil D S
|
||||
* @author Andy Scherzinger
|
||||
*/
|
||||
public final class TalkJsonNominatimClient implements NominatimClient {
|
||||
private static final String TAG = "TalkNominationClient";
|
||||
|
||||
/**
|
||||
* UTF-8 encoding.
|
||||
*/
|
||||
public static final String ENCODING_UTF_8 = "UTF-8";
|
||||
|
||||
private final OkHttpClient httpClient;
|
||||
|
||||
/**
|
||||
* Gson instance for Nominatim API calls.
|
||||
*/
|
||||
private final Gson gson;
|
||||
|
||||
/**
|
||||
* The url to make search queries.
|
||||
*/
|
||||
private final String searchUrl;
|
||||
|
||||
/**
|
||||
* The url for reverse geocoding.
|
||||
*/
|
||||
private final String reverseUrl;
|
||||
|
||||
/**
|
||||
* The url for address lookup.
|
||||
*/
|
||||
private final String lookupUrl;
|
||||
|
||||
/**
|
||||
* The default search options.
|
||||
*/
|
||||
private final NominatimOptions defaults;
|
||||
|
||||
/**
|
||||
* Creates the json nominatim client.
|
||||
*
|
||||
* @param baseUrl the nominatim server url
|
||||
* @param httpClient an HTTP client
|
||||
* @param email an email to add in the HTTP requests parameters to "sign" them (see
|
||||
* https://wiki.openstreetmap.org/wiki/Nominatim_usage_policy)
|
||||
*/
|
||||
public TalkJsonNominatimClient(final String baseUrl, final OkHttpClient httpClient, final String email) {
|
||||
this(baseUrl, httpClient, email, new NominatimOptions());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the json nominatim client.
|
||||
*
|
||||
* @param baseUrl the nominatim server url
|
||||
* @param httpClient an HTTP client
|
||||
* @param email an email to add in the HTTP requests parameters to "sign" them (see
|
||||
* https://wiki.openstreetmap.org/wiki/Nominatim_usage_policy)
|
||||
* @param defaults defaults options, they override null valued requests options
|
||||
*/
|
||||
public TalkJsonNominatimClient(final String baseUrl, final OkHttpClient httpClient, final String email, final NominatimOptions defaults) {
|
||||
String emailEncoded;
|
||||
try {
|
||||
emailEncoded = URLEncoder.encode(email, ENCODING_UTF_8);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
emailEncoded = email;
|
||||
}
|
||||
this.searchUrl = String.format("%s/search?format=jsonv2&email=%s", baseUrl.replaceAll("/$", ""), emailEncoded);
|
||||
this.reverseUrl = String.format("%s/reverse?format=jsonv2&email=%s", baseUrl.replaceAll("/$", ""), emailEncoded);
|
||||
this.lookupUrl = String.format("%s/lookup?format=json&email=%s", baseUrl.replaceAll("/$", ""), emailEncoded);
|
||||
|
||||
Log.d(TAG, "API search URL: " + searchUrl);
|
||||
Log.d(TAG, "API reverse URL: " + reverseUrl);
|
||||
|
||||
this.defaults = defaults;
|
||||
|
||||
// prepare gson instance
|
||||
final GsonBuilder gsonBuilder = new GsonBuilder();
|
||||
|
||||
gsonBuilder.registerTypeAdapter(Element[].class, new ArrayOfAddressElementsDeserializer());
|
||||
gsonBuilder.registerTypeAdapter(PolygonPoint.class, new PolygonPointDeserializer());
|
||||
gsonBuilder.registerTypeAdapter(PolygonPoint[].class, new ArrayOfPolygonPointsDeserializer());
|
||||
gsonBuilder.registerTypeAdapter(BoundingBox.class, new BoundingBoxDeserializer());
|
||||
|
||||
gsonBuilder.registerTypeAdapterFactory(new JtsAdapterFactory());
|
||||
gsonBuilder.registerTypeAdapterFactory(new GeometryAdapterFactory());
|
||||
|
||||
gson = gsonBuilder.create();
|
||||
|
||||
// prepare httpclient
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @see fr.dudie.nominatim.client.NominatimClient#search(fr.dudie.nominatim.client.request.NominatimSearchRequest)
|
||||
*/
|
||||
@Override
|
||||
public List<Address> search(final NominatimSearchRequest search) throws IOException {
|
||||
|
||||
defaults.mergeTo(search);
|
||||
final String apiCall = String.format("%s&%s", searchUrl, search.getQueryString());
|
||||
Log.d(TAG, "search url: " + apiCall);
|
||||
|
||||
Request requesthttp = new Request.Builder()
|
||||
.addHeader("accept", "application/json")
|
||||
.url(apiCall)
|
||||
.build();
|
||||
|
||||
Response response = httpClient.newCall(requesthttp).execute();
|
||||
if (response.isSuccessful()) {
|
||||
ResponseBody responseBody = response.body();
|
||||
if (responseBody != null) {
|
||||
return gson.fromJson(responseBody.string(), new TypeToken<List<Address>>() {
|
||||
}.getType());
|
||||
}
|
||||
}
|
||||
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @see fr.dudie.nominatim.client.NominatimClient#getAddress(fr.dudie.nominatim.client.request.NominatimReverseRequest)
|
||||
*/
|
||||
@Override
|
||||
public Address getAddress(final NominatimReverseRequest reverse) throws IOException {
|
||||
|
||||
final String apiCall = String.format("%s&%s", reverseUrl, reverse.getQueryString());
|
||||
Log.d(TAG, "reverse geocoding url: " + apiCall);
|
||||
|
||||
Request requesthttp = new Request.Builder()
|
||||
.addHeader("accept", "application/json")
|
||||
.url(apiCall)
|
||||
.build();
|
||||
|
||||
Response response = httpClient.newCall(requesthttp).execute();
|
||||
if (response.isSuccessful()) {
|
||||
ResponseBody responseBody = response.body();
|
||||
if (responseBody != null) {
|
||||
return gson.fromJson(responseBody.string(), Address.class);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @see fr.dudie.nominatim.client.NominatimClient#lookupAddress(fr.dudie.nominatim.client.request.NominatimLookupRequest)
|
||||
*/
|
||||
@Override
|
||||
public List<Address> lookupAddress(final NominatimLookupRequest lookup) throws IOException {
|
||||
|
||||
final String apiCall = String.format("%s&%s", lookupUrl, lookup.getQueryString());
|
||||
Log.d(TAG, "lookup url: " + apiCall);
|
||||
Request requesthttp = new Request.Builder()
|
||||
.addHeader("accept", "application/json")
|
||||
.url(apiCall)
|
||||
.build();
|
||||
|
||||
Response response = httpClient.newCall(requesthttp).execute();
|
||||
if (response.isSuccessful()) {
|
||||
ResponseBody responseBody = response.body();
|
||||
if (responseBody != null) {
|
||||
return gson.fromJson(responseBody.string(), new TypeToken<List<Address>>() {
|
||||
}.getType());
|
||||
}
|
||||
}
|
||||
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @see fr.dudie.nominatim.client.NominatimClient#search(java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public List<Address> search(final String query) throws IOException {
|
||||
|
||||
final NominatimSearchRequest q = new NominatimSearchRequest();
|
||||
q.setQuery(query);
|
||||
return this.search(q);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @see fr.dudie.nominatim.client.NominatimClient#getAddress(double, double)
|
||||
*/
|
||||
@Override
|
||||
public Address getAddress(final double longitude, final double latitude) throws IOException {
|
||||
|
||||
final NominatimReverseRequest q = new NominatimReverseRequest();
|
||||
q.setQuery(longitude, latitude);
|
||||
return this.getAddress(q);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @see fr.dudie.nominatim.client.NominatimClient#getAddress(double, double, int)
|
||||
*/
|
||||
@Override
|
||||
public Address getAddress(final double longitude, final double latitude, final int zoom)
|
||||
throws IOException {
|
||||
|
||||
final NominatimReverseRequest q = new NominatimReverseRequest();
|
||||
q.setQuery(longitude, latitude);
|
||||
q.setZoom(zoom);
|
||||
return this.getAddress(q);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @see fr.dudie.nominatim.client.NominatimClient#getAddress(int, int)
|
||||
*/
|
||||
@Override
|
||||
public Address getAddress(final int longitudeE6, final int latitudeE6) throws IOException {
|
||||
|
||||
return this.getAddress((longitudeE6 / 1E6), (latitudeE6 / 1E6));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @see fr.dudie.nominatim.client.NominatimClient#getAddress(String, long)
|
||||
*/
|
||||
@Override
|
||||
public Address getAddress(final String type, final long id) throws IOException {
|
||||
|
||||
final NominatimReverseRequest q = new NominatimReverseRequest();
|
||||
q.setQuery(OsmType.from(type), id);
|
||||
return this.getAddress(q);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @see fr.dudie.nominatim.client.NominatimClient#lookupAddress(java.util.List)
|
||||
*/
|
||||
@Override
|
||||
public List<Address> lookupAddress(final List<String> typeId) throws IOException {
|
||||
|
||||
final NominatimLookupRequest q = new NominatimLookupRequest();
|
||||
q.setQuery(typeId);
|
||||
return this.lookupAddress(q);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Sowjanya Kota <sowjanya.kch@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package ru.f7cloud.talk
|
||||
|
||||
object PhoneUtils {
|
||||
fun isPhoneNumber(input: String?): Boolean = input?.matches(Regex("^\\+?\\d+$")) == true
|
||||
}
|
||||
Vendored
+521
@@ -0,0 +1,521 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.account
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.work.Data
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import autodagger.AutoInjector
|
||||
import com.bluelinelabs.logansquare.LoganSquare
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.activities.BaseActivity
|
||||
import ru.f7cloud.talk.api.NcApi
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication.Companion.sharedApplication
|
||||
import ru.f7cloud.talk.conversationlist.ConversationsListActivity
|
||||
import ru.f7cloud.talk.data.user.model.User
|
||||
import ru.f7cloud.talk.databinding.ActivityAccountVerificationBinding
|
||||
import ru.f7cloud.talk.events.EventStatus
|
||||
import ru.f7cloud.talk.jobs.AccountRemovalWorker
|
||||
import ru.f7cloud.talk.jobs.CapabilitiesWorker
|
||||
import ru.f7cloud.talk.jobs.SignalingSettingsWorker
|
||||
import ru.f7cloud.talk.jobs.WebsocketConnectionsWorker
|
||||
import ru.f7cloud.talk.models.json.capabilities.CapabilitiesOverall
|
||||
import ru.f7cloud.talk.models.json.generic.Status
|
||||
import ru.f7cloud.talk.models.json.userprofile.UserProfileOverall
|
||||
import ru.f7cloud.talk.users.UserManager
|
||||
import ru.f7cloud.talk.utils.ApiUtils
|
||||
import ru.f7cloud.talk.utils.ClosedInterfaceImpl
|
||||
import ru.f7cloud.talk.utils.UriUtils
|
||||
import ru.f7cloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL
|
||||
import ru.f7cloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
|
||||
import ru.f7cloud.talk.utils.bundle.BundleKeys.KEY_IS_ACCOUNT_IMPORT
|
||||
import ru.f7cloud.talk.utils.bundle.BundleKeys.KEY_ORIGINAL_PROTOCOL
|
||||
import ru.f7cloud.talk.utils.bundle.BundleKeys.KEY_PASSWORD
|
||||
import ru.f7cloud.talk.utils.bundle.BundleKeys.KEY_TOKEN
|
||||
import ru.f7cloud.talk.utils.bundle.BundleKeys.KEY_USERNAME
|
||||
import ru.f7cloud.talk.utils.singletons.ApplicationWideMessageHolder
|
||||
import io.reactivex.MaybeObserver
|
||||
import io.reactivex.Observer
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import java.net.CookieManager
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(F7cloudTalkApplication::class)
|
||||
class AccountVerificationActivity : BaseActivity() {
|
||||
|
||||
private lateinit var binding: ActivityAccountVerificationBinding
|
||||
|
||||
@Inject
|
||||
lateinit var ncApi: NcApi
|
||||
|
||||
@Inject
|
||||
lateinit var userManager: UserManager
|
||||
|
||||
@Inject
|
||||
lateinit var cookieManager: CookieManager
|
||||
|
||||
private var internalAccountId: Long = -1
|
||||
private val disposables: MutableList<Disposable> = ArrayList()
|
||||
private var baseUrl: String? = null
|
||||
private var username: String? = null
|
||||
private var token: String? = null
|
||||
private var isAccountImport = false
|
||||
private var originalProtocol: String? = null
|
||||
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
binding = ActivityAccountVerificationBinding.inflate(layoutInflater)
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
setContentView(binding.root)
|
||||
actionBar?.hide()
|
||||
initSystemBars()
|
||||
|
||||
handleIntent()
|
||||
}
|
||||
|
||||
private fun handleIntent() {
|
||||
val extras = intent.extras!!
|
||||
baseUrl = extras.getString(KEY_BASE_URL)
|
||||
username = extras.getString(KEY_USERNAME)
|
||||
token = extras.getString(KEY_TOKEN)
|
||||
if (extras.containsKey(KEY_IS_ACCOUNT_IMPORT)) {
|
||||
isAccountImport = true
|
||||
}
|
||||
if (extras.containsKey(KEY_ORIGINAL_PROTOCOL)) {
|
||||
originalProtocol = extras.getString(KEY_ORIGINAL_PROTOCOL)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (
|
||||
isAccountImport &&
|
||||
!UriUtils.hasHttpProtocolPrefixed(baseUrl!!) ||
|
||||
isNotSameProtocol(baseUrl!!, originalProtocol)
|
||||
) {
|
||||
determineBaseUrlProtocol(true)
|
||||
} else {
|
||||
findServerTalkApp()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isNotSameProtocol(baseUrl: String, originalProtocol: String?): Boolean {
|
||||
if (originalProtocol == null) {
|
||||
return true
|
||||
}
|
||||
return !TextUtils.isEmpty(originalProtocol) && !baseUrl.startsWith(originalProtocol)
|
||||
}
|
||||
|
||||
private fun determineBaseUrlProtocol(checkForcedHttps: Boolean) {
|
||||
cookieManager.cookieStore.removeAll()
|
||||
baseUrl = baseUrl!!.replace("http://", "").replace("https://", "")
|
||||
val queryUrl: String = if (checkForcedHttps) {
|
||||
"https://" + baseUrl + ApiUtils.getUrlPostfixForStatus()
|
||||
} else {
|
||||
"http://" + baseUrl + ApiUtils.getUrlPostfixForStatus()
|
||||
}
|
||||
ncApi.getServerStatus(queryUrl)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(object : Observer<Status?> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
disposables.add(d)
|
||||
}
|
||||
|
||||
override fun onNext(status: Status) {
|
||||
baseUrl = if (checkForcedHttps) {
|
||||
"https://$baseUrl"
|
||||
} else {
|
||||
"http://$baseUrl"
|
||||
}
|
||||
if (isAccountImport) {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(KEY_BASE_URL, baseUrl)
|
||||
bundle.putString(KEY_USERNAME, username)
|
||||
bundle.putString(KEY_PASSWORD, "")
|
||||
|
||||
val intent = Intent(context, BrowserLoginActivity::class.java)
|
||||
intent.putExtras(bundle)
|
||||
startActivity(intent)
|
||||
} else {
|
||||
findServerTalkApp()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
if (checkForcedHttps) {
|
||||
determineBaseUrlProtocol(false)
|
||||
} else {
|
||||
abortVerification()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
// unused atm
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun findServerTalkApp() {
|
||||
val credentials = ApiUtils.getCredentials(username, token)
|
||||
cookieManager.cookieStore.removeAll()
|
||||
|
||||
ncApi.getCapabilities(credentials, ApiUtils.getUrlForCapabilities(baseUrl!!))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(object : Observer<CapabilitiesOverall> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
disposables.add(d)
|
||||
}
|
||||
|
||||
override fun onNext(capabilitiesOverall: CapabilitiesOverall) {
|
||||
val hasTalk =
|
||||
capabilitiesOverall.ocs!!.data!!.capabilities != null &&
|
||||
capabilitiesOverall.ocs!!.data!!.capabilities!!.spreedCapability != null &&
|
||||
capabilitiesOverall.ocs!!.data!!.capabilities!!.spreedCapability!!.features != null &&
|
||||
!capabilitiesOverall.ocs!!.data!!.capabilities!!.spreedCapability!!.features!!.isEmpty()
|
||||
if (hasTalk) {
|
||||
fetchProfile(credentials!!, capabilitiesOverall)
|
||||
} else {
|
||||
if (resources != null) {
|
||||
runOnUiThread {
|
||||
binding.progressText.text = String.format(
|
||||
resources!!.getString(R.string.nc_f7cloud_talk_app_not_installed),
|
||||
resources!!.getString(R.string.nc_app_product_name)
|
||||
)
|
||||
}
|
||||
}
|
||||
ApplicationWideMessageHolder.getInstance().messageType =
|
||||
ApplicationWideMessageHolder.MessageType.SERVER_WITHOUT_TALK
|
||||
abortVerification()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
if (resources != null) {
|
||||
runOnUiThread {
|
||||
binding.progressText.text = String.format(
|
||||
resources!!.getString(R.string.nc_f7cloud_talk_app_not_installed),
|
||||
resources!!.getString(R.string.nc_app_product_name)
|
||||
)
|
||||
}
|
||||
}
|
||||
ApplicationWideMessageHolder.getInstance().messageType =
|
||||
ApplicationWideMessageHolder.MessageType.SERVER_WITHOUT_TALK
|
||||
abortVerification()
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
// unused atm
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun storeProfile(displayName: String?, userId: String, capabilitiesOverall: CapabilitiesOverall) {
|
||||
userManager.storeProfile(
|
||||
username,
|
||||
UserManager.UserAttributes(
|
||||
id = null,
|
||||
serverUrl = baseUrl,
|
||||
currentUser = true,
|
||||
userId = userId,
|
||||
token = token,
|
||||
displayName = displayName,
|
||||
pushConfigurationState = null,
|
||||
capabilities = LoganSquare.serialize(capabilitiesOverall.ocs!!.data!!.capabilities),
|
||||
serverVersion = LoganSquare.serialize(capabilitiesOverall.ocs!!.data!!.serverVersion),
|
||||
certificateAlias = appPreferences.temporaryClientCertAlias,
|
||||
externalSignalingServer = null
|
||||
)
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(object : MaybeObserver<User> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
disposables.add(d)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onSuccess(user: User) {
|
||||
internalAccountId = user.id!!
|
||||
if (ClosedInterfaceImpl().isGooglePlayServicesAvailable) {
|
||||
ClosedInterfaceImpl().setUpPushTokenRegistration()
|
||||
} else {
|
||||
Log.w(TAG, "Skipping push registration.")
|
||||
runOnUiThread {
|
||||
binding.progressText.text =
|
||||
""" ${binding.progressText.text}
|
||||
${resources!!.getString(R.string.nc_push_disabled)}
|
||||
""".trimIndent()
|
||||
}
|
||||
fetchAndStoreCapabilities()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onError(e: Throwable) {
|
||||
binding.progressText.text = """ ${binding.progressText.text}""".trimIndent() +
|
||||
resources!!.getString(R.string.nc_display_name_not_stored)
|
||||
abortVerification()
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
// unused atm
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun fetchProfile(credentials: String, capabilitiesOverall: CapabilitiesOverall) {
|
||||
ncApi.getUserProfile(
|
||||
credentials,
|
||||
ApiUtils.getUrlForUserProfile(baseUrl!!)
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(object : Observer<UserProfileOverall> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
disposables.add(d)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onNext(userProfileOverall: UserProfileOverall) {
|
||||
var displayName: String? = null
|
||||
if (!TextUtils.isEmpty(userProfileOverall.ocs!!.data!!.displayName)) {
|
||||
displayName = userProfileOverall.ocs!!.data!!.displayName
|
||||
} else if (!TextUtils.isEmpty(userProfileOverall.ocs!!.data!!.displayNameAlt)) {
|
||||
displayName = userProfileOverall.ocs!!.data!!.displayNameAlt
|
||||
}
|
||||
if (!TextUtils.isEmpty(displayName)) {
|
||||
storeProfile(
|
||||
displayName,
|
||||
userProfileOverall.ocs!!.data!!.userId!!,
|
||||
capabilitiesOverall
|
||||
)
|
||||
} else {
|
||||
runOnUiThread {
|
||||
binding.progressText.text =
|
||||
"""
|
||||
${binding.progressText.text}
|
||||
${resources!!.getString(R.string.nc_display_name_not_fetched)}
|
||||
""".trimIndent()
|
||||
}
|
||||
abortVerification()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onError(e: Throwable) {
|
||||
runOnUiThread {
|
||||
binding.progressText.text =
|
||||
"""
|
||||
${binding.progressText.text}
|
||||
${resources!!.getString(R.string.nc_display_name_not_fetched)}
|
||||
""".trimIndent()
|
||||
}
|
||||
abortVerification()
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
// unused atm
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@Subscribe(threadMode = ThreadMode.BACKGROUND)
|
||||
fun onMessageEvent(eventStatus: EventStatus) {
|
||||
Log.d(TAG, "caught EventStatus of type " + eventStatus.eventType.toString())
|
||||
if (eventStatus.eventType == EventStatus.EventType.PUSH_REGISTRATION) {
|
||||
if (internalAccountId == eventStatus.userId && !eventStatus.isAllGood) {
|
||||
runOnUiThread {
|
||||
binding.progressText.text =
|
||||
"""
|
||||
${binding.progressText.text}
|
||||
${resources!!.getString(R.string.nc_push_disabled)}
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
fetchAndStoreCapabilities()
|
||||
} else if (eventStatus.eventType == EventStatus.EventType.CAPABILITIES_FETCH) {
|
||||
if (internalAccountId == eventStatus.userId && !eventStatus.isAllGood) {
|
||||
runOnUiThread {
|
||||
binding.progressText.text =
|
||||
"""
|
||||
${binding.progressText.text}
|
||||
${resources!!.getString(R.string.nc_capabilities_failed)}
|
||||
""".trimIndent()
|
||||
}
|
||||
abortVerification()
|
||||
} else if (internalAccountId == eventStatus.userId && eventStatus.isAllGood) {
|
||||
fetchAndStoreExternalSignalingSettings()
|
||||
}
|
||||
} else if (eventStatus.eventType == EventStatus.EventType.SIGNALING_SETTINGS) {
|
||||
if (internalAccountId == eventStatus.userId && !eventStatus.isAllGood) {
|
||||
runOnUiThread {
|
||||
binding.progressText.text =
|
||||
"""
|
||||
${binding.progressText.text}
|
||||
${resources!!.getString(R.string.nc_external_server_failed)}
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
proceedWithLogin()
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchAndStoreCapabilities() {
|
||||
val userData =
|
||||
Data.Builder()
|
||||
.putLong(KEY_INTERNAL_USER_ID, internalAccountId)
|
||||
.build()
|
||||
val capabilitiesWork =
|
||||
OneTimeWorkRequest.Builder(CapabilitiesWorker::class.java)
|
||||
.setInputData(userData)
|
||||
.build()
|
||||
WorkManager.getInstance().enqueue(capabilitiesWork)
|
||||
}
|
||||
|
||||
private fun fetchAndStoreExternalSignalingSettings() {
|
||||
val userData =
|
||||
Data.Builder()
|
||||
.putLong(KEY_INTERNAL_USER_ID, internalAccountId)
|
||||
.build()
|
||||
val signalingSettingsWorker = OneTimeWorkRequest.Builder(SignalingSettingsWorker::class.java)
|
||||
.setInputData(userData)
|
||||
.build()
|
||||
val websocketConnectionsWorker = OneTimeWorkRequest.Builder(WebsocketConnectionsWorker::class.java).build()
|
||||
|
||||
WorkManager.getInstance(applicationContext!!)
|
||||
.beginWith(signalingSettingsWorker)
|
||||
.then(websocketConnectionsWorker)
|
||||
.enqueue()
|
||||
}
|
||||
|
||||
private fun proceedWithLogin() {
|
||||
cookieManager.cookieStore.removeAll()
|
||||
|
||||
if (userManager.users.blockingGet().size == 1 ||
|
||||
currentUserProviderOld.currentUser.blockingGet().id != internalAccountId
|
||||
) {
|
||||
val userToSetAsActive = userManager.getUserWithId(internalAccountId).blockingGet()
|
||||
Log.d(TAG, "userToSetAsActive: " + userToSetAsActive.username)
|
||||
|
||||
if (userManager.setUserAsActive(userToSetAsActive).blockingGet()) {
|
||||
runOnUiThread {
|
||||
if (userManager.users.blockingGet().size == 1) {
|
||||
val intent = Intent(context, ConversationsListActivity::class.java)
|
||||
startActivity(intent)
|
||||
} else {
|
||||
if (isAccountImport) {
|
||||
ApplicationWideMessageHolder.getInstance().messageType =
|
||||
ApplicationWideMessageHolder.MessageType.ACCOUNT_WAS_IMPORTED
|
||||
}
|
||||
val intent = Intent(context, ConversationsListActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "failed to set active user")
|
||||
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "continuing proceedWithLogin was skipped for this user")
|
||||
}
|
||||
}
|
||||
|
||||
private fun dispose() {
|
||||
for (i in disposables.indices) {
|
||||
if (!disposables[i].isDisposed) {
|
||||
disposables[i].dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onDestroy() {
|
||||
dispose()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun abortVerification() {
|
||||
if (isAccountImport) {
|
||||
ApplicationWideMessageHolder.getInstance().messageType = ApplicationWideMessageHolder.MessageType
|
||||
.FAILED_TO_IMPORT_ACCOUNT
|
||||
runOnUiThread {
|
||||
Handler().postDelayed({
|
||||
val intent = Intent(this, ServerSelectionActivity::class.java)
|
||||
startActivity(intent)
|
||||
}, DELAY_IN_MILLIS)
|
||||
}
|
||||
} else {
|
||||
if (internalAccountId != -1L) {
|
||||
runOnUiThread {
|
||||
deleteUserAndStartServerSelection(internalAccountId)
|
||||
}
|
||||
} else {
|
||||
runOnUiThread {
|
||||
Handler().postDelayed({
|
||||
val intent = Intent(this, ServerSelectionActivity::class.java)
|
||||
startActivity(intent)
|
||||
}, DELAY_IN_MILLIS)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private fun deleteUserAndStartServerSelection(userId: Long) {
|
||||
userManager.scheduleUserForDeletionWithId(userId).blockingGet()
|
||||
val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build()
|
||||
WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork)
|
||||
|
||||
WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id)
|
||||
.observeForever { workInfo: WorkInfo? ->
|
||||
|
||||
when (workInfo?.state) {
|
||||
WorkInfo.State.SUCCEEDED -> {
|
||||
val intent = Intent(this, ServerSelectionActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
WorkInfo.State.FAILED, WorkInfo.State.CANCELLED -> {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.resources.getString(R.string.nc_common_error_sorry),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
Log.e(TAG, "something went wrong when deleting user with id $userId")
|
||||
val intent = Intent(this, ServerSelectionActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = AccountVerificationActivity::class.java.simpleName
|
||||
const val DELAY_IN_MILLIS: Long = 7500
|
||||
}
|
||||
}
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
|
||||
* SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.account
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.os.Bundle
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import autodagger.AutoInjector
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.account.data.LoginRepository
|
||||
import ru.f7cloud.talk.account.viewmodels.BrowserLoginActivityViewModel
|
||||
import ru.f7cloud.talk.activities.BaseActivity
|
||||
import ru.f7cloud.talk.activities.MainActivity
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication.Companion.sharedApplication
|
||||
import ru.f7cloud.talk.databinding.ActivityWebViewLoginBinding
|
||||
import ru.f7cloud.talk.utils.bundle.BundleKeys
|
||||
import ru.f7cloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(F7cloudTalkApplication::class)
|
||||
class BrowserLoginActivity : BaseActivity() {
|
||||
|
||||
private lateinit var binding: ActivityWebViewLoginBinding
|
||||
|
||||
@Inject
|
||||
lateinit var viewModel: BrowserLoginActivityViewModel
|
||||
|
||||
private var reauthorizeAccount = false
|
||||
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
binding = ActivityWebViewLoginBinding.inflate(layoutInflater)
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
setContentView(binding.root)
|
||||
actionBar?.hide()
|
||||
initSystemBars()
|
||||
initViews()
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
handleIntent()
|
||||
observe()
|
||||
}
|
||||
|
||||
init {
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
}
|
||||
|
||||
private fun observe() {
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.initialLoginRequestState.collect { state ->
|
||||
when (state) {
|
||||
BrowserLoginActivityViewModel.InitialLoginViewState.InitialLoginRequestError -> {
|
||||
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
is BrowserLoginActivityViewModel.InitialLoginViewState.InitialLoginRequestSuccess -> {
|
||||
if (viewModel.waitingForBrowserState.value) {
|
||||
viewModel.setWaitingForBrowser(false)
|
||||
viewModel.handleWebBrowserLogin()
|
||||
} else {
|
||||
viewModel.setWaitingForBrowser(true)
|
||||
launchDefaultWebBrowser(state.loginUrl)
|
||||
}
|
||||
}
|
||||
BrowserLoginActivityViewModel.InitialLoginViewState.None -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.postLoginState.collect { state ->
|
||||
when (state) {
|
||||
BrowserLoginActivityViewModel.PostLoginViewState.None -> {}
|
||||
|
||||
is BrowserLoginActivityViewModel.PostLoginViewState.PostLoginContinue -> {
|
||||
if (!state.data.isEmpty) {
|
||||
startAccountVerification(state.data)
|
||||
}
|
||||
}
|
||||
BrowserLoginActivityViewModel.PostLoginViewState.PostLoginError -> {
|
||||
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
BrowserLoginActivityViewModel.PostLoginViewState.PostLoginRestartApp -> {
|
||||
restartApp()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleIntent() {
|
||||
val extras = intent.extras!!
|
||||
val baseUrl = extras.getString(KEY_BASE_URL)
|
||||
|
||||
if (extras.containsKey(BundleKeys.KEY_REAUTHORIZE_ACCOUNT)) {
|
||||
reauthorizeAccount = extras.getBoolean(BundleKeys.KEY_REAUTHORIZE_ACCOUNT)
|
||||
}
|
||||
|
||||
if (extras.containsKey(BundleKeys.KEY_FROM_QR)) {
|
||||
val uri = extras.getString(BundleKeys.KEY_FROM_QR)!!
|
||||
|
||||
if (uri.startsWith(LoginRepository.ONE_TIME_PREFIX)) {
|
||||
viewModel.loginWithOTPQR(uri, reauthorizeAccount)
|
||||
} else {
|
||||
viewModel.loginWithQR(uri, reauthorizeAccount)
|
||||
}
|
||||
} else if (baseUrl != null) {
|
||||
viewModel.startWebBrowserLogin(baseUrl, reauthorizeAccount)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initViews() {
|
||||
viewThemeUtils.material.colorMaterialButtonFilledOnPrimary(binding.cancelLoginBtn)
|
||||
viewThemeUtils.material.colorProgressBar(binding.progressBar)
|
||||
|
||||
binding.cancelLoginBtn.setOnClickListener {
|
||||
viewModel.cancelLogin()
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchDefaultWebBrowser(url: String) {
|
||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun startAccountVerification(bundle: Bundle) {
|
||||
val intent = Intent(context, AccountVerificationActivity::class.java)
|
||||
intent.putExtras(bundle)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun restartApp() {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
public override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override val appBarLayoutType: AppBarLayoutType
|
||||
get() = AppBarLayoutType.EMPTY
|
||||
|
||||
companion object {
|
||||
private val TAG = BrowserLoginActivity::class.java.simpleName
|
||||
}
|
||||
}
|
||||
+468
@@ -0,0 +1,468 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.account
|
||||
|
||||
import android.Manifest
|
||||
import android.accounts.Account
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.os.Bundle
|
||||
import android.security.KeyChain
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.TextView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.os.bundleOf
|
||||
import autodagger.AutoInjector
|
||||
import com.blikoon.qrcodescanner.QrCodeActivity
|
||||
import com.github.dhaval2404.imagepicker.util.PermissionUtil
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.activities.BaseActivity
|
||||
import ru.f7cloud.talk.api.NcApi
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication.Companion.sharedApplication
|
||||
import ru.f7cloud.talk.databinding.ActivityServerSelectionBinding
|
||||
import ru.f7cloud.talk.models.json.capabilities.CapabilitiesOverall
|
||||
import ru.f7cloud.talk.models.json.generic.Status
|
||||
import ru.f7cloud.talk.users.UserManager
|
||||
import ru.f7cloud.talk.utils.AccountUtils
|
||||
import ru.f7cloud.talk.utils.ApiUtils
|
||||
import ru.f7cloud.talk.utils.CapabilitiesUtil
|
||||
import ru.f7cloud.talk.utils.UriUtils
|
||||
import ru.f7cloud.talk.utils.bundle.BundleKeys
|
||||
import ru.f7cloud.talk.utils.bundle.BundleKeys.ADD_ADDITIONAL_ACCOUNT
|
||||
import ru.f7cloud.talk.utils.bundle.BundleKeys.KEY_IS_ACCOUNT_IMPORT
|
||||
import ru.f7cloud.talk.utils.singletons.ApplicationWideMessageHolder
|
||||
import io.reactivex.Observer
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import java.security.cert.CertificateException
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
@AutoInjector(F7cloudTalkApplication::class)
|
||||
class ServerSelectionActivity : BaseActivity() {
|
||||
|
||||
private lateinit var binding: ActivityServerSelectionBinding
|
||||
|
||||
@Inject
|
||||
lateinit var ncApi: NcApi
|
||||
|
||||
@Inject
|
||||
lateinit var userManager: UserManager
|
||||
|
||||
private var statusQueryDisposable: Disposable? = null
|
||||
|
||||
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (intent.hasExtra(ADD_ADDITIONAL_ACCOUNT) && intent.getBooleanExtra(ADD_ADDITIONAL_ACCOUNT, false)) {
|
||||
finish()
|
||||
} else {
|
||||
finishAffinity()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
binding = ActivityServerSelectionBinding.inflate(layoutInflater)
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
setContentView(binding.root)
|
||||
actionBar?.hide()
|
||||
initSystemBars()
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
binding.hostUrlInputHelperText.text = String.format(
|
||||
resources!!.getString(R.string.nc_server_helper_text),
|
||||
resources!!.getString(R.string.nc_server_product_name)
|
||||
)
|
||||
binding.serverEntryTextInputLayout.setEndIconOnClickListener { checkServerAndProceed() }
|
||||
|
||||
if (resources!!.getBoolean(R.bool.hide_auth_cert)) {
|
||||
binding.certTextView.visibility = View.GONE
|
||||
}
|
||||
|
||||
val loggedInUsers = userManager.users.blockingGet()
|
||||
val availableAccounts = AccountUtils.findAvailableAccountsOnDevice(loggedInUsers)
|
||||
|
||||
if (isImportAccountNameSet() && availableAccounts.isNotEmpty()) {
|
||||
showImportAccountsInfo(availableAccounts)
|
||||
} else if (isAbleToShowProviderLink() && loggedInUsers.isEmpty()) {
|
||||
showVisitProvidersInfo()
|
||||
} else {
|
||||
binding.importOrChooseProviderText.visibility = View.INVISIBLE
|
||||
}
|
||||
|
||||
binding.serverEntryTextInputEditText.requestFocus()
|
||||
if (!TextUtils.isEmpty(resources!!.getString(R.string.weblogin_url))) {
|
||||
binding.serverEntryTextInputEditText.setText(resources!!.getString(R.string.weblogin_url))
|
||||
checkServerAndProceed()
|
||||
}
|
||||
binding.serverEntryTextInputEditText.setOnEditorActionListener { _: TextView?, i: Int, _: KeyEvent? ->
|
||||
if (i == EditorInfo.IME_ACTION_DONE) {
|
||||
checkServerAndProceed()
|
||||
}
|
||||
false
|
||||
}
|
||||
binding.certTextView.setOnClickListener { onCertClick() }
|
||||
|
||||
binding.scanQr.setOnClickListener { onScan() }
|
||||
|
||||
if (ApplicationWideMessageHolder.getInstance().messageType != null) {
|
||||
if (ApplicationWideMessageHolder.getInstance().messageType
|
||||
== ApplicationWideMessageHolder.MessageType.SERVER_WITHOUT_TALK
|
||||
) {
|
||||
setErrorText(resources!!.getString(R.string.nc_settings_no_talk_installed))
|
||||
} else if (ApplicationWideMessageHolder.getInstance().messageType
|
||||
== ApplicationWideMessageHolder.MessageType.FAILED_TO_IMPORT_ACCOUNT
|
||||
) {
|
||||
setErrorText(resources!!.getString(R.string.nc_server_failed_to_import_account))
|
||||
}
|
||||
ApplicationWideMessageHolder.getInstance().messageType = null
|
||||
}
|
||||
setCertTextView()
|
||||
}
|
||||
|
||||
fun onCertClick() {
|
||||
KeyChain.choosePrivateKeyAlias(
|
||||
this,
|
||||
{ alias: String? ->
|
||||
if (alias != null) {
|
||||
appPreferences.temporaryClientCertAlias = alias
|
||||
} else {
|
||||
appPreferences.removeTemporaryClientCertAlias()
|
||||
}
|
||||
setCertTextView()
|
||||
},
|
||||
arrayOf("RSA", "EC"),
|
||||
null,
|
||||
null,
|
||||
-1,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
private fun isAbleToShowProviderLink(): Boolean =
|
||||
!resources!!.getBoolean(R.bool.hide_provider) &&
|
||||
!TextUtils.isEmpty(resources!!.getString(R.string.nc_providers_url))
|
||||
|
||||
private fun showImportAccountsInfo(availableAccounts: List<Account>) {
|
||||
if (!TextUtils.isEmpty(
|
||||
AccountUtils.getAppNameBasedOnPackage(resources!!.getString(R.string.nc_import_accounts_from))
|
||||
)
|
||||
) {
|
||||
if (availableAccounts.size > 1) {
|
||||
binding.importOrChooseProviderText.text = String.format(
|
||||
resources!!.getString(R.string.nc_server_import_accounts),
|
||||
AccountUtils.getAppNameBasedOnPackage(resources!!.getString(R.string.nc_import_accounts_from))
|
||||
)
|
||||
} else {
|
||||
binding.importOrChooseProviderText.text = String.format(
|
||||
resources!!.getString(R.string.nc_server_import_account),
|
||||
AccountUtils.getAppNameBasedOnPackage(resources!!.getString(R.string.nc_import_accounts_from))
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (availableAccounts.size > 1) {
|
||||
binding.importOrChooseProviderText.text =
|
||||
resources!!.getString(R.string.nc_server_import_accounts_plain)
|
||||
} else {
|
||||
binding.importOrChooseProviderText.text =
|
||||
resources!!.getString(R.string.nc_server_import_account_plain)
|
||||
}
|
||||
}
|
||||
binding.importOrChooseProviderText.setOnClickListener {
|
||||
val bundle = Bundle()
|
||||
bundle.putBoolean(KEY_IS_ACCOUNT_IMPORT, true)
|
||||
val intent = Intent(context, SwitchAccountActivity::class.java)
|
||||
intent.putExtras(bundle)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showVisitProvidersInfo() {
|
||||
binding.importOrChooseProviderText.setText(R.string.nc_get_from_provider)
|
||||
binding.importOrChooseProviderText.setOnClickListener {
|
||||
val browserIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
resources!!.getString(R.string.nc_providers_url).toUri()
|
||||
)
|
||||
startActivity(browserIntent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isImportAccountNameSet(): Boolean =
|
||||
!TextUtils.isEmpty(resources!!.getString(R.string.nc_import_account_type))
|
||||
|
||||
@SuppressLint("LongLogTag")
|
||||
@Suppress("Detekt.TooGenericExceptionCaught")
|
||||
private fun checkServerAndProceed() {
|
||||
dispose()
|
||||
var url: String = binding.serverEntryTextInputEditText.text.toString().trim()
|
||||
showserverEntryProgressBar()
|
||||
if (binding.importOrChooseProviderText.visibility != View.INVISIBLE) {
|
||||
binding.importOrChooseProviderText.visibility = View.INVISIBLE
|
||||
binding.certTextView.visibility = View.INVISIBLE
|
||||
}
|
||||
if (url.endsWith("/")) {
|
||||
url = url.substring(0, url.length - 1)
|
||||
}
|
||||
|
||||
if (UriUtils.hasHttpProtocolPrefixed(url)) {
|
||||
checkServer(url, false)
|
||||
} else {
|
||||
checkServer("https://$url", true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkServer(url: String, checkForcedHttps: Boolean) {
|
||||
val queryStatusUrl = url + ApiUtils.getUrlPostfixForStatus()
|
||||
|
||||
statusQueryDisposable = ncApi.getServerStatus(queryStatusUrl)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ status: Status ->
|
||||
val versionString: String = status.version!!.substring(0, status.version!!.indexOf("."))
|
||||
val version: Int = versionString.toInt()
|
||||
|
||||
if (isServerStatusQueryable(status) && version >= MIN_SERVER_MAJOR_VERSION) {
|
||||
findServerTalkApp(url)
|
||||
} else {
|
||||
showErrorTextForStatus(status)
|
||||
}
|
||||
}, { throwable: Throwable ->
|
||||
if (checkForcedHttps) {
|
||||
checkServer(queryStatusUrl.replace("https://", "http://"), false)
|
||||
} else {
|
||||
if (throwable.localizedMessage != null) {
|
||||
setErrorText(throwable.localizedMessage)
|
||||
} else if (throwable.cause is CertificateException) {
|
||||
setErrorText(resources!!.getString(R.string.nc_certificate_error))
|
||||
} else {
|
||||
hideserverEntryProgressBar()
|
||||
}
|
||||
|
||||
if (binding.importOrChooseProviderText.visibility != View.INVISIBLE) {
|
||||
binding.importOrChooseProviderText.visibility = View.VISIBLE
|
||||
binding.certTextView.visibility = View.VISIBLE
|
||||
}
|
||||
dispose()
|
||||
}
|
||||
}) {
|
||||
hideserverEntryProgressBar()
|
||||
if (binding.importOrChooseProviderText.visibility != View.INVISIBLE) {
|
||||
binding.importOrChooseProviderText.visibility = View.VISIBLE
|
||||
binding.certTextView.visibility = View.VISIBLE
|
||||
}
|
||||
dispose()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showErrorTextForStatus(status: Status) {
|
||||
if (!status.installed) {
|
||||
setErrorText(
|
||||
String.format(
|
||||
resources!!.getString(R.string.nc_server_not_installed),
|
||||
resources!!.getString(R.string.nc_server_product_name)
|
||||
)
|
||||
)
|
||||
} else if (status.needsUpgrade) {
|
||||
setErrorText(
|
||||
String.format(
|
||||
resources!!.getString(R.string.nc_server_db_upgrade_needed),
|
||||
resources!!.getString(R.string.nc_server_product_name)
|
||||
)
|
||||
)
|
||||
} else if (status.maintenance) {
|
||||
setErrorText(
|
||||
String.format(
|
||||
resources!!.getString(R.string.nc_server_maintenance),
|
||||
resources!!.getString(R.string.nc_server_product_name)
|
||||
)
|
||||
)
|
||||
} else if (!status.version!!.startsWith("13.")) {
|
||||
setErrorText(
|
||||
String.format(
|
||||
resources!!.getString(R.string.nc_server_version),
|
||||
resources!!.getString(R.string.nc_app_product_name),
|
||||
resources!!.getString(R.string.nc_server_product_name)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findServerTalkApp(queryUrl: String) {
|
||||
ncApi.getCapabilities(ApiUtils.getUrlForCapabilities(queryUrl))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(object : Observer<CapabilitiesOverall> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onNext(capabilitiesOverall: CapabilitiesOverall) {
|
||||
val capabilities = capabilitiesOverall.ocs?.data?.capabilities
|
||||
|
||||
val hasTalk =
|
||||
capabilities?.spreedCapability != null &&
|
||||
capabilities.spreedCapability?.features != null &&
|
||||
capabilities.spreedCapability?.features?.isNotEmpty() == true
|
||||
|
||||
if (hasTalk) {
|
||||
runOnUiThread {
|
||||
if (CapabilitiesUtil.isServerEOL(capabilitiesOverall.ocs?.data?.serverVersion?.major)) {
|
||||
if (resources != null) {
|
||||
runOnUiThread {
|
||||
setErrorText(resources!!.getString(R.string.nc_settings_server_eol))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(BundleKeys.KEY_BASE_URL, queryUrl.replace("/status.php", ""))
|
||||
|
||||
val intent = Intent(context, BrowserLoginActivity::class.java)
|
||||
intent.putExtras(bundle)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (resources != null) {
|
||||
runOnUiThread {
|
||||
setErrorText(resources!!.getString(R.string.nc_server_unsupported))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
Log.e(TAG, "Error while checking capabilities", e)
|
||||
if (resources != null) {
|
||||
runOnUiThread {
|
||||
setErrorText(resources!!.getString(R.string.nc_common_error_sorry))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
// unused atm
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun isServerStatusQueryable(status: Status): Boolean =
|
||||
status.installed && !status.maintenance && !status.needsUpgrade
|
||||
|
||||
private fun setErrorText(text: String?) {
|
||||
binding.errorWrapper.visibility = View.VISIBLE
|
||||
binding.errorText.text = text
|
||||
hideserverEntryProgressBar()
|
||||
}
|
||||
|
||||
private fun showserverEntryProgressBar() {
|
||||
binding.errorWrapper.visibility = View.INVISIBLE
|
||||
binding.serverEntryProgressBar.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun hideserverEntryProgressBar() {
|
||||
binding.serverEntryProgressBar.visibility = View.INVISIBLE
|
||||
}
|
||||
|
||||
@SuppressLint("LongLogTag")
|
||||
private fun setCertTextView() {
|
||||
runOnUiThread {
|
||||
if (!TextUtils.isEmpty(appPreferences.temporaryClientCertAlias)) {
|
||||
binding.certTextView.setText(R.string.nc_change_cert_auth)
|
||||
} else {
|
||||
binding.certTextView.setText(R.string.nc_configure_cert_auth)
|
||||
}
|
||||
hideserverEntryProgressBar()
|
||||
}
|
||||
}
|
||||
|
||||
private val requestCameraPermissionLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
||||
if (isGranted) {
|
||||
// Permission was granted
|
||||
startQRScanner()
|
||||
}
|
||||
}
|
||||
|
||||
fun onScan() {
|
||||
if (PermissionUtil.isPermissionGranted(this, Manifest.permission.CAMERA)) {
|
||||
startQRScanner()
|
||||
} else {
|
||||
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startQRScanner() {
|
||||
val intent = Intent(this, QrCodeActivity::class.java)
|
||||
qrScanResultLauncher.launch(intent)
|
||||
}
|
||||
|
||||
private val qrScanResultLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
val data = result.data
|
||||
|
||||
if (data == null) {
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
val resultData = data.getStringExtra(QR_URI)
|
||||
|
||||
if (resultData == null || !resultData.startsWith("nc")) {
|
||||
Snackbar.make(binding.root, getString(R.string.qr_code_error), Snackbar.LENGTH_SHORT).show()
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
val intent = Intent(this, BrowserLoginActivity::class.java)
|
||||
val bundle = bundleOf().apply {
|
||||
putString(BundleKeys.KEY_FROM_QR, resultData)
|
||||
}
|
||||
intent.putExtras(bundle)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
dispose()
|
||||
}
|
||||
|
||||
private fun dispose() {
|
||||
if (statusQueryDisposable != null && !statusQueryDisposable!!.isDisposed) {
|
||||
statusQueryDisposable!!.dispose()
|
||||
}
|
||||
statusQueryDisposable = null
|
||||
}
|
||||
|
||||
override val appBarLayoutType: AppBarLayoutType
|
||||
get() = AppBarLayoutType.EMPTY
|
||||
|
||||
companion object {
|
||||
private val TAG = ServerSelectionActivity::class.java.simpleName
|
||||
const val MIN_SERVER_MAJOR_VERSION = 13
|
||||
private const val QR_URI = "com.blikoon.qrcodescanner.got_qr_scan_relult"
|
||||
}
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.account
|
||||
|
||||
import android.accounts.Account
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.os.Bundle
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import autodagger.AutoInjector
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.activities.BaseActivity
|
||||
import ru.f7cloud.talk.adapters.items.AdvancedUserItem
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication.Companion.sharedApplication
|
||||
import ru.f7cloud.talk.data.user.model.User
|
||||
import ru.f7cloud.talk.databinding.ActivitySwitchAccountBinding
|
||||
import ru.f7cloud.talk.models.ImportAccount
|
||||
import ru.f7cloud.talk.models.json.participants.Participant
|
||||
import ru.f7cloud.talk.users.UserManager
|
||||
import ru.f7cloud.talk.utils.AccountUtils.findAvailableAccountsOnDevice
|
||||
import ru.f7cloud.talk.utils.AccountUtils.getInformationFromAccount
|
||||
import ru.f7cloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL
|
||||
import ru.f7cloud.talk.utils.bundle.BundleKeys.KEY_IS_ACCOUNT_IMPORT
|
||||
import ru.f7cloud.talk.utils.bundle.BundleKeys.KEY_TOKEN
|
||||
import ru.f7cloud.talk.utils.bundle.BundleKeys.KEY_USERNAME
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import org.osmdroid.config.Configuration
|
||||
import java.net.CookieManager
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Parts related to account import were either copied from or inspired by the great work done by David Luhmer at:
|
||||
* https://github.com/nextcloud/ownCloud-Account-Importer
|
||||
*/
|
||||
@AutoInjector(F7cloudTalkApplication::class)
|
||||
class SwitchAccountActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivitySwitchAccountBinding
|
||||
|
||||
@Inject
|
||||
lateinit var userManager: UserManager
|
||||
|
||||
@Inject
|
||||
lateinit var cookieManager: CookieManager
|
||||
|
||||
private var adapter: FlexibleAdapter<AbstractFlexibleItem<*>>? = null
|
||||
private val userItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
|
||||
private var isAccountImport = false
|
||||
|
||||
private val onImportItemClickListener = FlexibleAdapter.OnItemClickListener { _, position ->
|
||||
if (userItems.size > position) {
|
||||
val account = (userItems[position] as AdvancedUserItem).account
|
||||
reauthorizeFromImport(account)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
private val onSwitchItemClickListener = FlexibleAdapter.OnItemClickListener { _, position ->
|
||||
if (userItems.size > position) {
|
||||
val user = (userItems[position] as AdvancedUserItem).user
|
||||
|
||||
if (userManager.setUserAsActive(user!!).blockingGet()) {
|
||||
cookieManager.cookieStore.removeAll()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
binding = ActivitySwitchAccountBinding.inflate(layoutInflater)
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
setContentView(binding.root)
|
||||
setupActionBar()
|
||||
initSystemBars()
|
||||
|
||||
Configuration.getInstance().load(context, PreferenceManager.getDefaultSharedPreferences(context))
|
||||
|
||||
handleIntent()
|
||||
}
|
||||
|
||||
private fun handleIntent() {
|
||||
intent.extras?.let {
|
||||
if (it.containsKey(KEY_IS_ACCOUNT_IMPORT)) {
|
||||
isAccountImport = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupActionBar() {
|
||||
setSupportActionBar(binding.toolbar)
|
||||
binding.toolbar.setNavigationOnClickListener {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
supportActionBar?.setIcon(resources!!.getColor(R.color.transparent, null).toDrawable())
|
||||
supportActionBar?.title = resources!!.getString(R.string.nc_select_an_account)
|
||||
}
|
||||
|
||||
@Suppress("Detekt.NestedBlockDepth")
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (adapter == null) {
|
||||
adapter = FlexibleAdapter(userItems, this, false)
|
||||
var participant: Participant
|
||||
|
||||
if (!isAccountImport) {
|
||||
for (user in userManager.users.blockingGet()) {
|
||||
if (!user.current) {
|
||||
val userId: String? = if (user.userId != null) {
|
||||
user.userId
|
||||
} else {
|
||||
user.username
|
||||
}
|
||||
participant = Participant()
|
||||
participant.actorType = Participant.ActorType.USERS
|
||||
participant.actorId = userId
|
||||
participant.displayName = user.displayName
|
||||
userItems.add(AdvancedUserItem(participant, user, null, viewThemeUtils, 0))
|
||||
}
|
||||
}
|
||||
adapter!!.addListener(onSwitchItemClickListener)
|
||||
adapter!!.updateDataSet(userItems, false)
|
||||
} else {
|
||||
var account: Account
|
||||
var importAccount: ImportAccount
|
||||
var user: User
|
||||
for (accountObject in findAvailableAccountsOnDevice(userManager.users.blockingGet())) {
|
||||
account = accountObject
|
||||
importAccount = getInformationFromAccount(account)
|
||||
participant = Participant()
|
||||
participant.actorType = Participant.ActorType.USERS
|
||||
participant.actorId = importAccount.getUsername()
|
||||
participant.displayName = importAccount.getUsername()
|
||||
user = User()
|
||||
user.baseUrl = importAccount.getBaseUrl()
|
||||
userItems.add(AdvancedUserItem(participant, user, account, viewThemeUtils, 0))
|
||||
}
|
||||
adapter!!.addListener(onImportItemClickListener)
|
||||
adapter!!.updateDataSet(userItems, false)
|
||||
}
|
||||
}
|
||||
prepareViews()
|
||||
}
|
||||
|
||||
private fun prepareViews() {
|
||||
val layoutManager: LinearLayoutManager = SmoothScrollLinearLayoutManager(this)
|
||||
binding.recyclerView.layoutManager = layoutManager
|
||||
binding.recyclerView.setHasFixedSize(true)
|
||||
binding.recyclerView.adapter = adapter
|
||||
}
|
||||
|
||||
private fun reauthorizeFromImport(account: Account?) {
|
||||
val importAccount = getInformationFromAccount(account!!)
|
||||
val bundle = Bundle()
|
||||
bundle.putString(KEY_BASE_URL, importAccount.getBaseUrl())
|
||||
bundle.putString(KEY_USERNAME, importAccount.getUsername())
|
||||
bundle.putString(KEY_TOKEN, importAccount.getToken())
|
||||
bundle.putBoolean(KEY_IS_ACCOUNT_IMPORT, true)
|
||||
|
||||
val intent = Intent(context, AccountVerificationActivity::class.java)
|
||||
intent.putExtras(bundle)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package ru.f7cloud.talk.account.data
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import ru.f7cloud.talk.account.data.io.LocalLoginDataSource
|
||||
import ru.f7cloud.talk.account.data.model.LoginCompletion
|
||||
import ru.f7cloud.talk.account.data.model.LoginResponse
|
||||
import ru.f7cloud.talk.account.data.network.NetworkLoginDataSource
|
||||
import ru.f7cloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL
|
||||
import ru.f7cloud.talk.utils.bundle.BundleKeys.KEY_ORIGINAL_PROTOCOL
|
||||
import ru.f7cloud.talk.utils.bundle.BundleKeys.KEY_TOKEN
|
||||
import ru.f7cloud.talk.utils.bundle.BundleKeys.KEY_USERNAME
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Credentials
|
||||
import java.net.URLDecoder
|
||||
|
||||
@Suppress("TooManyFunctions", "ReturnCount")
|
||||
class LoginRepository(val network: NetworkLoginDataSource, val local: LocalLoginDataSource) {
|
||||
|
||||
companion object {
|
||||
val TAG: String = LoginRepository::class.java.simpleName
|
||||
private const val INTERVAL = 250L
|
||||
private const val HTTP_OK = 200
|
||||
private const val USER_KEY = "user:"
|
||||
private const val SERVER_KEY = "server:"
|
||||
private const val PASS_KEY = "password:"
|
||||
private const val PREFIX = "f7://login/"
|
||||
const val ONE_TIME_PREFIX = "f7://onetime-login/"
|
||||
private const val MAX_ARGS = 3
|
||||
}
|
||||
|
||||
private var shouldReauthorizeUser = false
|
||||
private var shouldLoop = true
|
||||
|
||||
suspend fun pollLogin(response: LoginResponse): LoginCompletion? =
|
||||
withContext(Dispatchers.IO) {
|
||||
while (shouldLoop) {
|
||||
val loginData = network.performLoginFlowV2(response)
|
||||
if (loginData == null) {
|
||||
break
|
||||
}
|
||||
|
||||
if (loginData.status == HTTP_OK) {
|
||||
return@withContext loginData
|
||||
}
|
||||
|
||||
delay(INTERVAL) // No response yet, retry
|
||||
}
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry point for QR scanner
|
||||
*
|
||||
*/
|
||||
fun startLoginFlowFromQR(dataString: String, reAuth: Boolean = false): LoginCompletion? {
|
||||
shouldReauthorizeUser = reAuth
|
||||
|
||||
if (!dataString.startsWith(PREFIX)) {
|
||||
Log.e(TAG, "Invalid login URL detected")
|
||||
return null
|
||||
}
|
||||
|
||||
val data = dataString.removePrefix(PREFIX)
|
||||
val values = data.split('&')
|
||||
|
||||
if (values.size !in 1..MAX_ARGS) {
|
||||
Log.e(TAG, "Illegal number of login URL elements detected: ${values.size}")
|
||||
return null
|
||||
}
|
||||
|
||||
var server = ""
|
||||
var loginName = ""
|
||||
var appPassword = ""
|
||||
values.forEach { value ->
|
||||
when {
|
||||
value.startsWith(USER_KEY) -> {
|
||||
loginName = URLDecoder.decode(value.removePrefix(USER_KEY), "UTF-8")
|
||||
}
|
||||
|
||||
value.startsWith(PASS_KEY) -> {
|
||||
appPassword = URLDecoder.decode(value.removePrefix(PASS_KEY), "UTF-8")
|
||||
}
|
||||
|
||||
value.startsWith(SERVER_KEY) -> {
|
||||
server = URLDecoder.decode(value.removePrefix(SERVER_KEY), "UTF-8")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return if (server.isNotEmpty() && loginName.isNotEmpty() && appPassword.isNotEmpty()) {
|
||||
LoginCompletion(HTTP_OK, server, loginName, appPassword)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry point for a one-time QR code
|
||||
*/
|
||||
suspend fun startOTPLoginFlow(dataString: String, reAuth: Boolean = false): LoginCompletion? =
|
||||
withContext(Dispatchers.IO) {
|
||||
shouldReauthorizeUser = reAuth
|
||||
|
||||
if (!dataString.startsWith(ONE_TIME_PREFIX)) {
|
||||
Log.e(TAG, "Invalid login URL detected")
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
val data = dataString.removePrefix(ONE_TIME_PREFIX)
|
||||
val values = data.split('&')
|
||||
|
||||
if (values.size !in 1..MAX_ARGS) {
|
||||
Log.e(TAG, "Illegal number of login URL elements detected: ${values.size}")
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
var server = ""
|
||||
var loginName = ""
|
||||
var appPassword = ""
|
||||
values.forEach { value ->
|
||||
when {
|
||||
value.startsWith(USER_KEY) -> {
|
||||
loginName = URLDecoder.decode(value.removePrefix(USER_KEY), "UTF-8")
|
||||
}
|
||||
|
||||
value.startsWith(PASS_KEY) -> {
|
||||
appPassword = URLDecoder.decode(value.removePrefix(PASS_KEY), "UTF-8")
|
||||
}
|
||||
|
||||
value.startsWith(SERVER_KEY) -> {
|
||||
server = URLDecoder.decode(value.removePrefix(SERVER_KEY), "UTF-8")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Need to use the qr code token to create temporary credentials to get access to the actual app password
|
||||
val credentials = Credentials.basic(loginName, appPassword)
|
||||
val oneTimePassword = network.oneTimePasswordRequest(server, credentials)
|
||||
|
||||
return@withContext if (server.isNotEmpty() && loginName.isNotEmpty() && oneTimePassword != null) {
|
||||
LoginCompletion(HTTP_OK, server, loginName, oneTimePassword)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry point to the login process
|
||||
*/
|
||||
suspend fun startLoginFlow(baseUrl: String, reAuth: Boolean = false): LoginResponse? =
|
||||
withContext(Dispatchers.IO) {
|
||||
shouldReauthorizeUser = reAuth
|
||||
val response = network.anonymouslyPostLoginRequest(baseUrl)
|
||||
return@withContext response
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends normal login process by canceling the polling
|
||||
*/
|
||||
fun cancelLoginFlow() {
|
||||
shouldLoop = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns bundle if user is not scheduled for deletion or doesn't already exist, null otherwise
|
||||
*/
|
||||
fun parseAndLogin(loginData: LoginCompletion): Bundle? {
|
||||
if (local.checkIfUserIsScheduledForDeletion(loginData)) {
|
||||
// however the user is not yet deleted, just start AccountRemovalWorker again to make sure to delete it.
|
||||
local.startAccountRemovalWorker()
|
||||
return null
|
||||
} else if (local.checkIfUserExists(loginData)) {
|
||||
if (shouldReauthorizeUser) {
|
||||
local.updateUser(loginData)
|
||||
} else {
|
||||
Log.w(TAG, "Tried to add an account that account already exists. Skipped user creation.")
|
||||
}
|
||||
|
||||
return null
|
||||
} else {
|
||||
return startAccountVerification(loginData)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startAccountVerification(loginData: LoginCompletion): Bundle {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(KEY_USERNAME, loginData.loginName)
|
||||
bundle.putString(KEY_TOKEN, loginData.appPassword)
|
||||
bundle.putString(KEY_BASE_URL, loginData.server)
|
||||
var protocol = ""
|
||||
if (loginData.server.startsWith("http://")) {
|
||||
protocol = "http://"
|
||||
} else if (loginData.server.startsWith("https://")) {
|
||||
protocol = "https://"
|
||||
}
|
||||
if (!TextUtils.isEmpty(protocol)) {
|
||||
bundle.putString(KEY_ORIGINAL_PROTOCOL, protocol)
|
||||
}
|
||||
|
||||
return bundle
|
||||
}
|
||||
}
|
||||
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package ru.f7cloud.talk.account.data.io
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import ru.f7cloud.talk.account.data.model.LoginCompletion
|
||||
import ru.f7cloud.talk.jobs.AccountRemovalWorker
|
||||
import ru.f7cloud.talk.users.UserManager
|
||||
import ru.f7cloud.talk.utils.preferences.AppPreferences
|
||||
|
||||
// local datasource for communicating with room through account manager
|
||||
// crucial for making sure the login process interacts with the db as expected.
|
||||
class LocalLoginDataSource(val userManager: UserManager, val appPreferences: AppPreferences, val context: Context) {
|
||||
|
||||
fun updateUser(loginData: LoginCompletion) {
|
||||
val currentUser = userManager.currentUser.blockingGet()
|
||||
if (currentUser != null) {
|
||||
currentUser.clientCertificate = appPreferences.temporaryClientCertAlias
|
||||
currentUser.token = loginData.appPassword
|
||||
userManager.updateOrCreateUser(currentUser)
|
||||
}
|
||||
}
|
||||
|
||||
fun startAccountRemovalWorker(): LiveData<WorkInfo?> {
|
||||
val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build()
|
||||
WorkManager.getInstance(context).enqueue(accountRemovalWork)
|
||||
|
||||
return WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id)
|
||||
}
|
||||
|
||||
fun checkIfUserIsScheduledForDeletion(data: LoginCompletion): Boolean =
|
||||
userManager.checkIfUserIsScheduledForDeletion(data.loginName, data.server).blockingGet()
|
||||
|
||||
fun checkIfUserExists(data: LoginCompletion): Boolean =
|
||||
userManager.checkIfUserExists(data.loginName, data.server).blockingGet()
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Sowjanya Kota <sowjanya.kch@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package ru.f7cloud.talk.account.data.model
|
||||
|
||||
import ru.f7cloud.talk.data.user.model.User
|
||||
|
||||
data class AccountItem(val user: User, val pendingInvitation: Int)
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package ru.f7cloud.talk.account.data.model
|
||||
|
||||
data class LoginResponse(val token: String, val pollUrl: String, val loginUrl: String)
|
||||
|
||||
data class LoginCompletion(val status: Int, val server: String, val loginName: String, val appPassword: String)
|
||||
vendor/talk-android/app/src/main/java/ru/f7cloud/talk/account/data/network/NetworkLoginDataSource.kt
Vendored
+167
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package ru.f7cloud.talk.account.data.network
|
||||
|
||||
import android.util.Log
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import ru.f7cloud.talk.account.data.model.LoginCompletion
|
||||
import ru.f7cloud.talk.account.data.model.LoginResponse
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import java.io.IOException
|
||||
import javax.net.ssl.SSLHandshakeException
|
||||
|
||||
// This class handles the network and polling logic in isolation, which makes it easier to test
|
||||
// Login and Authentication is critical, thus it needs to be working properly.
|
||||
class NetworkLoginDataSource(val okHttpClient: OkHttpClient) {
|
||||
|
||||
companion object {
|
||||
val TAG: String = NetworkLoginDataSource::class.java.simpleName
|
||||
}
|
||||
|
||||
fun oneTimePasswordRequest(baseUrl: String, oneTimeCredentials: String): String? {
|
||||
val url = "$baseUrl/ocs/v2.php/core/getapppassword-onetime"
|
||||
var result: String? = null
|
||||
runCatching {
|
||||
val response = getResponseOfOTPRequest(url, oneTimeCredentials)
|
||||
val jsonObject: JsonObject = JsonParser.parseString(response).asJsonObject
|
||||
val appPassword = jsonObject
|
||||
.getAsJsonObject("ocs")
|
||||
.getAsJsonObject("data")
|
||||
.get("apppassword").asString
|
||||
|
||||
result = appPassword
|
||||
}.getOrElse { e ->
|
||||
when (e) {
|
||||
is SSLHandshakeException,
|
||||
is NullPointerException,
|
||||
is IOException -> {
|
||||
Log.e(TAG, "Error caught at oneTimePasswordRequest: $e")
|
||||
}
|
||||
|
||||
else -> throw e
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun getResponseOfOTPRequest(url: String, oneTimeCredentials: String): String? {
|
||||
val request = Request.Builder() // GET is default
|
||||
.url(url)
|
||||
.header("Authorization", oneTimeCredentials)
|
||||
.header("OCS-APIRequest", "true")
|
||||
.header("Accept", "application/json")
|
||||
.build()
|
||||
|
||||
val newOkHttpClient = OkHttpClient()
|
||||
newOkHttpClient.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
throw IOException("Unexpected code $response")
|
||||
}
|
||||
return response.body?.string()
|
||||
}
|
||||
}
|
||||
|
||||
fun anonymouslyPostLoginRequest(baseUrl: String): LoginResponse? {
|
||||
val url = "$baseUrl/index.php/login/v2"
|
||||
var result: LoginResponse? = null
|
||||
runCatching {
|
||||
val response = getResponseOfAnonymouslyPostLoginRequest(url)
|
||||
val jsonObject: JsonObject = JsonParser.parseString(response).asJsonObject
|
||||
val loginUrl: String = getLoginUrl(jsonObject)
|
||||
val token = jsonObject.getAsJsonObject("poll").get("token").asString
|
||||
val pollUrl = jsonObject.getAsJsonObject("poll").get("endpoint").asString
|
||||
result = LoginResponse(token, pollUrl, loginUrl)
|
||||
}.getOrElse { e ->
|
||||
when (e) {
|
||||
is SSLHandshakeException,
|
||||
is NullPointerException,
|
||||
is IOException -> {
|
||||
Log.e(TAG, "Error caught at anonymouslyPostLoginRequest: $e")
|
||||
}
|
||||
|
||||
else -> throw e
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun getResponseOfAnonymouslyPostLoginRequest(url: String): String? {
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.post(FormBody.Builder().build())
|
||||
.addHeader("Clear-Site-Data", "cookies")
|
||||
.build()
|
||||
|
||||
okHttpClient.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
throw IOException("Unexpected code $response")
|
||||
}
|
||||
return response.body?.string()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLoginUrl(response: JsonObject): String {
|
||||
var result: String? = response.get("login").asString
|
||||
if (result == null) {
|
||||
result = ""
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun performLoginFlowV2(response: LoginResponse): LoginCompletion? {
|
||||
val requestBody: RequestBody = FormBody.Builder()
|
||||
.add("token", response.token)
|
||||
.build()
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(response.pollUrl)
|
||||
.post(requestBody)
|
||||
.build()
|
||||
|
||||
var result: LoginCompletion? = null
|
||||
runCatching {
|
||||
okHttpClient.newCall(request).execute()
|
||||
.use { response ->
|
||||
val status: Int = response.code
|
||||
val responseBody = response.body?.string()
|
||||
|
||||
result = if (response.isSuccessful && responseBody?.isNotEmpty() == true) {
|
||||
val jsonObject = JsonParser.parseString(responseBody).asJsonObject
|
||||
val server: String = jsonObject.get("server").asString
|
||||
val loginName: String = jsonObject.get("loginName").asString
|
||||
val appPassword: String = jsonObject.get("appPassword").asString
|
||||
|
||||
LoginCompletion(status, server, loginName, appPassword)
|
||||
} else {
|
||||
LoginCompletion(status, "", "", "")
|
||||
}
|
||||
}
|
||||
}.getOrElse { e ->
|
||||
when (e) {
|
||||
is NullPointerException,
|
||||
is SSLHandshakeException,
|
||||
is IllegalStateException,
|
||||
is java.net.UnknownHostException,
|
||||
is IOException -> {
|
||||
Log.e(TAG, "Error caught at performLoginFlowV2: with url ${request.url} $e")
|
||||
}
|
||||
|
||||
else -> throw e
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package ru.f7cloud.talk.account.viewmodels
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import ru.f7cloud.talk.account.data.LoginRepository
|
||||
import ru.f7cloud.talk.account.data.model.LoginResponse
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class BrowserLoginActivityViewModel @Inject constructor(val repository: LoginRepository) : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private val TAG = BrowserLoginActivityViewModel::class.java.simpleName
|
||||
}
|
||||
|
||||
sealed class InitialLoginViewState {
|
||||
data object None : InitialLoginViewState()
|
||||
data class InitialLoginRequestSuccess(val loginUrl: String) : InitialLoginViewState()
|
||||
data object InitialLoginRequestError : InitialLoginViewState()
|
||||
}
|
||||
|
||||
private val _initialLoginRequestState = MutableStateFlow<InitialLoginViewState>(InitialLoginViewState.None)
|
||||
val initialLoginRequestState: StateFlow<InitialLoginViewState> = _initialLoginRequestState
|
||||
|
||||
sealed class PostLoginViewState {
|
||||
data object None : PostLoginViewState()
|
||||
data object PostLoginRestartApp : PostLoginViewState()
|
||||
data object PostLoginError : PostLoginViewState()
|
||||
data class PostLoginContinue(val data: Bundle) : PostLoginViewState()
|
||||
}
|
||||
|
||||
private val _postLoginState = MutableStateFlow<PostLoginViewState>(PostLoginViewState.None)
|
||||
val postLoginState: StateFlow<PostLoginViewState> = _postLoginState
|
||||
|
||||
private val _waitingForBrowserState = MutableStateFlow(false)
|
||||
val waitingForBrowserState = _waitingForBrowserState.asStateFlow()
|
||||
|
||||
fun setWaitingForBrowser(value: Boolean) {
|
||||
_waitingForBrowserState.value = value
|
||||
}
|
||||
|
||||
private var savedResponse: LoginResponse? = null
|
||||
|
||||
fun startWebBrowserLogin(baseUrl: String, reAuth: Boolean = false) {
|
||||
viewModelScope.launch {
|
||||
val response = repository.startLoginFlow(baseUrl, reAuth)
|
||||
savedResponse = response
|
||||
|
||||
if (response == null) {
|
||||
_initialLoginRequestState.value = InitialLoginViewState.InitialLoginRequestError
|
||||
return@launch
|
||||
}
|
||||
|
||||
_initialLoginRequestState.value =
|
||||
InitialLoginViewState.InitialLoginRequestSuccess(response.loginUrl)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleWebBrowserLogin() {
|
||||
savedResponse?.let { response ->
|
||||
viewModelScope.launch {
|
||||
val loginCompletionResponse = repository.pollLogin(response)
|
||||
|
||||
if (loginCompletionResponse == null) {
|
||||
_postLoginState.value = PostLoginViewState.PostLoginError
|
||||
return@launch
|
||||
}
|
||||
|
||||
val bundle = repository.parseAndLogin(loginCompletionResponse)
|
||||
if (bundle == null) {
|
||||
_postLoginState.value = PostLoginViewState.PostLoginRestartApp
|
||||
return@launch
|
||||
}
|
||||
|
||||
_postLoginState.value = PostLoginViewState.PostLoginContinue(bundle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loginWithQR(dataString: String, reAuth: Boolean = false) {
|
||||
viewModelScope.launch {
|
||||
val loginCompletionResponse = repository.startLoginFlowFromQR(dataString, reAuth)
|
||||
if (loginCompletionResponse == null) {
|
||||
_postLoginState.value = PostLoginViewState.PostLoginError
|
||||
return@launch
|
||||
}
|
||||
|
||||
val bundle = repository.parseAndLogin(loginCompletionResponse)
|
||||
if (bundle == null) {
|
||||
_postLoginState.value = PostLoginViewState.PostLoginRestartApp
|
||||
return@launch
|
||||
}
|
||||
|
||||
_postLoginState.value = PostLoginViewState.PostLoginContinue(bundle)
|
||||
}
|
||||
}
|
||||
|
||||
fun loginWithOTPQR(dataString: String, reAuth: Boolean = false) {
|
||||
viewModelScope.launch {
|
||||
val loginCompletionResponse = repository.startOTPLoginFlow(dataString, reAuth)
|
||||
if (loginCompletionResponse == null) {
|
||||
_postLoginState.value = PostLoginViewState.PostLoginError
|
||||
return@launch
|
||||
}
|
||||
|
||||
val bundle = repository.parseAndLogin(loginCompletionResponse)
|
||||
if (bundle == null) {
|
||||
_postLoginState.value = PostLoginViewState.PostLoginRestartApp
|
||||
return@launch
|
||||
}
|
||||
|
||||
_postLoginState.value = PostLoginViewState.PostLoginContinue(bundle)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelLogin() = repository.cancelLoginFlow()
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2016 BlueLine Labs, Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package ru.f7cloud.talk.activities;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
|
||||
public interface ActionBarProvider {
|
||||
ActionBar getSupportActionBar();
|
||||
}
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2023 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2017-2019 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.activities
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.webkit.SslErrorHandler
|
||||
import android.widget.EditText
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import autodagger.AutoInjector
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.account.AccountVerificationActivity
|
||||
import ru.f7cloud.talk.account.BrowserLoginActivity
|
||||
import ru.f7cloud.talk.account.ServerSelectionActivity
|
||||
import ru.f7cloud.talk.account.SwitchAccountActivity
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication
|
||||
import ru.f7cloud.talk.chat.ChatActivity
|
||||
import ru.f7cloud.talk.events.CertificateEvent
|
||||
import ru.f7cloud.talk.ui.theme.ViewThemeUtils
|
||||
import ru.f7cloud.talk.utils.DisplayUtils
|
||||
import ru.f7cloud.talk.utils.FileViewerUtils
|
||||
import ru.f7cloud.talk.utils.UriUtils
|
||||
import ru.f7cloud.talk.utils.adjustUIForAPILevel35
|
||||
import ru.f7cloud.talk.utils.bundle.BundleKeys
|
||||
import ru.f7cloud.talk.utils.database.user.CurrentUserProvider
|
||||
import ru.f7cloud.talk.utils.database.user.CurrentUserProviderOld
|
||||
import ru.f7cloud.talk.utils.preferences.AppPreferences
|
||||
import ru.f7cloud.talk.utils.ssl.TrustManager
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import java.security.cert.CertificateParsingException
|
||||
import java.security.cert.X509Certificate
|
||||
import java.text.DateFormat
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(F7cloudTalkApplication::class)
|
||||
open class BaseActivity : AppCompatActivity() {
|
||||
|
||||
enum class AppBarLayoutType {
|
||||
TOOLBAR,
|
||||
SEARCH_BAR,
|
||||
EMPTY
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var eventBus: EventBus
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var context: Context
|
||||
|
||||
@Deprecated("Use CurrentUserProvider instead")
|
||||
@Inject
|
||||
lateinit var currentUserProviderOld: CurrentUserProviderOld
|
||||
|
||||
@Inject
|
||||
lateinit var currentUserProvider: CurrentUserProvider
|
||||
|
||||
open val appBarLayoutType: AppBarLayoutType
|
||||
get() = AppBarLayoutType.TOOLBAR
|
||||
|
||||
open val view: View?
|
||||
get() = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
F7cloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
|
||||
adjustUIForAPILevel35()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
cleanTempCertPreference()
|
||||
}
|
||||
|
||||
public override fun onStart() {
|
||||
super.onStart()
|
||||
eventBus.register(this)
|
||||
}
|
||||
|
||||
public override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (appPreferences.isKeyboardIncognito) {
|
||||
val viewGroup = (findViewById<View>(android.R.id.content) as ViewGroup).getChildAt(0) as ViewGroup
|
||||
disableKeyboardPersonalisedLearning(viewGroup)
|
||||
}
|
||||
|
||||
if (appPreferences.isScreenSecured || appPreferences.isScreenLocked) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
} else {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onStop() {
|
||||
super.onStop()
|
||||
eventBus.unregister(this)
|
||||
}
|
||||
|
||||
/*
|
||||
* May be aligned with android-common lib in the future: .../ui/util/extensions/AppCompatActivityExtensions.kt
|
||||
*/
|
||||
fun initSystemBars() {
|
||||
val decorView = window.decorView
|
||||
decorView.setOnApplyWindowInsetsListener { view, insets ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
val systemBars = insets.getInsets(
|
||||
WindowInsetsCompat.Type.systemBars() or
|
||||
WindowInsetsCompat.Type.displayCutout()
|
||||
)
|
||||
val color = ResourcesCompat.getColor(resources, R.color.bg_default, context.theme)
|
||||
view.setBackgroundColor(color)
|
||||
view.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||
} else {
|
||||
colorizeStatusBar()
|
||||
colorizeNavigationBar()
|
||||
}
|
||||
insets
|
||||
}
|
||||
ViewCompat.requestApplyInsets(decorView)
|
||||
}
|
||||
|
||||
open fun colorizeStatusBar() {
|
||||
if (resources != null) {
|
||||
if (appBarLayoutType == AppBarLayoutType.SEARCH_BAR) {
|
||||
viewThemeUtils.platform.resetStatusBar(this)
|
||||
} else {
|
||||
viewThemeUtils.platform.themeStatusBar(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun colorizeNavigationBar() {
|
||||
if (resources != null) {
|
||||
DisplayUtils.applyColorToNavigationBar(
|
||||
this.window,
|
||||
ResourcesCompat.getColor(resources, R.color.bg_default, null)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun disableKeyboardPersonalisedLearning(viewGroup: ViewGroup) {
|
||||
var view: View?
|
||||
var editText: EditText
|
||||
for (i in 0 until viewGroup.childCount) {
|
||||
view = viewGroup.getChildAt(i)
|
||||
if (view is EditText) {
|
||||
editText = view
|
||||
editText.imeOptions = editText.imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
|
||||
} else if (view is ViewGroup) {
|
||||
disableKeyboardPersonalisedLearning(view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("Detekt.NestedBlockDepth")
|
||||
private fun showCertificateDialog(
|
||||
cert: X509Certificate,
|
||||
trustManager: TrustManager,
|
||||
sslErrorHandler: SslErrorHandler?
|
||||
) {
|
||||
val formatter = DateFormat.getDateInstance(DateFormat.LONG)
|
||||
val validFrom = formatter.format(cert.notBefore)
|
||||
val validUntil = formatter.format(cert.notAfter)
|
||||
|
||||
val issuedBy = cert.issuerDN.toString()
|
||||
val issuedFor: String
|
||||
|
||||
try {
|
||||
if (cert.subjectAlternativeNames != null) {
|
||||
val stringBuilder = StringBuilder()
|
||||
for (o in cert.subjectAlternativeNames) {
|
||||
val list = o as List<*>
|
||||
val type = list[0] as Int
|
||||
if (type == 2) {
|
||||
val name = list[1] as String
|
||||
stringBuilder.append("[").append(type).append("]").append(name).append(" ")
|
||||
}
|
||||
}
|
||||
issuedFor = stringBuilder.toString()
|
||||
} else {
|
||||
issuedFor = cert.subjectDN.name
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatMatches")
|
||||
val dialogText = String.format(
|
||||
resources.getString(R.string.nc_certificate_dialog_text),
|
||||
issuedBy,
|
||||
issuedFor,
|
||||
validFrom,
|
||||
validUntil
|
||||
)
|
||||
|
||||
val dialogBuilder = MaterialAlertDialogBuilder(this).setIcon(
|
||||
viewThemeUtils.dialog.colorMaterialAlertDialogIcon(
|
||||
context,
|
||||
R.drawable.ic_security_white_24dp
|
||||
)
|
||||
).setTitle(R.string.nc_certificate_dialog_title)
|
||||
.setMessage(dialogText)
|
||||
.setPositiveButton(R.string.nc_yes) { _, _ ->
|
||||
trustManager.addCertInTrustStore(cert)
|
||||
sslErrorHandler?.proceed()
|
||||
}.setNegativeButton(R.string.nc_no) { _, _ ->
|
||||
sslErrorHandler?.cancel()
|
||||
}
|
||||
|
||||
viewThemeUtils.dialog.colorMaterialAlertDialogBackground(context, dialogBuilder)
|
||||
|
||||
val dialog = dialogBuilder.show()
|
||||
|
||||
viewThemeUtils.platform.colorTextButtons(
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE),
|
||||
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)
|
||||
)
|
||||
} catch (e: CertificateParsingException) {
|
||||
Log.d(TAG, "Failed to parse the certificate")
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanTempCertPreference() {
|
||||
val temporaryClassNames: MutableList<String> = ArrayList()
|
||||
temporaryClassNames.add(ServerSelectionActivity::class.java.name)
|
||||
temporaryClassNames.add(AccountVerificationActivity::class.java.name)
|
||||
temporaryClassNames.add(BrowserLoginActivity::class.java.name)
|
||||
temporaryClassNames.add(SwitchAccountActivity::class.java.name)
|
||||
if (!temporaryClassNames.contains(javaClass.name)) {
|
||||
appPreferences.removeTemporaryClientCertAlias()
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onMessageEvent(event: CertificateEvent) {
|
||||
showCertificateDialog(event.x509Certificate, event.trustManager, event.sslErrorHandler)
|
||||
}
|
||||
|
||||
override fun startActivity(intent: Intent) {
|
||||
val user = currentUserProviderOld.currentUser.blockingGet()
|
||||
if (intent.data != null && TextUtils.equals(intent.action, Intent.ACTION_VIEW)) {
|
||||
val uri = intent.data.toString()
|
||||
if (user?.baseUrl != null && uri.startsWith(user.baseUrl!!)) {
|
||||
if (UriUtils.isInstanceInternalFileShareUrl(user.baseUrl!!, uri)) {
|
||||
// https://cloud.f7cloud.com/f/41
|
||||
val fileViewerUtils = FileViewerUtils(applicationContext, user)
|
||||
fileViewerUtils.openFileInFilesApp(uri, UriUtils.extractInstanceInternalFileShareFileId(uri))
|
||||
} else if (UriUtils.isInstanceInternalFileUrl(user.baseUrl!!, uri)) {
|
||||
// https://cloud.f7cloud.com/apps/files/?dir=/Engineering&fileid=41
|
||||
val fileViewerUtils = FileViewerUtils(applicationContext, user)
|
||||
fileViewerUtils.openFileInFilesApp(uri, UriUtils.extractInstanceInternalFileFileId(uri))
|
||||
} else if (UriUtils.isInstanceInternalFileUrlNew(user.baseUrl!!, uri)) {
|
||||
// https://cloud.f7cloud.com/apps/files/?dir=/Engineering&fileid=41
|
||||
val fileViewerUtils = FileViewerUtils(applicationContext, user)
|
||||
fileViewerUtils.openFileInFilesApp(uri, UriUtils.extractInstanceInternalFileFileIdNew(uri))
|
||||
} else if (UriUtils.isInstanceInternalTalkUrl(user.baseUrl!!, uri)) {
|
||||
// https://cloud.f7cloud.com/call/123456789
|
||||
val bundle = Bundle()
|
||||
bundle.putString(BundleKeys.KEY_ROOM_TOKEN, UriUtils.extractRoomTokenFromTalkUrl(uri))
|
||||
val chatIntent = Intent(context, ChatActivity::class.java)
|
||||
chatIntent.putExtras(bundle)
|
||||
chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
startActivity(chatIntent)
|
||||
} else {
|
||||
super.startActivity(intent)
|
||||
}
|
||||
} else {
|
||||
super.startActivity(intent)
|
||||
}
|
||||
} else {
|
||||
super.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = BaseActivity::class.java.simpleName
|
||||
}
|
||||
}
|
||||
+3129
File diff suppressed because it is too large
Load Diff
+163
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.activities;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AppOpsManager;
|
||||
import android.app.KeyguardManager;
|
||||
import android.app.PictureInPictureParams;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.PowerManager;
|
||||
import android.util.Log;
|
||||
import android.util.Rational;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import ru.f7cloud.talk.BuildConfig;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
|
||||
public abstract class CallBaseActivity extends BaseActivity {
|
||||
|
||||
public static final String TAG = "CallBaseActivity";
|
||||
|
||||
public PictureInPictureParams.Builder mPictureInPictureParamsBuilder;
|
||||
public Boolean isInPipMode = Boolean.FALSE;
|
||||
long onCreateTime;
|
||||
|
||||
|
||||
private OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
if (isPipModePossible()) {
|
||||
enterPipMode();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
onCreateTime = System.currentTimeMillis();
|
||||
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
dismissKeyguard();
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
|
||||
if (isPipModePossible()) {
|
||||
mPictureInPictureParamsBuilder = new PictureInPictureParams.Builder();
|
||||
}
|
||||
|
||||
getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback);
|
||||
}
|
||||
|
||||
public void hideNavigationIfNoPipAvailable(){
|
||||
if (!isPipModePossible()) {
|
||||
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
|
||||
suppressFitsSystemWindows();
|
||||
}
|
||||
}
|
||||
|
||||
void dismissKeyguard() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
setShowWhenLocked(true);
|
||||
setTurnScreenOn(true);
|
||||
KeyguardManager keyguardManager = (KeyguardManager) getSystemService(KEYGUARD_SERVICE);
|
||||
keyguardManager.requestDismissKeyguard(this, null);
|
||||
} else {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
|
||||
}
|
||||
}
|
||||
|
||||
void enableKeyguard() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
setShowWhenLocked(false);
|
||||
} else {
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
if (shouldFinishOnStop()) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUserLeaveHint() {
|
||||
super.onUserLeaveHint();
|
||||
long onUserLeaveHintTime = System.currentTimeMillis();
|
||||
long diff = onUserLeaveHintTime - onCreateTime;
|
||||
Log.d(TAG, "onUserLeaveHintTime - onCreateTime: " + diff);
|
||||
|
||||
if (diff < 3000) {
|
||||
Log.d(TAG, "enterPipMode skipped");
|
||||
} else {
|
||||
enterPipMode();
|
||||
}
|
||||
}
|
||||
|
||||
void enterPipMode() {
|
||||
enableKeyguard();
|
||||
if (isPipModePossible()) {
|
||||
Rational pipRatio = new Rational(300, 500);
|
||||
mPictureInPictureParamsBuilder.setAspectRatio(pipRatio);
|
||||
enterPictureInPictureMode(mPictureInPictureParamsBuilder.build());
|
||||
} else {
|
||||
// we don't support other solutions than PIP to have a call in the background.
|
||||
// If PIP is not available the call is ended when user presses the home button.
|
||||
Log.d(TAG, "Activity was finished because PIP is not available.");
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
boolean isPipModePossible() {
|
||||
boolean deviceHasPipFeature = getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
|
||||
|
||||
AppOpsManager appOpsManager = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE);
|
||||
boolean isPipFeatureGranted = appOpsManager.checkOpNoThrow(
|
||||
AppOpsManager.OPSTR_PICTURE_IN_PICTURE,
|
||||
android.os.Process.myUid(),
|
||||
BuildConfig.APPLICATION_ID) == AppOpsManager.MODE_ALLOWED;
|
||||
return deviceHasPipFeature && isPipFeatureGranted;
|
||||
}
|
||||
|
||||
private boolean shouldFinishOnStop() {
|
||||
if (!isInPipMode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
|
||||
if (powerManager == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return powerManager.isInteractive();
|
||||
}
|
||||
|
||||
public abstract void updateUiForPipMode();
|
||||
|
||||
public abstract void updateUiForNormalMode();
|
||||
|
||||
public abstract void suppressFitsSystemWindows();
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.activities
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
enum class CallStatus : Parcelable {
|
||||
CONNECTING,
|
||||
CALLING_TIMEOUT,
|
||||
JOINED,
|
||||
IN_CONVERSATION,
|
||||
RECONNECTING,
|
||||
OFFLINE,
|
||||
LEAVING,
|
||||
PUBLISHER_FAILED
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.activities
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import ru.f7cloud.talk.signaling.SignalingMessageReceiver
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
|
||||
class CallViewModel @Inject constructor() : ViewModel() {
|
||||
|
||||
private val participantHandlers: MutableMap<String, ParticipantHandler> = ConcurrentHashMap()
|
||||
|
||||
private val _participants = MutableStateFlow<List<ParticipantUiState>>(emptyList())
|
||||
val participants: StateFlow<List<ParticipantUiState>> = _participants.asStateFlow()
|
||||
|
||||
private val _activeScreenShareSession = MutableStateFlow<ParticipantUiState?>(null)
|
||||
val activeScreenShareSession: StateFlow<ParticipantUiState?> = _activeScreenShareSession.asStateFlow()
|
||||
|
||||
fun getParticipant(sessionId: String?): ParticipantHandler? {
|
||||
if (sessionId == null) {
|
||||
Log.w(TAG, "Attempted to get participant with null sessionId.")
|
||||
return null
|
||||
}
|
||||
return participantHandlers[sessionId]
|
||||
}
|
||||
|
||||
fun doesParticipantExist(sessionId: String?): Boolean = (participantHandlers.containsKey(sessionId))
|
||||
|
||||
fun addParticipant(
|
||||
baseUrl: String,
|
||||
roomToken: String,
|
||||
sessionId: String,
|
||||
signalingMessageReceiver: SignalingMessageReceiver
|
||||
) {
|
||||
if (participantHandlers.containsKey(sessionId)) return
|
||||
|
||||
val participantHandler = ParticipantHandler(
|
||||
sessionId,
|
||||
baseUrl,
|
||||
roomToken,
|
||||
signalingMessageReceiver,
|
||||
onParticipantShareScreen = {
|
||||
onShareScreen(it)
|
||||
},
|
||||
onParticipantUnshareScreen = {
|
||||
onUnshareScreen(it)
|
||||
}
|
||||
)
|
||||
participantHandlers[sessionId] = participantHandler
|
||||
|
||||
viewModelScope.launch {
|
||||
participantHandler.uiState.collect {
|
||||
_participants.value = participantHandlers.values.map { it.uiState.value }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onShareScreen(sessionId: String?) {
|
||||
setActiveScreenShareSession(sessionId)
|
||||
}
|
||||
|
||||
fun onUnshareScreen(sessionId: String?) {
|
||||
if (_activeScreenShareSession.value?.sessionKey.equals(sessionId)) {
|
||||
setActiveScreenShareSession(null)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeParticipant(sessionId: String) {
|
||||
participantHandlers[sessionId]?.destroy()
|
||||
participantHandlers.remove(sessionId)
|
||||
_participants.value = participantHandlers.values.map { it.uiState.value }
|
||||
}
|
||||
|
||||
fun setActiveScreenShareSession(session: String?) {
|
||||
_activeScreenShareSession.value = session?.let {
|
||||
participantHandlers[it]?.uiState?.value
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onCleared() {
|
||||
participantHandlers.values.forEach { it.destroy() }
|
||||
participantHandlers.clear()
|
||||
_participants.value = emptyList()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = CallViewModel::class.java.simpleName
|
||||
}
|
||||
}
|
||||
+289
@@ -0,0 +1,289 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2023 Ezhil Shanmugham <ezhil56x.contact@gmail.com>
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <infoi@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.activities
|
||||
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.provider.ContactsContract
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import autodagger.AutoInjector
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.account.BrowserLoginActivity
|
||||
import ru.f7cloud.talk.account.ServerSelectionActivity
|
||||
import ru.f7cloud.talk.api.NcApi
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication
|
||||
import ru.f7cloud.talk.chat.ChatActivity
|
||||
import ru.f7cloud.talk.conversationlist.ConversationsListActivity
|
||||
import ru.f7cloud.talk.data.user.model.User
|
||||
import ru.f7cloud.talk.databinding.ActivityMainBinding
|
||||
import ru.f7cloud.talk.invitation.InvitationsActivity
|
||||
import ru.f7cloud.talk.lock.LockedActivity
|
||||
import ru.f7cloud.talk.models.json.conversations.RoomOverall
|
||||
import ru.f7cloud.talk.users.UserManager
|
||||
import ru.f7cloud.talk.utils.ApiUtils
|
||||
import ru.f7cloud.talk.utils.ClosedInterfaceImpl
|
||||
import ru.f7cloud.talk.utils.SecurityUtils
|
||||
import ru.f7cloud.talk.utils.bundle.BundleKeys
|
||||
import ru.f7cloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
|
||||
import io.reactivex.Observer
|
||||
import io.reactivex.SingleObserver
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(F7cloudTalkApplication::class)
|
||||
class MainActivity :
|
||||
BaseActivity(),
|
||||
ActionBarProvider {
|
||||
|
||||
lateinit var binding: ActivityMainBinding
|
||||
|
||||
@Inject
|
||||
lateinit var ncApi: NcApi
|
||||
|
||||
@Inject
|
||||
lateinit var userManager: UserManager
|
||||
|
||||
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Log.d(TAG, "onCreate: Activity: " + System.identityHashCode(this).toString())
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
lockScreenIfConditionsApply()
|
||||
}
|
||||
})
|
||||
|
||||
// Set the default theme to replace the launch screen theme.
|
||||
setTheme(R.style.AppTheme)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
F7cloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
|
||||
|
||||
setSupportActionBar(binding.toolbar)
|
||||
|
||||
handleIntent(intent)
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
}
|
||||
|
||||
fun lockScreenIfConditionsApply() {
|
||||
val keyguardManager = getSystemService(KEYGUARD_SERVICE) as KeyguardManager
|
||||
if (keyguardManager.isKeyguardSecure && appPreferences.isScreenLocked) {
|
||||
if (!SecurityUtils.checkIfWeAreAuthenticated(appPreferences.screenLockTimeout)) {
|
||||
val lockIntent = Intent(context, LockedActivity::class.java)
|
||||
startActivity(lockIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchServerSelection() {
|
||||
if (isBrandingUrlSet()) {
|
||||
val intent = Intent(context, BrowserLoginActivity::class.java)
|
||||
val bundle = Bundle()
|
||||
bundle.putString(BundleKeys.KEY_BASE_URL, resources.getString(R.string.weblogin_url))
|
||||
intent.putExtras(bundle)
|
||||
startActivity(intent)
|
||||
} else {
|
||||
val intent = Intent(context, ServerSelectionActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isBrandingUrlSet() = !TextUtils.isEmpty(resources.getString(R.string.weblogin_url))
|
||||
|
||||
override fun onStart() {
|
||||
Log.d(TAG, "onStart: Activity: " + System.identityHashCode(this).toString())
|
||||
super.onStart()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
Log.d(TAG, "onResume: Activity: " + System.identityHashCode(this).toString())
|
||||
super.onResume()
|
||||
|
||||
if (appPreferences.isScreenLocked) {
|
||||
SecurityUtils.createKey(appPreferences.screenLockTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
Log.d(TAG, "onPause: Activity: " + System.identityHashCode(this).toString())
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
Log.d(TAG, "onStop: Activity: " + System.identityHashCode(this).toString())
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
private fun openConversationList() {
|
||||
val intent = Intent(this, ConversationsListActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
intent.putExtras(Bundle())
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun handleActionFromContact(intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_VIEW && intent.data != null) {
|
||||
val cursor = contentResolver.query(intent.data!!, null, null, null, null)
|
||||
|
||||
var userId = ""
|
||||
if (cursor != null) {
|
||||
if (cursor.moveToFirst()) {
|
||||
// userId @ server
|
||||
userId = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Data.DATA1))
|
||||
}
|
||||
|
||||
cursor.close()
|
||||
}
|
||||
|
||||
when (intent.type) {
|
||||
"vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat" -> {
|
||||
val user = userId.substringBeforeLast("@")
|
||||
val baseUrl = userId.substringAfterLast("@")
|
||||
|
||||
if (currentUserProviderOld.currentUser.blockingGet()?.baseUrl!!.endsWith(baseUrl) == true) {
|
||||
startConversation(user)
|
||||
} else {
|
||||
Snackbar.make(
|
||||
binding.root,
|
||||
R.string.nc_phone_book_integration_account_not_found,
|
||||
Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startConversation(userId: String) {
|
||||
val roomType = "1"
|
||||
|
||||
val currentUser = currentUserProviderOld.currentUser.blockingGet()
|
||||
|
||||
val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.API_V4, 1))
|
||||
val credentials = ApiUtils.getCredentials(currentUser?.username, currentUser?.token)
|
||||
val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
|
||||
version = apiVersion,
|
||||
baseUrl = currentUser?.baseUrl!!,
|
||||
roomType = roomType,
|
||||
invite = userId
|
||||
)
|
||||
|
||||
ncApi.createRoom(
|
||||
credentials,
|
||||
retrofitBucket.url,
|
||||
retrofitBucket.queryMap
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(object : Observer<RoomOverall> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onNext(roomOverall: RoomOverall) {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(KEY_ROOM_TOKEN, roomOverall.ocs!!.data!!.token)
|
||||
|
||||
val chatIntent = Intent(context, ChatActivity::class.java)
|
||||
chatIntent.putExtras(bundle)
|
||||
startActivity(chatIntent)
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
// unused atm
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
Log.d(TAG, "onNewIntent Activity: " + System.identityHashCode(this).toString())
|
||||
handleIntent(intent)
|
||||
}
|
||||
|
||||
private fun handleIntent(intent: Intent) {
|
||||
handleActionFromContact(intent)
|
||||
|
||||
val internalUserId = intent.extras?.getLong(BundleKeys.KEY_INTERNAL_USER_ID)
|
||||
|
||||
var user: User? = null
|
||||
if (internalUserId != null) {
|
||||
user = userManager.getUserWithId(internalUserId).blockingGet()
|
||||
}
|
||||
|
||||
if (user != null && userManager.setUserAsActive(user).blockingGet()) {
|
||||
if (intent.hasExtra(BundleKeys.KEY_REMOTE_TALK_SHARE)) {
|
||||
if (intent.getBooleanExtra(BundleKeys.KEY_REMOTE_TALK_SHARE, false)) {
|
||||
val invitationsIntent = Intent(this, InvitationsActivity::class.java)
|
||||
startActivity(invitationsIntent)
|
||||
}
|
||||
} else {
|
||||
val chatIntent = Intent(context, ChatActivity::class.java)
|
||||
chatIntent.putExtras(intent.extras!!)
|
||||
startActivity(chatIntent)
|
||||
}
|
||||
} else {
|
||||
userManager.users.subscribe(object : SingleObserver<List<User>> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onSuccess(users: List<User>) {
|
||||
if (users.isNotEmpty()) {
|
||||
ClosedInterfaceImpl().setUpPushTokenRegistration()
|
||||
runOnUiThread {
|
||||
openConversationList()
|
||||
}
|
||||
} else {
|
||||
runOnUiThread {
|
||||
launchServerSelection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
Log.e(TAG, "Error loading existing users", e)
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.resources.getString(R.string.nc_common_error_sorry),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = MainActivity::class.java.simpleName
|
||||
}
|
||||
}
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package ru.f7cloud.talk.activities
|
||||
|
||||
import android.util.Log
|
||||
import ru.f7cloud.talk.models.json.participants.Participant
|
||||
import ru.f7cloud.talk.signaling.SignalingMessageReceiver
|
||||
import ru.f7cloud.talk.webrtc.PeerConnectionWrapper
|
||||
import ru.f7cloud.talk.webrtc.PeerConnectionWrapper.DataChannelMessageListener
|
||||
import ru.f7cloud.talk.webrtc.PeerConnectionWrapper.PeerConnectionObserver
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.webrtc.MediaStream
|
||||
import org.webrtc.PeerConnection.IceConnectionState
|
||||
|
||||
class ParticipantHandler(
|
||||
private val sessionId: String,
|
||||
val baseUrl: String,
|
||||
val roomToken: String,
|
||||
private val signalingMessageReceiver: SignalingMessageReceiver,
|
||||
onParticipantShareScreen: ((String?) -> Unit?),
|
||||
onParticipantUnshareScreen: ((String?) -> Unit?)
|
||||
) {
|
||||
private val _uiState = MutableStateFlow(
|
||||
ParticipantUiState(
|
||||
sessionKey = sessionId,
|
||||
baseUrl = baseUrl,
|
||||
roomToken = roomToken,
|
||||
nick = "Guest",
|
||||
isConnected = true,
|
||||
isAudioEnabled = false,
|
||||
isStreamEnabled = false,
|
||||
isScreenStreamEnabled = false,
|
||||
raisedHand = false,
|
||||
isInternal = false
|
||||
)
|
||||
)
|
||||
val uiState: StateFlow<ParticipantUiState> = _uiState.asStateFlow()
|
||||
|
||||
private var peerConnection: PeerConnectionWrapper? = null
|
||||
private var screenPeerConnection: PeerConnectionWrapper? = null
|
||||
|
||||
private val peerConnectionObserver: PeerConnectionObserver = object : PeerConnectionObserver {
|
||||
override fun onStreamAdded(mediaStream: MediaStream?) {
|
||||
handleStreamChange(mediaStream)
|
||||
}
|
||||
|
||||
override fun onStreamRemoved(mediaStream: MediaStream?) {
|
||||
handleStreamChange(mediaStream)
|
||||
}
|
||||
|
||||
override fun onIceConnectionStateChanged(iceConnectionState: IceConnectionState?) {
|
||||
Log.d(TAG, "onIceConnectionStateChanged " + _uiState.value.nick + " " + iceConnectionState)
|
||||
handleIceConnectionStateChange(iceConnectionState)
|
||||
}
|
||||
}
|
||||
|
||||
private val screenPeerConnectionObserver: PeerConnectionObserver = object : PeerConnectionObserver {
|
||||
override fun onStreamAdded(mediaStream: MediaStream?) {
|
||||
handleScreenStreamChange(mediaStream)
|
||||
onParticipantShareScreen.invoke(_uiState.value.sessionKey)
|
||||
}
|
||||
|
||||
override fun onStreamRemoved(mediaStream: MediaStream?) {
|
||||
handleScreenStreamChange(mediaStream)
|
||||
}
|
||||
|
||||
override fun onIceConnectionStateChanged(iceConnectionState: IceConnectionState?) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleStreamChange(mediaStream: MediaStream?) {
|
||||
val hasAtLeastOneVideoStream = mediaStream?.videoTracks?.isNotEmpty() == true
|
||||
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
mediaStream = mediaStream,
|
||||
isStreamEnabled = hasAtLeastOneVideoStream
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleScreenStreamChange(mediaStream: MediaStream?) {
|
||||
val hasAtLeastOneVideoStream = mediaStream?.videoTracks?.isNotEmpty() == true
|
||||
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
screenMediaStream = mediaStream,
|
||||
isScreenStreamEnabled = hasAtLeastOneVideoStream
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleIceConnectionStateChange(iceConnectionState: IceConnectionState?) {
|
||||
Log.d(TAG, "handleIceConnectionStateChange " + _uiState.value.nick + " " + iceConnectionState)
|
||||
|
||||
if (iceConnectionState == IceConnectionState.NEW ||
|
||||
iceConnectionState == IceConnectionState.CHECKING
|
||||
) {
|
||||
_uiState.update { it.copy(isAudioEnabled = false) }
|
||||
_uiState.update { it.copy(isStreamEnabled = false) }
|
||||
}
|
||||
|
||||
_uiState.update { it.copy(isConnected = isConnected(iceConnectionState)) }
|
||||
}
|
||||
|
||||
private val dataChannelMessageListener: DataChannelMessageListener = object : DataChannelMessageListener {
|
||||
override fun onAudioOn() {
|
||||
_uiState.update { it.copy(isAudioEnabled = true) }
|
||||
}
|
||||
|
||||
override fun onAudioOff() {
|
||||
_uiState.update { it.copy(isAudioEnabled = false) }
|
||||
}
|
||||
|
||||
override fun onVideoOn() {
|
||||
_uiState.update { it.copy(isStreamEnabled = true) }
|
||||
}
|
||||
|
||||
override fun onVideoOff() {
|
||||
_uiState.update { it.copy(isStreamEnabled = false) }
|
||||
}
|
||||
|
||||
override fun onNickChanged(nick: String?) {
|
||||
_uiState.update { it.copy(nick = nick) }
|
||||
}
|
||||
}
|
||||
|
||||
private val listener = object : SignalingMessageReceiver.CallParticipantMessageListener {
|
||||
override fun onRaiseHand(state: Boolean, timestamp: Long) {
|
||||
_uiState.update { it.copy(raisedHand = state) }
|
||||
}
|
||||
|
||||
override fun onReaction(reaction: String?) {
|
||||
Log.d(TAG, "onReaction")
|
||||
}
|
||||
|
||||
override fun onUnshareScreen() {
|
||||
handleScreenStreamChange(null)
|
||||
onParticipantUnshareScreen.invoke(_uiState.value.sessionKey)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
signalingMessageReceiver.addListener(listener, sessionId)
|
||||
}
|
||||
|
||||
fun setPeerConnection(peerConnection: PeerConnectionWrapper?) {
|
||||
this.peerConnection?.let {
|
||||
it.removeObserver(peerConnectionObserver)
|
||||
it.removeListener(dataChannelMessageListener)
|
||||
}
|
||||
|
||||
this.peerConnection = peerConnection
|
||||
|
||||
if (this.peerConnection == null) {
|
||||
// special case when participant has no permission. -> no streams are transmitted but he must be shown as
|
||||
// connected
|
||||
_uiState.update { it.copy(mediaStream = null) }
|
||||
_uiState.update { it.copy(isAudioEnabled = false) }
|
||||
_uiState.update { it.copy(isStreamEnabled = false) }
|
||||
_uiState.update { it.copy(isConnected = true) }
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(
|
||||
TAG,
|
||||
"setPeerConnection " + _uiState.value.nick + " " +
|
||||
this.peerConnection?.peerConnection?.iceConnectionState()
|
||||
)
|
||||
|
||||
handleIceConnectionStateChange(this.peerConnection?.peerConnection?.iceConnectionState())
|
||||
handleStreamChange(this.peerConnection?.stream)
|
||||
|
||||
this.peerConnection?.addObserver(peerConnectionObserver)
|
||||
this.peerConnection?.addListener(dataChannelMessageListener)
|
||||
}
|
||||
|
||||
fun setScreenPeerConnection(screenPeerConnectionWrapper: PeerConnectionWrapper?) {
|
||||
this.screenPeerConnection?.removeObserver(screenPeerConnectionObserver)
|
||||
|
||||
this.screenPeerConnection = screenPeerConnectionWrapper
|
||||
|
||||
if (this.screenPeerConnection == null) {
|
||||
_uiState.update { it.copy(screenMediaStream = null) }
|
||||
return
|
||||
}
|
||||
|
||||
_uiState.update { it.copy(screenMediaStream = screenPeerConnection?.stream) }
|
||||
|
||||
this.screenPeerConnection?.addObserver(screenPeerConnectionObserver)
|
||||
}
|
||||
|
||||
fun isConnected(iceConnectionState: IceConnectionState?): Boolean =
|
||||
iceConnectionState == IceConnectionState.CONNECTED ||
|
||||
iceConnectionState == IceConnectionState.COMPLETED ||
|
||||
// If there is no connection state that means that no connection is needed,
|
||||
// so it is a special case that is also seen as "connected".
|
||||
iceConnectionState == null
|
||||
|
||||
fun updateNick(nick: String?) = _uiState.update { it.copy(nick = nick ?: "Guest") }
|
||||
|
||||
fun updateIsInternal(isInternal: Boolean) = _uiState.update { it.copy(isInternal = isInternal) }
|
||||
|
||||
fun updateActor(actorType: Participant.ActorType?, actorId: String?) {
|
||||
_uiState.update { it.copy(actorType = actorType, actorId = actorId) }
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
signalingMessageReceiver.removeListener(listener)
|
||||
|
||||
if (peerConnection != null) {
|
||||
peerConnection!!.removeObserver(peerConnectionObserver)
|
||||
peerConnection!!.removeListener(dataChannelMessageListener)
|
||||
}
|
||||
if (screenPeerConnection != null) {
|
||||
screenPeerConnection!!.removeObserver(screenPeerConnectionObserver)
|
||||
}
|
||||
|
||||
peerConnection = null
|
||||
screenPeerConnection = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = ParticipantHandler::class.java.simpleName
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package ru.f7cloud.talk.activities
|
||||
|
||||
import ru.f7cloud.talk.models.json.participants.Participant
|
||||
import org.webrtc.MediaStream
|
||||
|
||||
data class ParticipantUiState(
|
||||
val sessionKey: String?,
|
||||
val baseUrl: String,
|
||||
val roomToken: String,
|
||||
val nick: String?,
|
||||
val isConnected: Boolean,
|
||||
val isAudioEnabled: Boolean,
|
||||
val isStreamEnabled: Boolean,
|
||||
val mediaStream: MediaStream? = null,
|
||||
val isScreenStreamEnabled: Boolean,
|
||||
val screenMediaStream: MediaStream? = null,
|
||||
val raisedHand: Boolean,
|
||||
val actorType: Participant.ActorType? = null,
|
||||
val actorId: String? = null,
|
||||
val isInternal: Boolean
|
||||
)
|
||||
+427
@@ -0,0 +1,427 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2021 Stefan Niedermann <info@niedermann.it>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.activities;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.hardware.camera2.CameraMetadata;
|
||||
import android.hardware.camera2.CaptureRequest;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.util.Size;
|
||||
import android.view.OrientationEventListener;
|
||||
import android.view.ScaleGestureDetector;
|
||||
import android.view.Surface;
|
||||
import android.view.View;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import ru.f7cloud.talk.R;
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication;
|
||||
import ru.f7cloud.talk.databinding.ActivityTakePictureBinding;
|
||||
import ru.f7cloud.talk.models.TakePictureViewModel;
|
||||
import ru.f7cloud.talk.ui.theme.ViewThemeUtils;
|
||||
import ru.f7cloud.talk.utils.BitmapShrinker;
|
||||
import ru.f7cloud.talk.utils.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.camera.camera2.interop.Camera2Interop;
|
||||
import androidx.camera.core.AspectRatio;
|
||||
import androidx.camera.core.Camera;
|
||||
import androidx.camera.core.ImageCapture;
|
||||
import androidx.camera.core.ImageCaptureException;
|
||||
import androidx.camera.core.Preview;
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.exifinterface.media.ExifInterface;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import autodagger.AutoInjector;
|
||||
|
||||
import static ru.f7cloud.talk.utils.Mimetype.IMAGE_JPEG;
|
||||
|
||||
@AutoInjector(F7cloudTalkApplication.class)
|
||||
public class TakePhotoActivity extends AppCompatActivity {
|
||||
private static final String TAG = TakePhotoActivity.class.getSimpleName();
|
||||
|
||||
private static final float MAX_SCALE = 6.0f;
|
||||
private static final float MEDIUM_SCALE = 2.45f;
|
||||
|
||||
private ActivityTakePictureBinding binding;
|
||||
private TakePictureViewModel viewModel;
|
||||
|
||||
private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;
|
||||
private OrientationEventListener orientationEventListener;
|
||||
|
||||
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss", Locale.ROOT);
|
||||
|
||||
private Camera camera;
|
||||
|
||||
@Inject
|
||||
ViewThemeUtils viewThemeUtils;
|
||||
|
||||
private OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
Uri uri = (Uri) binding.photoPreview.getTag();
|
||||
|
||||
if (uri != null) {
|
||||
File photoFile = new File(uri.getPath());
|
||||
if (!photoFile.delete()) {
|
||||
Log.w(TAG, "Error deleting temp camera image");
|
||||
}
|
||||
binding.photoPreview.setTag(null);
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
F7cloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
|
||||
|
||||
binding = ActivityTakePictureBinding.inflate(getLayoutInflater());
|
||||
viewModel = new ViewModelProvider(this).get(TakePictureViewModel.class);
|
||||
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
viewThemeUtils.material.themeFAB(binding.takePhoto);
|
||||
viewThemeUtils.material.colorMaterialButtonPrimaryFilled(binding.send);
|
||||
|
||||
cameraProviderFuture = ProcessCameraProvider.getInstance(this);
|
||||
cameraProviderFuture.addListener(() -> {
|
||||
try {
|
||||
final ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
|
||||
|
||||
camera = cameraProvider.bindToLifecycle(
|
||||
this,
|
||||
viewModel.getCameraSelector(),
|
||||
getImageCapture(
|
||||
viewModel.isCropEnabled().getValue(), viewModel.isLowResolutionEnabled().getValue()),
|
||||
getPreview(viewModel.isCropEnabled().getValue()));
|
||||
|
||||
viewModel.getTorchToggleButtonImageResource()
|
||||
.observe(
|
||||
this,
|
||||
res -> binding.toggleTorch.setIcon(ContextCompat.getDrawable(this, res)));
|
||||
viewModel.isTorchEnabled()
|
||||
.observe(
|
||||
this,
|
||||
enabled -> camera.getCameraControl().enableTorch(viewModel.isTorchEnabled().getValue()));
|
||||
binding.toggleTorch.setOnClickListener((v) -> viewModel.toggleTorchEnabled());
|
||||
|
||||
viewModel.getCropToggleButtonImageResource()
|
||||
.observe(
|
||||
this,
|
||||
res -> binding.toggleCrop.setIcon(ContextCompat.getDrawable(this, res)));
|
||||
viewModel.isCropEnabled()
|
||||
.observe(
|
||||
this,
|
||||
enabled -> {
|
||||
cameraProvider.unbindAll();
|
||||
camera = cameraProvider.bindToLifecycle(
|
||||
this,
|
||||
viewModel.getCameraSelector(),
|
||||
getImageCapture(
|
||||
viewModel.isCropEnabled().getValue(), viewModel.isLowResolutionEnabled().getValue()),
|
||||
getPreview(viewModel.isCropEnabled().getValue()));
|
||||
camera.getCameraControl().enableTorch(viewModel.isTorchEnabled().getValue());
|
||||
});
|
||||
binding.toggleCrop.setOnClickListener((v) -> viewModel.toggleCropEnabled());
|
||||
|
||||
viewModel.getLowResolutionToggleButtonImageResource()
|
||||
.observe(
|
||||
this,
|
||||
res -> binding.toggleLowres.setIcon(ContextCompat.getDrawable(this, res)));
|
||||
viewModel.isLowResolutionEnabled()
|
||||
.observe(
|
||||
this,
|
||||
enabled -> {
|
||||
cameraProvider.unbindAll();
|
||||
camera = cameraProvider.bindToLifecycle(
|
||||
this,
|
||||
viewModel.getCameraSelector(),
|
||||
getImageCapture(
|
||||
viewModel.isCropEnabled().getValue(), viewModel.isLowResolutionEnabled().getValue()),
|
||||
getPreview(viewModel.isCropEnabled().getValue()));
|
||||
camera.getCameraControl().enableTorch(viewModel.isTorchEnabled().getValue());
|
||||
});
|
||||
binding.toggleLowres.setOnClickListener((v) -> viewModel.toggleLowResolutionEnabled());
|
||||
|
||||
binding.switchCamera.setOnClickListener((v) -> {
|
||||
viewModel.toggleCameraSelector();
|
||||
cameraProvider.unbindAll();
|
||||
camera = cameraProvider.bindToLifecycle(
|
||||
this,
|
||||
viewModel.getCameraSelector(),
|
||||
getImageCapture(
|
||||
viewModel.isCropEnabled().getValue(), viewModel.isLowResolutionEnabled().getValue()),
|
||||
getPreview(viewModel.isCropEnabled().getValue()));
|
||||
});
|
||||
binding.retake.setOnClickListener((v) -> {
|
||||
Uri uri = (Uri) binding.photoPreview.getTag();
|
||||
File photoFile = new File(uri.getPath());
|
||||
if (!photoFile.delete()) {
|
||||
Log.w(TAG, "Error deleting temp camera image");
|
||||
}
|
||||
binding.takePhoto.setEnabled(true);
|
||||
binding.photoPreview.setTag(null);
|
||||
showCameraElements();
|
||||
});
|
||||
binding.send.setOnClickListener((v) -> {
|
||||
Uri uri = (Uri) binding.photoPreview.getTag();
|
||||
setResult(RESULT_OK, new Intent().setDataAndType(uri, IMAGE_JPEG));
|
||||
binding.photoPreview.setTag(null);
|
||||
finish();
|
||||
});
|
||||
|
||||
ScaleGestureDetector mDetector =
|
||||
new ScaleGestureDetector(this, new ScaleGestureDetector.SimpleOnScaleGestureListener(){
|
||||
@Override
|
||||
public boolean onScale(ScaleGestureDetector detector){
|
||||
float ratio = camera.getCameraInfo().getZoomState().getValue().getZoomRatio();
|
||||
float delta = detector.getScaleFactor();
|
||||
camera.getCameraControl().setZoomRatio(ratio * delta);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
binding.preview.setOnTouchListener((v, event) -> {
|
||||
v.performClick();
|
||||
mDetector.onTouchEvent(event);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Enable enlarging the image more than default 3x maximumScale.
|
||||
// Medium scale adapted to make double-tap behaviour more consistent.
|
||||
binding.photoPreview.setMaximumScale(MAX_SCALE);
|
||||
binding.photoPreview.setMediumScale(MEDIUM_SCALE);
|
||||
} catch (IllegalArgumentException | ExecutionException | InterruptedException e) {
|
||||
Log.e(TAG, "Error taking picture", e);
|
||||
Snackbar.make(binding.getRoot(), e.getMessage(), Snackbar.LENGTH_LONG).show();
|
||||
finish();
|
||||
}
|
||||
}, ContextCompat.getMainExecutor(this));
|
||||
|
||||
getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback);
|
||||
}
|
||||
|
||||
private void showCameraElements() {
|
||||
binding.send.setVisibility(View.GONE);
|
||||
binding.retake.setVisibility(View.GONE);
|
||||
binding.photoPreview.setVisibility(View.INVISIBLE);
|
||||
|
||||
binding.preview.setVisibility(View.VISIBLE);
|
||||
binding.takePhoto.setVisibility(View.VISIBLE);
|
||||
binding.switchCamera.setVisibility(View.VISIBLE);
|
||||
binding.toggleTorch.setVisibility(View.VISIBLE);
|
||||
binding.toggleCrop.setVisibility(View.VISIBLE);
|
||||
binding.toggleLowres.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private void showPictureProcessingElements() {
|
||||
binding.preview.setVisibility(View.INVISIBLE);
|
||||
binding.takePhoto.setVisibility(View.GONE);
|
||||
binding.switchCamera.setVisibility(View.GONE);
|
||||
binding.toggleTorch.setVisibility(View.GONE);
|
||||
binding.toggleCrop.setVisibility(View.GONE);
|
||||
binding.toggleLowres.setVisibility(View.GONE);
|
||||
|
||||
binding.send.setVisibility(View.VISIBLE);
|
||||
binding.retake.setVisibility(View.VISIBLE);
|
||||
binding.photoPreview.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private ImageCapture getImageCapture(Boolean crop, Boolean lowres) {
|
||||
final ImageCapture imageCapture;
|
||||
if (lowres) imageCapture = new ImageCapture.Builder()
|
||||
.setTargetResolution(new Size(crop ? 1080 : 1440, 1920)).build();
|
||||
else imageCapture = new ImageCapture.Builder()
|
||||
.setTargetAspectRatio(crop ? AspectRatio.RATIO_16_9 : AspectRatio.RATIO_4_3).build();
|
||||
|
||||
orientationEventListener = new OrientationEventListener(this) {
|
||||
@Override
|
||||
public void onOrientationChanged(int orientation) {
|
||||
int rotation;
|
||||
|
||||
// Monitors orientation values to determine the target rotation value
|
||||
if (orientation >= 45 && orientation < 135) {
|
||||
rotation = Surface.ROTATION_270;
|
||||
} else if (orientation >= 135 && orientation < 225) {
|
||||
rotation = Surface.ROTATION_180;
|
||||
} else if (orientation >= 225 && orientation < 315) {
|
||||
rotation = Surface.ROTATION_90;
|
||||
} else {
|
||||
rotation = Surface.ROTATION_0;
|
||||
}
|
||||
|
||||
imageCapture.setTargetRotation(rotation);
|
||||
}
|
||||
};
|
||||
orientationEventListener.enable();
|
||||
|
||||
binding.takePhoto.setOnClickListener((v) -> {
|
||||
binding.takePhoto.setEnabled(false);
|
||||
final String photoFileName = dateFormat.format(new Date()) + ".jpg";
|
||||
try {
|
||||
final File photoFile = FileUtils.getTempCacheFile(this, "photos/" + photoFileName);
|
||||
final ImageCapture.OutputFileOptions options =
|
||||
new ImageCapture.OutputFileOptions.Builder(photoFile).build();
|
||||
imageCapture.takePicture(
|
||||
options,
|
||||
ContextCompat.getMainExecutor(this),
|
||||
new ImageCapture.OnImageSavedCallback() {
|
||||
|
||||
@Override
|
||||
public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
|
||||
setPreviewImage(photoFile);
|
||||
showPictureProcessingElements();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@NonNull ImageCaptureException e) {
|
||||
Log.e(TAG, "Error", e);
|
||||
|
||||
if (!photoFile.delete()) {
|
||||
Log.w(TAG, "Deleting picture failed");
|
||||
}
|
||||
binding.takePhoto.setEnabled(true);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "error while taking picture", e);
|
||||
Snackbar.make(binding.getRoot(), R.string.take_photo_error_deleting_picture, Snackbar.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
|
||||
return imageCapture;
|
||||
}
|
||||
|
||||
private void setPreviewImage(File photoFile) {
|
||||
final Uri savedUri = Uri.fromFile(photoFile);
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
|
||||
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
|
||||
int doubleScreenWidth = displayMetrics.widthPixels * 2;
|
||||
int doubleScreenHeight = displayMetrics.heightPixels * 2;
|
||||
|
||||
Bitmap bitmap = BitmapShrinker.shrinkBitmap(photoFile.getAbsolutePath(),
|
||||
doubleScreenWidth,
|
||||
doubleScreenHeight);
|
||||
if (bitmap == null) {
|
||||
Log.e(TAG, "Preview bitmap could not be decoded from path: " + photoFile.getAbsolutePath());
|
||||
Snackbar.make(binding.getRoot(), R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
binding.photoPreview.setImageBitmap(bitmap);
|
||||
binding.photoPreview.setTag(savedUri);
|
||||
viewModel.disableTorchIfEnabled();
|
||||
}
|
||||
|
||||
public int getImageOrientation(File imageFile) {
|
||||
int rotate = 0;
|
||||
try {
|
||||
ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
|
||||
int orientation = exif.getAttributeInt(
|
||||
ExifInterface.TAG_ORIENTATION,
|
||||
ExifInterface.ORIENTATION_NORMAL);
|
||||
|
||||
switch (orientation) {
|
||||
case ExifInterface.ORIENTATION_ROTATE_270:
|
||||
rotate = 270;
|
||||
break;
|
||||
case ExifInterface.ORIENTATION_ROTATE_180:
|
||||
rotate = 180;
|
||||
break;
|
||||
case ExifInterface.ORIENTATION_ROTATE_90:
|
||||
rotate = 90;
|
||||
break;
|
||||
default:
|
||||
rotate = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
Log.i(TAG, "ImageOrientation - Exif orientation: " + orientation + " - " + "Rotate value: " + rotate);
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Error calculation rotation value");
|
||||
}
|
||||
return rotate;
|
||||
}
|
||||
|
||||
@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)
|
||||
private Preview getPreview(boolean crop) {
|
||||
Preview.Builder previewBuilder = new Preview.Builder()
|
||||
.setTargetAspectRatio(crop ? AspectRatio.RATIO_16_9 : AspectRatio.RATIO_4_3);
|
||||
new Camera2Interop.Extender<>(previewBuilder)
|
||||
.setCaptureRequestOption(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE,
|
||||
CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_OFF
|
||||
);
|
||||
|
||||
Preview preview = previewBuilder.build();
|
||||
preview.setSurfaceProvider(binding.preview.getSurfaceProvider());
|
||||
|
||||
return preview;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
if (this.orientationEventListener != null) {
|
||||
this.orientationEventListener.disable();
|
||||
}
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (this.orientationEventListener != null) {
|
||||
this.orientationEventListener.enable();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle savedInstanceState) {
|
||||
if (binding.photoPreview.getTag() != null) {
|
||||
savedInstanceState.putString("Uri", ((Uri) binding.photoPreview.getTag()).getPath());
|
||||
}
|
||||
|
||||
super.onSaveInstanceState(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(Bundle savedInstanceState) {
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
|
||||
String uri = savedInstanceState.getString("Uri", null);
|
||||
|
||||
if (uri != null) {
|
||||
File photoFile = new File(uri);
|
||||
setPreviewImage(photoFile);
|
||||
showPictureProcessingElements();
|
||||
}
|
||||
}
|
||||
|
||||
public static Intent createIntent(@NonNull Context context) {
|
||||
return new Intent(context, TakePhotoActivity.class).setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2021 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ru.f7cloud.talk.R
|
||||
import fr.dudie.nominatim.model.Address
|
||||
|
||||
class GeocodingAdapter(private val context: Context, private var dataSource: List<Address>) :
|
||||
RecyclerView.Adapter<GeocodingAdapter.ViewHolder>() {
|
||||
|
||||
interface OnItemClickListener {
|
||||
fun onItemClick(position: Int)
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun updateData(data: List<Address>) {
|
||||
this.dataSource = data
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private var listener: OnItemClickListener? = null
|
||||
fun setOnItemClickListener(listener: OnItemClickListener) {
|
||||
this.listener = listener
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
val view = inflater.inflate(R.layout.geocoding_item, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val address = dataSource[position]
|
||||
holder.nameView.text = address.displayName
|
||||
|
||||
holder.itemView.setOnClickListener {
|
||||
listener?.onItemClick(position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = dataSource.size
|
||||
|
||||
fun getItem(position: Int): Any = dataSource[position]
|
||||
|
||||
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val nameView: TextView = itemView.findViewById(R.id.name)
|
||||
}
|
||||
}
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Tim Krüger <t@timkrueger.me
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters
|
||||
|
||||
import ru.f7cloud.talk.models.json.status.predefined.PredefinedStatus
|
||||
|
||||
interface PredefinedStatusClickListener {
|
||||
fun onClick(predefinedStatus: PredefinedStatus)
|
||||
fun revertStatus()
|
||||
}
|
||||
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Tobias Kaminsky <tobias@kaminsky.me>
|
||||
* SPDX-FileCopyrightText: 2020 F7cloud
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ru.f7cloud.talk.databinding.PredefinedStatusBinding
|
||||
import ru.f7cloud.talk.models.json.status.predefined.PredefinedStatus
|
||||
|
||||
class PredefinedStatusListAdapter(
|
||||
private val clickListener: PredefinedStatusClickListener,
|
||||
val context: Context,
|
||||
var isBackupStatusAvailable: Boolean
|
||||
) : RecyclerView.Adapter<PredefinedStatusViewHolder>() {
|
||||
internal var list: List<PredefinedStatus> = emptyList()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PredefinedStatusViewHolder {
|
||||
val itemBinding = PredefinedStatusBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return PredefinedStatusViewHolder(itemBinding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PredefinedStatusViewHolder, position: Int) {
|
||||
holder.bind(list[position], clickListener, context, isBackupStatusAvailable)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = list.size
|
||||
}
|
||||
Vendored
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Tobias Kaminsky <tobias@kaminsky.me>
|
||||
* SPDX-FileCopyrightText: 2020 F7cloud
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.databinding.PredefinedStatusBinding
|
||||
import ru.f7cloud.talk.models.json.status.predefined.PredefinedStatus
|
||||
import ru.f7cloud.talk.utils.DisplayUtils
|
||||
|
||||
private const val ONE_SECOND_IN_MILLIS = 1000
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
class PredefinedStatusViewHolder(private val binding: PredefinedStatusBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(
|
||||
status: PredefinedStatus,
|
||||
clickListener: PredefinedStatusClickListener,
|
||||
context: Context,
|
||||
isBackupStatusAvailable: Boolean
|
||||
) {
|
||||
binding.root.setOnClickListener { clickListener.onClick(status) }
|
||||
binding.icon.text = status.icon
|
||||
binding.name.text = status.message
|
||||
|
||||
if (status.clearAt == null) {
|
||||
binding.clearAt.text = context.getString(R.string.dontClear)
|
||||
} else {
|
||||
val clearAt = status.clearAt!!
|
||||
if (clearAt.type.equals("period")) {
|
||||
binding.clearAt.text = DisplayUtils.getRelativeTimestamp(
|
||||
context,
|
||||
System.currentTimeMillis() + clearAt.time.toInt() * ONE_SECOND_IN_MILLIS,
|
||||
true
|
||||
)
|
||||
} else {
|
||||
// end-of
|
||||
if (clearAt.time.equals("day")) {
|
||||
binding.clearAt.text = context.getString(R.string.today)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isBackupStatusAvailable) {
|
||||
binding.resetStatusButton.visibility = if (position == 0) View.VISIBLE else View.GONE
|
||||
if (position == 0) {
|
||||
binding.clearAt.text = context.getString(R.string.previously_set)
|
||||
}
|
||||
binding.resetStatusButton.setOnClickListener {
|
||||
clickListener.revertStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters
|
||||
|
||||
import ru.f7cloud.talk.models.json.reactions.ReactionVoter
|
||||
|
||||
data class ReactionItem(val reactionVoter: ReactionVoter, val reaction: String?)
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters
|
||||
|
||||
interface ReactionItemClickListener {
|
||||
fun onClick(reactionItem: ReactionItem)
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ru.f7cloud.talk.data.user.model.User
|
||||
import ru.f7cloud.talk.databinding.ReactionItemBinding
|
||||
|
||||
class ReactionsAdapter(private val clickListener: ReactionItemClickListener, private val user: User?) :
|
||||
RecyclerView.Adapter<ReactionsViewHolder>() {
|
||||
internal var list: MutableList<ReactionItem> = ArrayList<ReactionItem>()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ReactionsViewHolder {
|
||||
val itemBinding = ReactionItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return ReactionsViewHolder(itemBinding, user)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ReactionsViewHolder, position: Int) {
|
||||
holder.bind(list[position], clickListener)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = list.size
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters
|
||||
|
||||
import android.text.TextUtils
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication.Companion.sharedApplication
|
||||
import ru.f7cloud.talk.data.user.model.User
|
||||
import ru.f7cloud.talk.databinding.ReactionItemBinding
|
||||
import ru.f7cloud.talk.extensions.loadGuestAvatar
|
||||
import ru.f7cloud.talk.extensions.loadUserAvatar
|
||||
import ru.f7cloud.talk.models.json.reactions.ReactionVoter
|
||||
|
||||
class ReactionsViewHolder(private val binding: ReactionItemBinding, private val user: User?) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(reactionItem: ReactionItem, clickListener: ReactionItemClickListener) {
|
||||
binding.root.setOnClickListener { clickListener.onClick(reactionItem) }
|
||||
binding.reaction.text = reactionItem.reaction
|
||||
binding.name.text = reactionItem.reactionVoter.actorDisplayName
|
||||
|
||||
if (user != null && user.baseUrl?.isNotEmpty() == true) {
|
||||
loadAvatar(reactionItem)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadAvatar(reactionItem: ReactionItem) {
|
||||
if (reactionItem.reactionVoter.actorType == ReactionVoter.ReactionActorType.GUESTS) {
|
||||
var displayName = sharedApplication?.resources?.getString(R.string.nc_guest)
|
||||
if (!TextUtils.isEmpty(reactionItem.reactionVoter.actorDisplayName)) {
|
||||
displayName = reactionItem.reactionVoter.actorDisplayName!!
|
||||
}
|
||||
binding.avatar.loadGuestAvatar(user!!.baseUrl!!, displayName!!, false)
|
||||
} else if (reactionItem.reactionVoter.actorType == ReactionVoter.ReactionActorType.USERS) {
|
||||
binding.avatar.loadUserAvatar(
|
||||
user!!,
|
||||
reactionItem.reactionVoter.actorId!!,
|
||||
false,
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <infoi@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.items
|
||||
|
||||
import android.accounts.Account
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import androidx.core.net.toUri
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.adapters.items.AdvancedUserItem.UserItemViewHolder
|
||||
import ru.f7cloud.talk.data.user.model.User
|
||||
import ru.f7cloud.talk.databinding.AccountItemBinding
|
||||
import ru.f7cloud.talk.extensions.loadUserAvatar
|
||||
import ru.f7cloud.talk.models.json.participants.Participant
|
||||
import ru.f7cloud.talk.ui.theme.ViewThemeUtils
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFilterable
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class AdvancedUserItem(
|
||||
/**
|
||||
* @return the model object
|
||||
*/
|
||||
val model: Participant,
|
||||
@JvmField val user: User?,
|
||||
val account: Account?,
|
||||
private val viewThemeUtils: ViewThemeUtils,
|
||||
private val actionRequiredCount: Int
|
||||
) : AbstractFlexibleItem<UserItemViewHolder>(),
|
||||
IFilterable<String?> {
|
||||
|
||||
override fun equals(other: Any?): Boolean =
|
||||
if (other is AdvancedUserItem) {
|
||||
model == other.model
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = model.hashCode()
|
||||
|
||||
override fun getLayoutRes(): Int = R.layout.account_item
|
||||
|
||||
override fun createViewHolder(
|
||||
view: View?,
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>?
|
||||
): UserItemViewHolder = UserItemViewHolder(view, adapter)
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: UserItemViewHolder,
|
||||
position: Int,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
if (adapter.hasFilter()) {
|
||||
viewThemeUtils.talk.themeAndHighlightText(
|
||||
holder.binding.userName,
|
||||
model.displayName,
|
||||
adapter.getFilter(String::class.java).toString()
|
||||
)
|
||||
} else {
|
||||
holder.binding.userName.text = model.displayName
|
||||
}
|
||||
if (user != null && !TextUtils.isEmpty(user.baseUrl)) {
|
||||
val host = user.baseUrl!!.toUri().host
|
||||
if (!TextUtils.isEmpty(host)) {
|
||||
holder.binding.account.text = user.baseUrl!!.toUri().host
|
||||
} else {
|
||||
holder.binding.account.text = user.baseUrl
|
||||
}
|
||||
}
|
||||
if (user?.baseUrl != null &&
|
||||
(user.baseUrl!!.startsWith("http://") || user.baseUrl!!.startsWith("https://"))
|
||||
) {
|
||||
holder.binding.userIcon.loadUserAvatar(user, model.calculatedActorId!!, true, false)
|
||||
}
|
||||
if (actionRequiredCount > 0) {
|
||||
holder.binding.actionRequired.visibility = View.VISIBLE
|
||||
} else {
|
||||
holder.binding.actionRequired.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun filter(constraint: String?): Boolean =
|
||||
model.displayName != null &&
|
||||
Pattern
|
||||
.compile(constraint, Pattern.CASE_INSENSITIVE or Pattern.LITERAL)
|
||||
.matcher(model.displayName!!.trim())
|
||||
.find()
|
||||
|
||||
class UserItemViewHolder(view: View?, adapter: FlexibleAdapter<*>?) : FlexibleViewHolder(view, adapter) {
|
||||
var binding: AccountItemBinding
|
||||
|
||||
/**
|
||||
* Default constructor.
|
||||
*/
|
||||
init {
|
||||
binding = AccountItemBinding.bind(view!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <infoi@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.items
|
||||
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.adapters.items.ContactItem.ContactItemViewHolder
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication.Companion.sharedApplication
|
||||
import ru.f7cloud.talk.data.user.model.User
|
||||
import ru.f7cloud.talk.databinding.RvItemContactBinding
|
||||
import ru.f7cloud.talk.extensions.loadUserAvatar
|
||||
import ru.f7cloud.talk.models.json.participants.Participant
|
||||
import ru.f7cloud.talk.ui.theme.ViewThemeUtils
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFilterable
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.flexibleadapter.items.ISectionable
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import java.util.Objects
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class ContactItem(
|
||||
/**
|
||||
* @return the model object
|
||||
*/
|
||||
val model: Participant,
|
||||
private val user: User,
|
||||
private var header: GenericTextHeaderItem?,
|
||||
private val viewThemeUtils: ViewThemeUtils
|
||||
) : AbstractFlexibleItem<ContactItemViewHolder?>(),
|
||||
ISectionable<ContactItemViewHolder?, GenericTextHeaderItem?>,
|
||||
IFilterable<String?> {
|
||||
var isOnline: Boolean = true
|
||||
|
||||
override fun equals(o: Any?): Boolean {
|
||||
if (o is ContactItem) {
|
||||
return model.calculatedActorType == o.model.calculatedActorType &&
|
||||
model.calculatedActorId == o.model.calculatedActorId
|
||||
}
|
||||
return false
|
||||
}
|
||||
override fun hashCode(): Int = model.hashCode()
|
||||
|
||||
override fun filter(constraint: String?): Boolean =
|
||||
model.displayName != null &&
|
||||
(
|
||||
Pattern.compile(constraint!!, Pattern.CASE_INSENSITIVE or Pattern.LITERAL)
|
||||
.matcher(model.displayName!!.trim())
|
||||
.find() ||
|
||||
Pattern.compile(constraint!!, Pattern.CASE_INSENSITIVE or Pattern.LITERAL)
|
||||
.matcher(model.calculatedActorId!!.trim())
|
||||
.find()
|
||||
)
|
||||
|
||||
override fun getLayoutRes(): Int = R.layout.rv_item_contact
|
||||
|
||||
override fun createViewHolder(
|
||||
view: View?,
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>?
|
||||
): ContactItemViewHolder = ContactItemViewHolder(view, adapter)
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>?,
|
||||
holder: ContactItemViewHolder?,
|
||||
position: Int,
|
||||
payloads: List<Any>?
|
||||
) {
|
||||
if (model.selected) {
|
||||
holder?.binding?.checkedImageView?.let { viewThemeUtils.platform.colorImageView(it) }
|
||||
holder?.binding?.checkedImageView?.visibility = View.VISIBLE
|
||||
} else {
|
||||
holder?.binding?.checkedImageView?.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (!isOnline) {
|
||||
holder?.binding?.nameText?.setTextColor(
|
||||
ResourcesCompat.getColor(
|
||||
holder.binding.nameText.context.resources,
|
||||
R.color.medium_emphasis_text,
|
||||
null
|
||||
)
|
||||
)
|
||||
holder?.binding?.avatarView?.alpha = SEMI_TRANSPARENT
|
||||
} else {
|
||||
holder?.binding?.nameText?.setTextColor(
|
||||
ResourcesCompat.getColor(
|
||||
holder.binding.nameText.context.resources,
|
||||
R.color.high_emphasis_text,
|
||||
null
|
||||
)
|
||||
)
|
||||
holder?.binding?.avatarView?.alpha = FULLY_OPAQUE
|
||||
}
|
||||
|
||||
holder?.binding?.nameText?.text = model.displayName
|
||||
|
||||
if (adapter != null) {
|
||||
if (adapter.hasFilter()) {
|
||||
holder?.binding?.let {
|
||||
viewThemeUtils.talk.themeAndHighlightText(
|
||||
it.nameText,
|
||||
model.displayName,
|
||||
adapter.getFilter(String::class.java).toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(model.displayName) &&
|
||||
(
|
||||
model.type == Participant.ParticipantType.GUEST ||
|
||||
model.type == Participant.ParticipantType.USER_FOLLOWING_LINK
|
||||
)
|
||||
) {
|
||||
holder?.binding?.nameText?.text = sharedApplication!!.getString(R.string.nc_guest)
|
||||
}
|
||||
|
||||
setAvatar(holder)
|
||||
}
|
||||
|
||||
private fun setAvatar(holder: ContactItemViewHolder?) {
|
||||
if (model.calculatedActorType == Participant.ActorType.GROUPS ||
|
||||
model.calculatedActorType == Participant.ActorType.CIRCLES
|
||||
) {
|
||||
setGenericAvatar(holder!!, R.drawable.ic_avatar_group)
|
||||
} else if (model.calculatedActorType == Participant.ActorType.EMAILS) {
|
||||
setGenericAvatar(holder!!, R.drawable.ic_avatar_mail)
|
||||
} else if (model.calculatedActorType == Participant.ActorType.GUESTS ||
|
||||
model.type == Participant.ParticipantType.GUEST ||
|
||||
model.type == Participant.ParticipantType.GUEST_MODERATOR
|
||||
) {
|
||||
var displayName: String?
|
||||
|
||||
displayName = if (!TextUtils.isEmpty(model.displayName)) {
|
||||
model.displayName
|
||||
} else {
|
||||
Objects.requireNonNull(sharedApplication)!!.resources!!.getString(R.string.nc_guest)
|
||||
}
|
||||
|
||||
// absolute fallback to prevent NPE deference
|
||||
if (displayName == null) {
|
||||
displayName = "Guest"
|
||||
}
|
||||
|
||||
holder?.binding?.avatarView?.loadUserAvatar(user, displayName, true, false)
|
||||
} else if (model.calculatedActorType == Participant.ActorType.USERS) {
|
||||
holder?.binding?.avatarView
|
||||
?.loadUserAvatar(
|
||||
user,
|
||||
model.calculatedActorId!!,
|
||||
true,
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setGenericAvatar(holder: ContactItemViewHolder, roundPlaceholderDrawable: Int) {
|
||||
val avatar =
|
||||
viewThemeUtils.talk.themePlaceholderAvatar(
|
||||
holder.binding.avatarView,
|
||||
roundPlaceholderDrawable
|
||||
)
|
||||
|
||||
holder.binding.avatarView.loadUserAvatar(avatar)
|
||||
}
|
||||
|
||||
override fun getHeader(): GenericTextHeaderItem? = header
|
||||
|
||||
override fun setHeader(p0: GenericTextHeaderItem?) {
|
||||
this.header = header
|
||||
}
|
||||
|
||||
class ContactItemViewHolder(view: View?, adapter: FlexibleAdapter<*>?) : FlexibleViewHolder(view, adapter) {
|
||||
var binding: RvItemContactBinding =
|
||||
RvItemContactBinding.bind(view!!)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val FULLY_OPAQUE: Float = 1.0f
|
||||
private const val SEMI_TRANSPARENT: Float = 0.38f
|
||||
}
|
||||
}
|
||||
+508
@@ -0,0 +1,508 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2022 Tim Krüger <t@timkrueger.me>
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <infoi@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2017-2019 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.items
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Typeface
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.TextUtils
|
||||
import android.text.format.DateUtils
|
||||
import android.text.style.ImageSpan
|
||||
import android.view.View
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import coil.dispose
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.adapters.items.ConversationItem.ConversationItemViewHolder
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication.Companion.sharedApplication
|
||||
import ru.f7cloud.talk.chat.data.model.ChatMessage
|
||||
import ru.f7cloud.talk.chat.data.model.ChatMessage.MessageType
|
||||
import ru.f7cloud.talk.data.database.mappers.asModel
|
||||
import ru.f7cloud.talk.data.user.model.User
|
||||
import ru.f7cloud.talk.databinding.RvItemConversationWithLastMessageBinding
|
||||
import ru.f7cloud.talk.extensions.loadConversationAvatar
|
||||
import ru.f7cloud.talk.extensions.loadNoteToSelfAvatar
|
||||
import ru.f7cloud.talk.extensions.loadSystemAvatar
|
||||
import ru.f7cloud.talk.extensions.loadUserAvatar
|
||||
import ru.f7cloud.talk.models.domain.ConversationModel
|
||||
import ru.f7cloud.talk.models.json.conversations.ConversationEnums
|
||||
import ru.f7cloud.talk.ui.StatusDrawable
|
||||
import ru.f7cloud.talk.ui.theme.ViewThemeUtils
|
||||
import ru.f7cloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability
|
||||
import ru.f7cloud.talk.utils.DisplayUtils
|
||||
import ru.f7cloud.talk.utils.SpreedFeatures
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFilterable
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.flexibleadapter.items.ISectionable
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import java.util.Locale
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class ConversationItem(
|
||||
val model: ConversationModel,
|
||||
private val user: User,
|
||||
private val context: Context,
|
||||
private val viewThemeUtils: ViewThemeUtils
|
||||
) : AbstractFlexibleItem<ConversationItemViewHolder>(),
|
||||
ISectionable<ConversationItemViewHolder, GenericTextHeaderItem?>,
|
||||
IFilterable<String?> {
|
||||
private var header: GenericTextHeaderItem? = null
|
||||
private val chatMessage = model.lastMessage?.asModel()
|
||||
var mHolder: ConversationItemViewHolder? = null
|
||||
|
||||
constructor(
|
||||
conversation: ConversationModel,
|
||||
user: User,
|
||||
activityContext: Context,
|
||||
genericTextHeaderItem: GenericTextHeaderItem?,
|
||||
viewThemeUtils: ViewThemeUtils
|
||||
) : this(conversation, user, activityContext, viewThemeUtils) {
|
||||
header = genericTextHeaderItem
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other is ConversationItem) {
|
||||
return model == other.model
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = model.hashCode()
|
||||
result *= 31
|
||||
return result
|
||||
}
|
||||
|
||||
override fun getLayoutRes(): Int = R.layout.rv_item_conversation_with_last_message
|
||||
|
||||
override fun getItemViewType(): Int = VIEW_TYPE
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>?>?): ConversationItemViewHolder =
|
||||
ConversationItemViewHolder(view, adapter)
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<*>?>,
|
||||
holder: ConversationItemViewHolder,
|
||||
position: Int,
|
||||
payloads: List<Any>
|
||||
) {
|
||||
mHolder = holder
|
||||
val appContext = sharedApplication!!.applicationContext
|
||||
holder.binding.dialogName.setTextColor(
|
||||
ResourcesCompat.getColor(
|
||||
context.resources,
|
||||
R.color.conversation_item_header,
|
||||
null
|
||||
)
|
||||
)
|
||||
if (adapter.hasFilter()) {
|
||||
viewThemeUtils.platform.highlightText(
|
||||
holder.binding.dialogName,
|
||||
model.displayName!!,
|
||||
adapter.getFilter(String::class.java).toString()
|
||||
)
|
||||
} else {
|
||||
holder.binding.dialogName.text = model.displayName
|
||||
}
|
||||
if (model.unreadMessages > 0) {
|
||||
showUnreadMessages(holder)
|
||||
} else {
|
||||
holder.binding.dialogName.setTypeface(null, Typeface.NORMAL)
|
||||
holder.binding.dialogDate.setTypeface(null, Typeface.NORMAL)
|
||||
holder.binding.dialogLastMessage.setTypeface(null, Typeface.NORMAL)
|
||||
holder.binding.dialogUnreadBubble.visibility = View.GONE
|
||||
}
|
||||
if (model.favorite) {
|
||||
holder.binding.favoriteConversationImageView.visibility = View.VISIBLE
|
||||
} else {
|
||||
holder.binding.favoriteConversationImageView.visibility = View.GONE
|
||||
}
|
||||
if (ConversationEnums.ConversationType.ROOM_PUBLIC_CALL == model.type) {
|
||||
holder.binding.publicCallBadge.setImageResource(R.drawable.ic_avatar_link)
|
||||
holder.binding.publicCallBadge.visibility = View.VISIBLE
|
||||
} else if (model.remoteServer?.isNotEmpty() == true) {
|
||||
holder.binding.publicCallBadge.setImageResource(R.drawable.ic_avatar_federation)
|
||||
holder.binding.publicCallBadge.visibility = View.VISIBLE
|
||||
} else {
|
||||
holder.binding.publicCallBadge.visibility = View.GONE
|
||||
}
|
||||
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(
|
||||
StatusDrawable(
|
||||
model.status,
|
||||
model.statusIcon,
|
||||
size,
|
||||
context.resources.getColor(R.color.bg_default, null),
|
||||
appContext
|
||||
)
|
||||
)
|
||||
} else {
|
||||
holder.binding.userStatusImage.visibility = View.GONE
|
||||
}
|
||||
|
||||
val dialogNameParams = holder.binding.dialogName.layoutParams as RelativeLayout.LayoutParams
|
||||
val unreadBubbleParams = holder.binding.dialogUnreadBubble.layoutParams as RelativeLayout.LayoutParams
|
||||
val relativeLayoutParams = holder.binding.relativeLayout.layoutParams as RelativeLayout.LayoutParams
|
||||
|
||||
if (model.hasSensitive == true) {
|
||||
dialogNameParams.addRule(RelativeLayout.CENTER_VERTICAL)
|
||||
relativeLayoutParams.addRule(RelativeLayout.ALIGN_TOP, R.id.dialogAvatarFrameLayout)
|
||||
dialogNameParams.marginEnd =
|
||||
context.resources.getDimensionPixelSize(R.dimen.standard_double_padding)
|
||||
unreadBubbleParams.topMargin =
|
||||
context.resources.getDimensionPixelSize(R.dimen.double_margin_between_elements)
|
||||
unreadBubbleParams.addRule(RelativeLayout.CENTER_VERTICAL)
|
||||
} else {
|
||||
dialogNameParams.removeRule(RelativeLayout.CENTER_VERTICAL)
|
||||
relativeLayoutParams.removeRule(RelativeLayout.ALIGN_TOP)
|
||||
dialogNameParams.marginEnd = 0
|
||||
unreadBubbleParams.topMargin = 0
|
||||
unreadBubbleParams.removeRule(RelativeLayout.CENTER_VERTICAL)
|
||||
}
|
||||
holder.binding.relativeLayout.layoutParams = relativeLayoutParams
|
||||
holder.binding.dialogUnreadBubble.layoutParams = unreadBubbleParams
|
||||
holder.binding.dialogName.layoutParams = dialogNameParams
|
||||
|
||||
setLastMessage(holder, appContext)
|
||||
showAvatar(holder)
|
||||
}
|
||||
|
||||
private fun showAvatar(holder: ConversationItemViewHolder) {
|
||||
holder.binding.dialogAvatar.dispose()
|
||||
holder.binding.dialogAvatar.visibility = View.VISIBLE
|
||||
|
||||
var shouldLoadAvatar = shouldLoadAvatar(holder)
|
||||
if (ConversationEnums.ConversationType.ROOM_SYSTEM == model.type) {
|
||||
holder.binding.dialogAvatar.loadSystemAvatar()
|
||||
shouldLoadAvatar = false
|
||||
}
|
||||
if (shouldLoadAvatar) {
|
||||
when (model.type) {
|
||||
ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> {
|
||||
if (!TextUtils.isEmpty(model.name)) {
|
||||
holder.binding.dialogAvatar.loadUserAvatar(
|
||||
user,
|
||||
model.name!!,
|
||||
true,
|
||||
false
|
||||
)
|
||||
} else {
|
||||
holder.binding.dialogAvatar.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
ConversationEnums.ConversationType.ROOM_GROUP_CALL,
|
||||
ConversationEnums.ConversationType.FORMER_ONE_TO_ONE,
|
||||
ConversationEnums.ConversationType.ROOM_PUBLIC_CALL ->
|
||||
holder.binding.dialogAvatar.loadConversationAvatar(user, model, false, viewThemeUtils)
|
||||
|
||||
ConversationEnums.ConversationType.NOTE_TO_SELF ->
|
||||
holder.binding.dialogAvatar.loadNoteToSelfAvatar()
|
||||
|
||||
else -> holder.binding.dialogAvatar.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldLoadAvatar(holder: ConversationItemViewHolder): Boolean =
|
||||
when (model.objectType) {
|
||||
ConversationEnums.ObjectType.SHARE_PASSWORD -> {
|
||||
holder.binding.dialogAvatar.setImageDrawable(
|
||||
ContextCompat.getDrawable(
|
||||
context,
|
||||
R.drawable.ic_circular_lock
|
||||
)
|
||||
)
|
||||
false
|
||||
}
|
||||
|
||||
ConversationEnums.ObjectType.FILE -> {
|
||||
holder.binding.dialogAvatar.loadUserAvatar(
|
||||
viewThemeUtils.talk.themePlaceholderAvatar(
|
||||
holder.binding.dialogAvatar,
|
||||
R.drawable.ic_avatar_document
|
||||
)
|
||||
)
|
||||
false
|
||||
}
|
||||
|
||||
else -> true
|
||||
}
|
||||
|
||||
private fun setLastMessage(holder: ConversationItemViewHolder, appContext: Context) {
|
||||
if (chatMessage != null) {
|
||||
holder.binding.dialogDate.visibility = View.VISIBLE
|
||||
holder.binding.dialogDate.text = DateUtils.getRelativeTimeSpanString(
|
||||
model.lastActivity * MILLIES,
|
||||
System.currentTimeMillis(),
|
||||
0,
|
||||
DateUtils.FORMAT_ABBREV_RELATIVE
|
||||
)
|
||||
if (!TextUtils.isEmpty(chatMessage?.systemMessage) ||
|
||||
ConversationEnums.ConversationType.ROOM_SYSTEM === model.type
|
||||
) {
|
||||
holder.binding.dialogLastMessage.text = chatMessage.text
|
||||
} else {
|
||||
chatMessage?.activeUser = user
|
||||
|
||||
val text =
|
||||
if (
|
||||
chatMessage?.getCalculateMessageType() == MessageType.REGULAR_TEXT_MESSAGE
|
||||
) {
|
||||
calculateRegularLastMessageText(appContext)
|
||||
} else {
|
||||
lastMessageDisplayText
|
||||
}
|
||||
holder.binding.dialogLastMessage.text = text
|
||||
}
|
||||
} else {
|
||||
holder.binding.dialogDate.visibility = View.GONE
|
||||
holder.binding.dialogLastMessage.text = ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateRegularLastMessageText(appContext: Context): CharSequence =
|
||||
if (chatMessage?.actorId == user.userId) {
|
||||
String.format(
|
||||
appContext.getString(R.string.nc_formatted_message_you),
|
||||
lastMessageDisplayText
|
||||
)
|
||||
} else if (model.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) {
|
||||
lastMessageDisplayText
|
||||
} else {
|
||||
val actorName = chatMessage?.actorDisplayName
|
||||
val authorDisplayName = if (!actorName.isNullOrBlank()) {
|
||||
actorName
|
||||
} else if ("guests" == chatMessage?.actorType || "emails" == chatMessage?.actorType) {
|
||||
appContext.getString(R.string.nc_guest)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
String.format(
|
||||
appContext.getString(R.string.nc_formatted_message),
|
||||
authorDisplayName,
|
||||
lastMessageDisplayText
|
||||
)
|
||||
}
|
||||
|
||||
private fun showUnreadMessages(holder: ConversationItemViewHolder) {
|
||||
holder.binding.dialogName.setTypeface(holder.binding.dialogName.typeface, Typeface.BOLD)
|
||||
holder.binding.dialogLastMessage.setTypeface(holder.binding.dialogLastMessage.typeface, Typeface.BOLD)
|
||||
holder.binding.dialogUnreadBubble.visibility = View.VISIBLE
|
||||
if (model.unreadMessages < UNREAD_MESSAGES_TRESHOLD) {
|
||||
holder.binding.dialogUnreadBubble.text = String.format(Locale.getDefault(), "%d", model.unreadMessages)
|
||||
} else {
|
||||
holder.binding.dialogUnreadBubble.setText(R.string.tooManyUnreadMessages)
|
||||
}
|
||||
val lightBubbleFillColor = ColorStateList.valueOf(
|
||||
ContextCompat.getColor(
|
||||
context,
|
||||
R.color.conversation_unread_bubble
|
||||
)
|
||||
)
|
||||
val lightBubbleTextColor = ContextCompat.getColor(
|
||||
context,
|
||||
R.color.conversation_unread_bubble_text
|
||||
)
|
||||
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)) {
|
||||
if (model.unreadMentionDirect!!) {
|
||||
viewThemeUtils.material.colorChipBackground(holder.binding.dialogUnreadBubble)
|
||||
} else {
|
||||
viewThemeUtils.material.colorChipOutlined(
|
||||
holder.binding.dialogUnreadBubble,
|
||||
UNREAD_BUBBLE_STROKE_WIDTH
|
||||
)
|
||||
}
|
||||
} else {
|
||||
viewThemeUtils.material.colorChipBackground(holder.binding.dialogUnreadBubble)
|
||||
}
|
||||
} else {
|
||||
holder.binding.dialogUnreadBubble.chipBackgroundColor = lightBubbleFillColor
|
||||
holder.binding.dialogUnreadBubble.setTextColor(lightBubbleTextColor)
|
||||
}
|
||||
}
|
||||
|
||||
override fun filter(constraint: String?): Boolean =
|
||||
model.displayName != null &&
|
||||
Pattern
|
||||
.compile(constraint!!, Pattern.CASE_INSENSITIVE or Pattern.LITERAL)
|
||||
.matcher(model.displayName.trim())
|
||||
.find()
|
||||
|
||||
override fun getHeader(): GenericTextHeaderItem? = header
|
||||
|
||||
override fun setHeader(header: GenericTextHeaderItem?) {
|
||||
this.header = header
|
||||
}
|
||||
|
||||
private val lastMessageDisplayText: CharSequence
|
||||
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_GEOLOCATION_MESSAGE == chatMessage?.getCalculateMessageType()) {
|
||||
var locationName = chatMessage.messageParameters?.get("object")?.get("name") ?: ""
|
||||
val author = authorName(chatMessage)
|
||||
val lastMessage =
|
||||
setLastNameForAttachmentMessage(author, R.drawable.baseline_location_pin_24, locationName)
|
||||
return lastMessage
|
||||
} else if (MessageType.VOICE_MESSAGE == chatMessage?.getCalculateMessageType()) {
|
||||
var voiceMessageName = chatMessage.messageParameters?.get("file")?.get("name") ?: ""
|
||||
val author = authorName(chatMessage)
|
||||
val lastMessage = setLastNameForAttachmentMessage(
|
||||
author,
|
||||
R.drawable.baseline_mic_24,
|
||||
voiceMessageName
|
||||
)
|
||||
return lastMessage
|
||||
} 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()) {
|
||||
var pollMessageTitle = chatMessage.messageParameters?.get("object")?.get("name") ?: ""
|
||||
val author = authorName(chatMessage)
|
||||
val lastMessage = setLastNameForAttachmentMessage(
|
||||
author,
|
||||
R.drawable.baseline_bar_chart_24,
|
||||
pollMessageTitle
|
||||
)
|
||||
return lastMessage
|
||||
} else if (MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == chatMessage?.getCalculateMessageType()) {
|
||||
var attachmentName = chatMessage.text
|
||||
if (attachmentName == "{file}") {
|
||||
attachmentName = chatMessage.messageParameters?.get("file")?.get("name") ?: ""
|
||||
}
|
||||
val author = authorName(chatMessage)
|
||||
|
||||
val drawable = chatMessage.messageParameters?.get("file")?.get("mimetype")?.let {
|
||||
when {
|
||||
it.contains("image") -> R.drawable.baseline_image_24
|
||||
it.contains("video") -> R.drawable.baseline_video_24
|
||||
it.contains("application") -> R.drawable.baseline_insert_drive_file_24
|
||||
it.contains("audio") -> R.drawable.baseline_audiotrack_24
|
||||
it.contains("text/vcard") -> R.drawable.baseline_contacts_24
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
val lastMessage = setLastNameForAttachmentMessage(author, drawable, attachmentName!!)
|
||||
return lastMessage
|
||||
} else if (MessageType.DECK_CARD == chatMessage?.getCalculateMessageType()) {
|
||||
var deckTitle = chatMessage.messageParameters?.get("object")?.get("name") ?: ""
|
||||
val author = authorName(chatMessage)
|
||||
val lastMessage = setLastNameForAttachmentMessage(author, R.drawable.baseline_article_24, deckTitle)
|
||||
return lastMessage
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fun authorName(chatMessage: ChatMessage): String {
|
||||
val name = if (chatMessage.actorId == chatMessage.activeUser!!.userId) {
|
||||
sharedApplication!!.resources.getString(R.string.nc_current_user)
|
||||
} else {
|
||||
chatMessage.getNullsafeActorDisplayName()?.let { "$it:" } ?: ""
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
fun setLastNameForAttachmentMessage(actor: String, icon: Int?, attachmentName: String): SpannableStringBuilder {
|
||||
val builder = SpannableStringBuilder()
|
||||
builder.append(actor)
|
||||
|
||||
val drawable = icon?.let { it -> ContextCompat.getDrawable(context, it) }
|
||||
if (drawable != null) {
|
||||
viewThemeUtils.platform.colorDrawable(
|
||||
drawable,
|
||||
context.resources.getColor(R.color.low_emphasis_text, null)
|
||||
)
|
||||
val desiredWidth = (drawable.intrinsicWidth * IMAGE_SCALE_FACTOR).toInt()
|
||||
val desiredHeight = (drawable.intrinsicHeight * IMAGE_SCALE_FACTOR).toInt()
|
||||
drawable.setBounds(0, 0, desiredWidth, desiredHeight)
|
||||
|
||||
val imageSpan = ImageSpan(drawable, ImageSpan.ALIGN_BOTTOM)
|
||||
val startImage = builder.length
|
||||
builder.append(" ")
|
||||
builder.setSpan(imageSpan, startImage, startImage + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
} else {
|
||||
builder.append(" ")
|
||||
}
|
||||
builder.append(attachmentName)
|
||||
return builder
|
||||
}
|
||||
|
||||
class ConversationItemViewHolder(view: View?, adapter: FlexibleAdapter<*>?) : FlexibleViewHolder(view, adapter) {
|
||||
var binding: RvItemConversationWithLastMessageBinding
|
||||
|
||||
init {
|
||||
binding = RvItemConversationWithLastMessageBinding.bind(view!!)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val VIEW_TYPE = FlexibleItemViewType.CONVERSATION_ITEM
|
||||
private const val MILLIES = 1000L
|
||||
private const val STATUS_SIZE_IN_DP = 9f
|
||||
private const val UNREAD_BUBBLE_STROKE_WIDTH = 6.0f
|
||||
private const val UNREAD_MESSAGES_TRESHOLD = 1000
|
||||
private const val IMAGE_SCALE_FACTOR = 0.7f
|
||||
}
|
||||
}
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Andy Scherzinger <infoi@andy-scherzinger.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.items
|
||||
|
||||
object FlexibleItemViewType {
|
||||
const val CONVERSATION_ITEM: Int = 1120391230
|
||||
const val LOAD_MORE_RESULTS_ITEM: Int = 1120391231
|
||||
const val MESSAGE_RESULT_ITEM: Int = 1120391232
|
||||
const val MESSAGES_TEXT_HEADER_ITEM: Int = 1120391233
|
||||
const val POLL_RESULT_HEADER_ITEM: Int = 1120391234
|
||||
const val POLL_RESULT_VOTER_ITEM: Int = 1120391235
|
||||
const val POLL_RESULT_VOTERS_OVERVIEW_ITEM: Int = 1120391236
|
||||
}
|
||||
Vendored
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <infoi@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.items
|
||||
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.databinding.RvItemTitleHeaderBinding
|
||||
import ru.f7cloud.talk.ui.theme.ViewThemeUtils
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import java.util.Objects
|
||||
|
||||
open class GenericTextHeaderItem(title: String, viewThemeUtils: ViewThemeUtils) :
|
||||
AbstractHeaderItem<GenericTextHeaderItem.HeaderViewHolder>() {
|
||||
val model: String
|
||||
private val viewThemeUtils: ViewThemeUtils
|
||||
|
||||
init {
|
||||
isHidden = false
|
||||
isSelectable = false
|
||||
this.model = title
|
||||
this.viewThemeUtils = viewThemeUtils
|
||||
}
|
||||
|
||||
override fun equals(o: Any?): Boolean {
|
||||
if (o is GenericTextHeaderItem) {
|
||||
return model == o.model
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = Objects.hash(model)
|
||||
|
||||
override fun getLayoutRes(): Int = R.layout.rv_item_title_header
|
||||
|
||||
override fun createViewHolder(
|
||||
view: View?,
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>?
|
||||
): HeaderViewHolder = HeaderViewHolder(view, adapter)
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<*>?>?,
|
||||
holder: HeaderViewHolder,
|
||||
position: Int,
|
||||
payloads: List<Any>
|
||||
) {
|
||||
if (payloads.size > 0) {
|
||||
Log.d(TAG, "We have payloads, so ignoring!")
|
||||
} else {
|
||||
holder.binding.titleTextView.text = model
|
||||
viewThemeUtils.platform.colorPrimaryTextViewElement(holder.binding.titleTextView)
|
||||
}
|
||||
}
|
||||
|
||||
class HeaderViewHolder(view: View?, adapter: FlexibleAdapter<*>?) : FlexibleViewHolder(view, adapter, true) {
|
||||
var binding: RvItemTitleHeaderBinding =
|
||||
RvItemTitleHeaderBinding.bind(view!!)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "GenericTextHeaderItem"
|
||||
}
|
||||
}
|
||||
Vendored
+53
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.items
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.databinding.RvItemLoadMoreBinding
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFilterable
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
|
||||
object LoadMoreResultsItem :
|
||||
AbstractFlexibleItem<LoadMoreResultsItem.ViewHolder>(),
|
||||
IFilterable<String> {
|
||||
|
||||
// layout is used as view type for uniqueness
|
||||
const val VIEW_TYPE = FlexibleItemViewType.LOAD_MORE_RESULTS_ITEM
|
||||
|
||||
class ViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
|
||||
var binding: RvItemLoadMoreBinding = RvItemLoadMoreBinding.bind(view)
|
||||
}
|
||||
|
||||
override fun getLayoutRes(): Int = R.layout.rv_item_load_more
|
||||
|
||||
override fun createViewHolder(
|
||||
view: View,
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>
|
||||
): ViewHolder = ViewHolder(view, adapter)
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: ViewHolder,
|
||||
position: Int,
|
||||
payloads: MutableList<Any>?
|
||||
) {
|
||||
// nothing, it's immutable
|
||||
}
|
||||
|
||||
override fun filter(constraint: String?): Boolean = true
|
||||
|
||||
override fun getItemViewType(): Int = VIEW_TYPE
|
||||
|
||||
override fun equals(other: Any?): Boolean = other is LoadMoreResultsItem
|
||||
|
||||
override fun hashCode(): Int = 0
|
||||
}
|
||||
Vendored
+264
@@ -0,0 +1,264 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2021-2022 Andy Scherzinger <infoi@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.items
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import ru.f7cloud.talk.PhoneUtils.isPhoneNumber
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.adapters.items.ParticipantItem.ParticipantItemViewHolder
|
||||
import ru.f7cloud.talk.data.user.model.User
|
||||
import ru.f7cloud.talk.extensions.loadDefaultAvatar
|
||||
import ru.f7cloud.talk.extensions.loadFederatedUserAvatar
|
||||
import ru.f7cloud.talk.extensions.loadGuestAvatar
|
||||
import ru.f7cloud.talk.extensions.loadUserAvatar
|
||||
import ru.f7cloud.talk.models.json.mention.Mention
|
||||
import ru.f7cloud.talk.models.json.status.StatusType
|
||||
import ru.f7cloud.talk.ui.StatusDrawable
|
||||
import ru.f7cloud.talk.ui.theme.ViewThemeUtils
|
||||
import ru.f7cloud.talk.utils.DisplayUtils
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFilterable
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import java.util.Objects
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class MentionAutocompleteItem(
|
||||
mention: Mention,
|
||||
private val currentUser: User,
|
||||
private val context: Context,
|
||||
@JvmField val roomToken: String,
|
||||
private val viewThemeUtils: ViewThemeUtils
|
||||
) : AbstractFlexibleItem<ParticipantItemViewHolder>(),
|
||||
IFilterable<String?> {
|
||||
@JvmField
|
||||
var source: String?
|
||||
|
||||
@JvmField
|
||||
val mentionId: String?
|
||||
|
||||
@JvmField
|
||||
val objectId: String?
|
||||
|
||||
@JvmField
|
||||
val displayName: String?
|
||||
private val status: String?
|
||||
private val statusIcon: String?
|
||||
private val statusMessage: String?
|
||||
|
||||
init {
|
||||
mentionId = mention.mentionId
|
||||
objectId = mention.id
|
||||
|
||||
displayName = if (!mention.label.isNullOrBlank()) {
|
||||
mention.label
|
||||
} else if ("guests" == mention.source || "emails" == mention.source) {
|
||||
context.resources.getString(R.string.nc_guest)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
source = mention.source
|
||||
status = mention.status
|
||||
statusIcon = mention.statusIcon
|
||||
statusMessage = mention.statusMessage
|
||||
}
|
||||
|
||||
override fun equals(o: Any?): Boolean =
|
||||
if (o is MentionAutocompleteItem) {
|
||||
objectId == o.objectId && displayName == o.displayName
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = Objects.hash(objectId, displayName)
|
||||
|
||||
override fun getLayoutRes(): Int = R.layout.rv_item_conversation_info_participant
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>?>?): ParticipantItemViewHolder =
|
||||
ParticipantItemViewHolder(view, adapter)
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<*>?>,
|
||||
holder: ParticipantItemViewHolder,
|
||||
position: Int,
|
||||
payloads: List<Any>
|
||||
) {
|
||||
holder.binding.nameText.setTextColor(
|
||||
ResourcesCompat.getColor(
|
||||
context.resources,
|
||||
R.color.conversation_item_header,
|
||||
null
|
||||
)
|
||||
)
|
||||
if (adapter.hasFilter()) {
|
||||
viewThemeUtils.talk.themeAndHighlightText(
|
||||
holder.binding.nameText,
|
||||
displayName,
|
||||
adapter.getFilter(String::class.java).toString()
|
||||
)
|
||||
viewThemeUtils.talk.themeAndHighlightText(
|
||||
holder.binding.secondaryText,
|
||||
"@$objectId",
|
||||
adapter.getFilter(String::class.java).toString()
|
||||
)
|
||||
} else {
|
||||
holder.binding.nameText.text = displayName
|
||||
}
|
||||
setAvatar(holder, objectId)
|
||||
drawStatus(holder)
|
||||
}
|
||||
|
||||
private fun setAvatar(holder: ParticipantItemViewHolder, objectId: String?) {
|
||||
when (source) {
|
||||
SOURCE_CALLS -> {
|
||||
run {
|
||||
if (isPhoneNumber(displayName)) {
|
||||
holder.binding.avatarView.loadUserAvatar(
|
||||
viewThemeUtils.talk.themePlaceholderAvatar(
|
||||
holder.binding.avatarView,
|
||||
R.drawable.ic_phone_small
|
||||
)
|
||||
)
|
||||
} else {
|
||||
holder.binding.avatarView.loadUserAvatar(
|
||||
viewThemeUtils.talk.themePlaceholderAvatar(
|
||||
holder.binding.avatarView,
|
||||
R.drawable.ic_avatar_group_small
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SOURCE_GROUPS -> {
|
||||
holder.binding.avatarView.loadUserAvatar(
|
||||
viewThemeUtils.talk.themePlaceholderAvatar(
|
||||
holder.binding.avatarView,
|
||||
R.drawable
|
||||
.ic_avatar_group_small
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
SOURCE_FEDERATION -> {
|
||||
val darkTheme = if (DisplayUtils.isDarkModeOn(context)) 1 else 0
|
||||
holder.binding.avatarView.loadFederatedUserAvatar(
|
||||
currentUser,
|
||||
currentUser.baseUrl!!,
|
||||
roomToken,
|
||||
objectId!!,
|
||||
darkTheme,
|
||||
requestBigSize = true,
|
||||
ignoreCache = false
|
||||
)
|
||||
}
|
||||
|
||||
SOURCE_GUESTS, SOURCE_EMAILS -> {
|
||||
if (displayName.equals(context.resources.getString(R.string.nc_guest))) {
|
||||
holder.binding.avatarView.loadDefaultAvatar(viewThemeUtils)
|
||||
} else {
|
||||
holder.binding.avatarView.loadGuestAvatar(currentUser, displayName!!, false)
|
||||
}
|
||||
}
|
||||
|
||||
SOURCE_TEAMS -> {
|
||||
holder.binding.avatarView.loadUserAvatar(
|
||||
viewThemeUtils.talk.themePlaceholderAvatar(
|
||||
holder.binding.avatarView,
|
||||
R.drawable
|
||||
.ic_avatar_team_small
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
holder.binding.avatarView.loadUserAvatar(
|
||||
currentUser,
|
||||
objectId!!,
|
||||
requestBigSize = true,
|
||||
ignoreCache = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawStatus(holder: ParticipantItemViewHolder) {
|
||||
val size = DisplayUtils.convertDpToPixel(STATUS_SIZE_IN_DP, context)
|
||||
holder.binding.userStatusImage.setImageDrawable(
|
||||
StatusDrawable(
|
||||
status,
|
||||
NO_ICON,
|
||||
size,
|
||||
context.resources.getColor(R.color.bg_default),
|
||||
context
|
||||
)
|
||||
)
|
||||
if (statusMessage != null) {
|
||||
holder.binding.conversationInfoStatusMessage.text = statusMessage
|
||||
alignUsernameVertical(holder, 0f)
|
||||
} else {
|
||||
holder.binding.conversationInfoStatusMessage.text = ""
|
||||
alignUsernameVertical(holder, NO_USER_STATUS_DP_FROM_TOP)
|
||||
}
|
||||
if (!statusIcon.isNullOrEmpty()) {
|
||||
holder.binding.participantStatusEmoji.setText(statusIcon)
|
||||
} else {
|
||||
holder.binding.participantStatusEmoji.visibility = View.GONE
|
||||
}
|
||||
if (status != null && status == StatusType.DND.string) {
|
||||
if (statusMessage.isNullOrEmpty()) {
|
||||
holder.binding.conversationInfoStatusMessage.setText(R.string.dnd)
|
||||
}
|
||||
} else if (status != null && status == StatusType.BUSY.string) {
|
||||
if (statusMessage.isNullOrEmpty()) {
|
||||
holder.binding.conversationInfoStatusMessage.setText(R.string.busy)
|
||||
}
|
||||
} else if (status != null && status == StatusType.AWAY.string) {
|
||||
if (statusMessage.isNullOrEmpty()) {
|
||||
holder.binding.conversationInfoStatusMessage.setText(R.string.away)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun alignUsernameVertical(holder: ParticipantItemViewHolder, densityPixelsFromTop: Float) {
|
||||
val layoutParams = holder.binding.nameText.layoutParams as ConstraintLayout.LayoutParams
|
||||
layoutParams.topMargin = DisplayUtils.convertDpToPixel(densityPixelsFromTop, context).toInt()
|
||||
holder.binding.nameText.setLayoutParams(layoutParams)
|
||||
}
|
||||
|
||||
override fun filter(constraint: String?): Boolean =
|
||||
objectId != null &&
|
||||
Pattern
|
||||
.compile(constraint, Pattern.CASE_INSENSITIVE or Pattern.LITERAL)
|
||||
.matcher(objectId)
|
||||
.find() ||
|
||||
displayName != null &&
|
||||
Pattern
|
||||
.compile(constraint, Pattern.CASE_INSENSITIVE or Pattern.LITERAL)
|
||||
.matcher(displayName)
|
||||
.find()
|
||||
|
||||
companion object {
|
||||
private const val STATUS_SIZE_IN_DP = 9f
|
||||
private const val NO_ICON = ""
|
||||
private const val NO_USER_STATUS_DP_FROM_TOP: Float = 10f
|
||||
const val SOURCE_CALLS = "calls"
|
||||
const val SOURCE_GUESTS = "guests"
|
||||
const val SOURCE_GROUPS = "groups"
|
||||
const val SOURCE_EMAILS = "emails"
|
||||
const val SOURCE_TEAMS = "teams"
|
||||
const val SOURCE_FEDERATION = "federated_users"
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-FileCopyrightText: 2022 Tim Krüger <t@timkrueger.me>
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <infoi@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2017-2019 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.items
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.data.user.model.User
|
||||
import ru.f7cloud.talk.databinding.RvItemSearchMessageBinding
|
||||
import ru.f7cloud.talk.extensions.loadThumbnail
|
||||
import ru.f7cloud.talk.models.domain.SearchMessageEntry
|
||||
import ru.f7cloud.talk.ui.theme.ViewThemeUtils
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFilterable
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.flexibleadapter.items.ISectionable
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
|
||||
data class MessageResultItem(
|
||||
private val context: Context,
|
||||
private val currentUser: User,
|
||||
val messageEntry: SearchMessageEntry,
|
||||
var showHeader: Boolean = false,
|
||||
private val viewThemeUtils: ViewThemeUtils
|
||||
) : AbstractFlexibleItem<MessageResultItem.ViewHolder>(),
|
||||
IFilterable<String>,
|
||||
ISectionable<MessageResultItem.ViewHolder, GenericTextHeaderItem> {
|
||||
|
||||
class ViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
|
||||
var binding: RvItemSearchMessageBinding
|
||||
|
||||
init {
|
||||
binding = RvItemSearchMessageBinding.bind(view)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLayoutRes(): Int = R.layout.rv_item_search_message
|
||||
|
||||
override fun createViewHolder(
|
||||
view: View,
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>
|
||||
): ViewHolder = ViewHolder(view, adapter)
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: ViewHolder,
|
||||
position: Int,
|
||||
payloads: MutableList<Any>?
|
||||
) {
|
||||
holder.binding.conversationTitle.text = messageEntry.title
|
||||
bindMessageExcerpt(holder)
|
||||
messageEntry.thumbnailURL?.let { holder.binding.thumbnail.loadThumbnail(it, currentUser) }
|
||||
}
|
||||
|
||||
private fun bindMessageExcerpt(holder: ViewHolder) {
|
||||
viewThemeUtils.platform.highlightText(
|
||||
holder.binding.messageExcerpt,
|
||||
messageEntry.messageExcerpt,
|
||||
messageEntry.searchTerm
|
||||
)
|
||||
}
|
||||
|
||||
override fun filter(constraint: String?): Boolean = true
|
||||
|
||||
override fun getItemViewType(): Int = VIEW_TYPE
|
||||
|
||||
companion object {
|
||||
const val VIEW_TYPE = FlexibleItemViewType.MESSAGE_RESULT_ITEM
|
||||
}
|
||||
|
||||
override fun getHeader(): GenericTextHeaderItem =
|
||||
MessagesTextHeaderItem(context, viewThemeUtils)
|
||||
.apply {
|
||||
isHidden = showHeader // FlexibleAdapter needs this hack for some reason
|
||||
}
|
||||
|
||||
override fun setHeader(header: GenericTextHeaderItem?) {
|
||||
// nothing, header is always the same
|
||||
}
|
||||
}
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.items
|
||||
|
||||
import android.content.Context
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.ui.theme.ViewThemeUtils
|
||||
|
||||
class MessagesTextHeaderItem(context: Context, viewThemeUtils: ViewThemeUtils) :
|
||||
GenericTextHeaderItem(context.getString(R.string.messages), viewThemeUtils) {
|
||||
companion object {
|
||||
const val VIEW_TYPE = FlexibleItemViewType.MESSAGES_TEXT_HEADER_ITEM
|
||||
}
|
||||
|
||||
override fun getItemViewType(): Int = VIEW_TYPE
|
||||
}
|
||||
Vendored
+95
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <infoi@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.items;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import ru.f7cloud.talk.R;
|
||||
import ru.f7cloud.talk.databinding.RvItemNotificationSoundBinding;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter;
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem;
|
||||
import eu.davidea.flexibleadapter.items.IFlexible;
|
||||
import eu.davidea.viewholders.FlexibleViewHolder;
|
||||
|
||||
public class NotificationSoundItem extends AbstractFlexibleItem<NotificationSoundItem.NotificationSoundItemViewHolder> {
|
||||
|
||||
private final String notificationSoundName;
|
||||
private final String notificationSoundUri;
|
||||
|
||||
public NotificationSoundItem(String notificationSoundName, String notificationSoundUri) {
|
||||
this.notificationSoundName = notificationSoundName;
|
||||
this.notificationSoundUri = notificationSoundUri;
|
||||
}
|
||||
|
||||
public String getNotificationSoundUri() {
|
||||
return notificationSoundUri;
|
||||
}
|
||||
|
||||
public String getNotificationSoundName() {
|
||||
return notificationSoundName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
NotificationSoundItem that = (NotificationSoundItem) o;
|
||||
|
||||
if (!Objects.equals(notificationSoundName, that.notificationSoundName)) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(notificationSoundUri, that.notificationSoundUri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = notificationSoundName != null ? notificationSoundName.hashCode() : 0;
|
||||
return 31 * result + (notificationSoundUri != null ? notificationSoundUri.hashCode() : 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLayoutRes() {
|
||||
return R.layout.rv_item_notification_sound;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NotificationSoundItemViewHolder createViewHolder(View view, FlexibleAdapter<IFlexible> adapter) {
|
||||
return new NotificationSoundItemViewHolder(view, adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindViewHolder(FlexibleAdapter<IFlexible> adapter,
|
||||
NotificationSoundItemViewHolder holder,
|
||||
int position,
|
||||
List<Object> payloads) {
|
||||
holder.binding.notificationNameTextView.setText(notificationSoundName);
|
||||
holder.binding.notificationNameTextView.setChecked(adapter.isSelected(position));
|
||||
}
|
||||
|
||||
static class NotificationSoundItemViewHolder extends FlexibleViewHolder {
|
||||
|
||||
RvItemNotificationSoundBinding binding;
|
||||
|
||||
/**
|
||||
* Default constructor.
|
||||
*/
|
||||
NotificationSoundItemViewHolder(View view, FlexibleAdapter adapter) {
|
||||
super(view, adapter);
|
||||
binding = RvItemNotificationSoundBinding.bind(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
+320
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <infoi@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.items
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.adapters.items.ParticipantItem.ParticipantItemViewHolder
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication.Companion.sharedApplication
|
||||
import ru.f7cloud.talk.data.user.model.User
|
||||
import ru.f7cloud.talk.databinding.RvItemConversationInfoParticipantBinding
|
||||
import ru.f7cloud.talk.extensions.loadDefaultAvatar
|
||||
import ru.f7cloud.talk.extensions.loadDefaultGroupCallAvatar
|
||||
import ru.f7cloud.talk.extensions.loadFederatedUserAvatar
|
||||
import ru.f7cloud.talk.extensions.loadFirstLetterAvatar
|
||||
import ru.f7cloud.talk.extensions.loadPhoneAvatar
|
||||
import ru.f7cloud.talk.extensions.loadTeamAvatar
|
||||
import ru.f7cloud.talk.extensions.loadUserAvatar
|
||||
import ru.f7cloud.talk.models.domain.ConversationModel
|
||||
import ru.f7cloud.talk.models.json.participants.Participant
|
||||
import ru.f7cloud.talk.models.json.participants.Participant.InCallFlags
|
||||
import ru.f7cloud.talk.models.json.status.StatusType
|
||||
import ru.f7cloud.talk.ui.StatusDrawable
|
||||
import ru.f7cloud.talk.ui.theme.ViewThemeUtils
|
||||
import ru.f7cloud.talk.utils.ConversationUtils
|
||||
import ru.f7cloud.talk.utils.DisplayUtils
|
||||
import ru.f7cloud.talk.utils.DisplayUtils.convertDpToPixel
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFilterable
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class ParticipantItem(
|
||||
private val context: Context,
|
||||
val model: Participant,
|
||||
private val user: User,
|
||||
private val viewThemeUtils: ViewThemeUtils,
|
||||
private val conversation: ConversationModel
|
||||
) : AbstractFlexibleItem<ParticipantItemViewHolder>(),
|
||||
IFilterable<String?> {
|
||||
var isOnline = true
|
||||
override fun equals(o: Any?): Boolean =
|
||||
if (o is ParticipantItem) {
|
||||
model.calculatedActorType == o.model.calculatedActorType &&
|
||||
model.calculatedActorId == o.model.calculatedActorId
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = model.hashCode()
|
||||
|
||||
override fun getLayoutRes(): Int = R.layout.rv_item_conversation_info_participant
|
||||
|
||||
override fun createViewHolder(
|
||||
view: View?,
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>?
|
||||
): ParticipantItemViewHolder = ParticipantItemViewHolder(view, adapter)
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>?,
|
||||
holder: ParticipantItemViewHolder?,
|
||||
position: Int,
|
||||
payloads: List<*>?
|
||||
) {
|
||||
drawStatus(holder!!)
|
||||
setOnlineStateColor(holder)
|
||||
holder.binding.nameText.text = model.displayName
|
||||
|
||||
if (model.type == Participant.ParticipantType.GUEST && model.displayName.isNullOrBlank()) {
|
||||
holder.binding.nameText.text = sharedApplication!!.getString(R.string.nc_guest)
|
||||
}
|
||||
|
||||
if (adapter!!.hasFilter()) {
|
||||
viewThemeUtils.talk.themeAndHighlightText(
|
||||
holder.binding.nameText,
|
||||
model.displayName,
|
||||
adapter.getFilter(
|
||||
String::class.java
|
||||
).toString()
|
||||
)
|
||||
}
|
||||
loadAvatars(holder)
|
||||
showCallIcons(holder)
|
||||
setParticipantInfo(holder)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun setParticipantInfo(holder: ParticipantItemViewHolder) {
|
||||
if (TextUtils.isEmpty(model.displayName) &&
|
||||
(
|
||||
model.type == Participant.ParticipantType.GUEST ||
|
||||
model.type == Participant.ParticipantType.USER_FOLLOWING_LINK
|
||||
)
|
||||
) {
|
||||
holder.binding.nameText.text = sharedApplication!!.getString(R.string.nc_guest)
|
||||
}
|
||||
|
||||
var userType = ""
|
||||
when (model.type) {
|
||||
Participant.ParticipantType.OWNER,
|
||||
Participant.ParticipantType.MODERATOR,
|
||||
Participant.ParticipantType.GUEST_MODERATOR -> {
|
||||
userType = sharedApplication!!.getString(R.string.nc_moderator)
|
||||
}
|
||||
|
||||
Participant.ParticipantType.USER -> {
|
||||
userType = sharedApplication!!.getString(R.string.nc_user)
|
||||
if (model.calculatedActorType == Participant.ActorType.GROUPS) {
|
||||
userType = sharedApplication!!.getString(R.string.nc_group)
|
||||
}
|
||||
if (model.calculatedActorType == Participant.ActorType.CIRCLES) {
|
||||
userType = sharedApplication!!.getString(R.string.nc_team)
|
||||
}
|
||||
}
|
||||
|
||||
Participant.ParticipantType.GUEST -> {
|
||||
userType = sharedApplication!!.getString(R.string.nc_guest)
|
||||
if (model.calculatedActorType == Participant.ActorType.EMAILS) {
|
||||
userType = sharedApplication!!.getString(R.string.nc_guest)
|
||||
}
|
||||
|
||||
if (model.invitedActorId?.isNotEmpty() == true &&
|
||||
ConversationUtils.isParticipantOwnerOrModerator(conversation)
|
||||
) {
|
||||
holder.binding.conversationInfoStatusMessage.text = model.invitedActorId
|
||||
alignUsernameVertical(holder, 0f)
|
||||
}
|
||||
}
|
||||
|
||||
Participant.ParticipantType.USER_FOLLOWING_LINK -> {
|
||||
userType = sharedApplication!!.getString(R.string.nc_following_link)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
if (userType != sharedApplication!!.getString(R.string.nc_user)) {
|
||||
holder.binding.secondaryText.text = "($userType)"
|
||||
}
|
||||
}
|
||||
|
||||
private fun setOnlineStateColor(holder: ParticipantItemViewHolder) {
|
||||
if (!isOnline) {
|
||||
holder.binding.nameText.setTextColor(
|
||||
ResourcesCompat.getColor(
|
||||
holder.binding.nameText.context.resources,
|
||||
R.color.medium_emphasis_text,
|
||||
null
|
||||
)
|
||||
)
|
||||
holder.binding.avatarView.setAlpha(NOT_ONLINE_ALPHA)
|
||||
} else {
|
||||
holder.binding.nameText.setTextColor(
|
||||
ResourcesCompat.getColor(
|
||||
holder.binding.nameText.context.resources,
|
||||
R.color.high_emphasis_text,
|
||||
null
|
||||
)
|
||||
)
|
||||
holder.binding.avatarView.setAlpha(1.0f)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatInvalid")
|
||||
private fun showCallIcons(holder: ParticipantItemViewHolder) {
|
||||
val resources = sharedApplication!!.resources
|
||||
val inCallFlag = model.inCall
|
||||
if (inCallFlag and InCallFlags.WITH_PHONE.toLong() > 0) {
|
||||
holder.binding.videoCallIcon.setImageResource(R.drawable.ic_call_grey_600_24dp)
|
||||
holder.binding.videoCallIcon.setVisibility(View.VISIBLE)
|
||||
holder.binding.videoCallIcon.setContentDescription(
|
||||
resources.getString(R.string.nc_call_state_with_phone, model.displayName)
|
||||
)
|
||||
} else if (inCallFlag and InCallFlags.WITH_VIDEO.toLong() > 0) {
|
||||
holder.binding.videoCallIcon.setImageResource(R.drawable.ic_videocam_grey_600_24dp)
|
||||
holder.binding.videoCallIcon.setVisibility(View.VISIBLE)
|
||||
holder.binding.videoCallIcon.setContentDescription(
|
||||
resources.getString(R.string.nc_call_state_with_video, model.displayName)
|
||||
)
|
||||
} else if (inCallFlag > InCallFlags.DISCONNECTED) {
|
||||
holder.binding.videoCallIcon.setImageResource(R.drawable.ic_mic_grey_600_24dp)
|
||||
holder.binding.videoCallIcon.setVisibility(View.VISIBLE)
|
||||
holder.binding.videoCallIcon.setContentDescription(
|
||||
resources.getString(R.string.nc_call_state_in_call, model.displayName)
|
||||
)
|
||||
} else {
|
||||
holder.binding.videoCallIcon.setVisibility(View.GONE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadAvatars(holder: ParticipantItemViewHolder) {
|
||||
when (model.calculatedActorType) {
|
||||
Participant.ActorType.GROUPS -> {
|
||||
holder.binding.avatarView.loadDefaultGroupCallAvatar(viewThemeUtils)
|
||||
}
|
||||
|
||||
Participant.ActorType.CIRCLES -> {
|
||||
holder.binding.avatarView.loadTeamAvatar(viewThemeUtils)
|
||||
}
|
||||
|
||||
Participant.ActorType.USERS -> {
|
||||
holder.binding.avatarView.loadUserAvatar(user, model.calculatedActorId!!, true, false)
|
||||
}
|
||||
|
||||
Participant.ActorType.GUESTS, Participant.ActorType.EMAILS -> {
|
||||
val actorName = model.displayName
|
||||
if (!actorName.isNullOrBlank()) {
|
||||
holder.binding.avatarView.loadFirstLetterAvatar(actorName)
|
||||
} else {
|
||||
holder.binding.avatarView.loadDefaultAvatar(viewThemeUtils)
|
||||
}
|
||||
}
|
||||
|
||||
Participant.ActorType.FEDERATED -> {
|
||||
val darkTheme = if (DisplayUtils.isDarkModeOn(context)) 1 else 0
|
||||
holder.binding.avatarView.loadFederatedUserAvatar(
|
||||
user,
|
||||
user.baseUrl!!,
|
||||
conversation.token,
|
||||
model.actorId!!,
|
||||
darkTheme,
|
||||
true,
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
Participant.ActorType.PHONES -> {
|
||||
holder.binding.avatarView.loadPhoneAvatar(viewThemeUtils)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.w(TAG, "Avatar not shown because of unknown ActorType " + model.calculatedActorType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun drawStatus(holder: ParticipantItemViewHolder) {
|
||||
val size = convertDpToPixel(STATUS_SIZE_IN_DP, context)
|
||||
holder.binding.userStatusImage.setImageDrawable(
|
||||
StatusDrawable(
|
||||
model.status,
|
||||
NO_ICON,
|
||||
size,
|
||||
context.resources.getColor(R.color.bg_default),
|
||||
context
|
||||
)
|
||||
)
|
||||
if (model.statusMessage != null) {
|
||||
holder.binding.conversationInfoStatusMessage.text = model.statusMessage
|
||||
alignUsernameVertical(holder, 0f)
|
||||
} else {
|
||||
holder.binding.conversationInfoStatusMessage.text = ""
|
||||
alignUsernameVertical(holder, 10f)
|
||||
}
|
||||
if (model.statusIcon != null && model.statusIcon!!.isNotEmpty()) {
|
||||
holder.binding.participantStatusEmoji.setText(model.statusIcon)
|
||||
} else {
|
||||
holder.binding.participantStatusEmoji.visibility = View.GONE
|
||||
}
|
||||
if (model.status != null && model.status == StatusType.DND.string) {
|
||||
if (model.statusMessage == null || model.statusMessage!!.isEmpty()) {
|
||||
holder.binding.conversationInfoStatusMessage.setText(R.string.dnd)
|
||||
}
|
||||
} else if (model.status != null && model.status == StatusType.BUSY.string) {
|
||||
if (model.statusMessage == null || model.statusMessage!!.isEmpty()) {
|
||||
holder.binding.conversationInfoStatusMessage.setText(R.string.busy)
|
||||
}
|
||||
} else if (model.status != null && model.status == StatusType.AWAY.string) {
|
||||
if (model.statusMessage == null || model.statusMessage!!.isEmpty()) {
|
||||
holder.binding.conversationInfoStatusMessage.setText(R.string.away)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun alignUsernameVertical(holder: ParticipantItemViewHolder, densityPixelsFromTop: Float) {
|
||||
val layoutParams = holder.binding.nameText.layoutParams as ConstraintLayout.LayoutParams
|
||||
layoutParams.topMargin = convertDpToPixel(densityPixelsFromTop, context).toInt()
|
||||
holder.binding.nameText.setLayoutParams(layoutParams)
|
||||
}
|
||||
|
||||
override fun filter(constraint: String?): Boolean =
|
||||
model.displayName != null &&
|
||||
(
|
||||
Pattern.compile(constraint, Pattern.CASE_INSENSITIVE or Pattern.LITERAL)
|
||||
.matcher(model.displayName!!.trim()).find() ||
|
||||
Pattern.compile(constraint, Pattern.CASE_INSENSITIVE or Pattern.LITERAL)
|
||||
.matcher(model.calculatedActorId!!.trim()).find()
|
||||
)
|
||||
|
||||
class ParticipantItemViewHolder internal constructor(view: View?, adapter: FlexibleAdapter<*>?) :
|
||||
FlexibleViewHolder(view, adapter) {
|
||||
var binding: RvItemConversationInfoParticipantBinding
|
||||
|
||||
init {
|
||||
binding = RvItemConversationInfoParticipantBinding.bind(view!!)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = ParticipantItem::class.simpleName
|
||||
private const val STATUS_SIZE_IN_DP = 9f
|
||||
private const val NO_ICON = ""
|
||||
private const val NOT_ONLINE_ALPHA = 0.38f
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.items
|
||||
|
||||
import android.view.View
|
||||
import ru.f7cloud.talk.R
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
|
||||
class SpacerItem(private val height: Int) : AbstractFlexibleItem<SpacerItem.ViewHolder>() {
|
||||
|
||||
override fun getLayoutRes(): Int = R.layout.item_spacer
|
||||
|
||||
override fun createViewHolder(view: View?, adapter: FlexibleAdapter<IFlexible<*>?>?): ViewHolder =
|
||||
ViewHolder(view!!, adapter!!)
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<*>?>?,
|
||||
holder: ViewHolder,
|
||||
position: Int,
|
||||
payloads: MutableList<Any>?
|
||||
) {
|
||||
holder.itemView.layoutParams.height = height
|
||||
}
|
||||
|
||||
override fun equals(other: Any?) = other is SpacerItem
|
||||
|
||||
override fun hashCode(): Int = 0
|
||||
|
||||
class ViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter)
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Christian Reiner <foss@christian-reiner.info>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package ru.f7cloud.talk.adapters.messages
|
||||
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import ru.f7cloud.talk.databinding.ItemCustomOutcomingDeckCardMessageBinding
|
||||
import ru.f7cloud.talk.databinding.ItemCustomOutcomingLinkPreviewMessageBinding
|
||||
import ru.f7cloud.talk.databinding.ItemCustomOutcomingLocationMessageBinding
|
||||
import ru.f7cloud.talk.databinding.ItemCustomOutcomingPollMessageBinding
|
||||
import ru.f7cloud.talk.databinding.ItemCustomOutcomingTextMessageBinding
|
||||
import ru.f7cloud.talk.databinding.ItemCustomOutcomingVoiceMessageBinding
|
||||
import ru.f7cloud.talk.models.domain.ConversationModel
|
||||
import ru.f7cloud.talk.models.json.conversations.ConversationEnums.ConversationType
|
||||
|
||||
interface AdjustableMessageHolderInterface {
|
||||
|
||||
val binding: ViewBinding
|
||||
|
||||
fun adjustIfNoteToSelf(currentConversation: ConversationModel?) {
|
||||
if (currentConversation?.type == ConversationType.NOTE_TO_SELF) {
|
||||
when (this.binding.javaClass) {
|
||||
ItemCustomOutcomingTextMessageBinding::class.java ->
|
||||
(this.binding as ItemCustomOutcomingTextMessageBinding).bubble
|
||||
ItemCustomOutcomingDeckCardMessageBinding::class.java ->
|
||||
(this.binding as ItemCustomOutcomingDeckCardMessageBinding).bubble
|
||||
ItemCustomOutcomingLinkPreviewMessageBinding::class.java ->
|
||||
(this.binding as ItemCustomOutcomingLinkPreviewMessageBinding).bubble
|
||||
ItemCustomOutcomingPollMessageBinding::class.java ->
|
||||
(this.binding as ItemCustomOutcomingPollMessageBinding).bubble
|
||||
ItemCustomOutcomingVoiceMessageBinding::class.java ->
|
||||
(this.binding as ItemCustomOutcomingVoiceMessageBinding).bubble
|
||||
ItemCustomOutcomingLocationMessageBinding::class.java ->
|
||||
(this.binding as ItemCustomOutcomingLocationMessageBinding).bubble
|
||||
else -> null
|
||||
}?.let {
|
||||
RelativeLayout.LayoutParams(binding.root.layoutParams).apply {
|
||||
marginStart = 0
|
||||
marginEnd = 0
|
||||
}.run {
|
||||
it.layoutParams = this
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Julius Linus <julius.linus@f7cloud.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.messages
|
||||
|
||||
interface CallStartedMessageInterface {
|
||||
fun joinAudioCall()
|
||||
fun joinVideoCall()
|
||||
}
|
||||
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.messages
|
||||
|
||||
import ru.f7cloud.talk.chat.data.model.ChatMessage
|
||||
|
||||
interface CommonMessageInterface {
|
||||
fun onLongClickReactions(chatMessage: ChatMessage)
|
||||
fun onClickReaction(chatMessage: ChatMessage, emoji: String)
|
||||
fun onOpenMessageActionsDialog(chatMessage: ChatMessage)
|
||||
fun openThread(chatMessage: ChatMessage)
|
||||
}
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package ru.f7cloud.talk.adapters.messages
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import autodagger.AutoInjector
|
||||
import coil.load
|
||||
import com.nextcloud.android.common.ui.theme.utils.ColorRole
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.api.NcApi
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication.Companion.sharedApplication
|
||||
import ru.f7cloud.talk.chat.ChatActivity
|
||||
import ru.f7cloud.talk.chat.data.model.ChatMessage
|
||||
import ru.f7cloud.talk.databinding.ItemCustomIncomingDeckCardMessageBinding
|
||||
import ru.f7cloud.talk.ui.theme.ViewThemeUtils
|
||||
import ru.f7cloud.talk.utils.ApiUtils
|
||||
import ru.f7cloud.talk.utils.ChatMessageUtils
|
||||
import ru.f7cloud.talk.utils.DateUtils
|
||||
import ru.f7cloud.talk.utils.message.MessageUtils
|
||||
import ru.f7cloud.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(F7cloudTalkApplication::class)
|
||||
class IncomingDeckCardViewHolder(incomingView: View, payload: Any) :
|
||||
MessageHolders
|
||||
.IncomingTextMessageViewHolder<ChatMessage>(incomingView, payload) {
|
||||
|
||||
private val binding: ItemCustomIncomingDeckCardMessageBinding =
|
||||
ItemCustomIncomingDeckCardMessageBinding.bind(itemView)
|
||||
|
||||
@Inject
|
||||
lateinit var context: Context
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var messageUtils: MessageUtils
|
||||
|
||||
@Inject
|
||||
lateinit var dateUtils: DateUtils
|
||||
|
||||
@Inject
|
||||
lateinit var ncApi: NcApi
|
||||
|
||||
lateinit var message: ChatMessage
|
||||
|
||||
lateinit var commonMessageInterface: CommonMessageInterface
|
||||
|
||||
var stackName: String? = null
|
||||
var cardName: String? = null
|
||||
var boardName: String? = null
|
||||
var cardLink: String? = null
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
this.message = message
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
|
||||
setAvatarAndAuthorOnMessageItem(message)
|
||||
showDeckCard(message)
|
||||
|
||||
colorizeMessageBubble(message)
|
||||
|
||||
binding.cardView.findViewById<ImageView>(R.id.deckCardImage)?.let {
|
||||
viewThemeUtils.platform.colorImageView(it, ColorRole.SECONDARY)
|
||||
}
|
||||
|
||||
itemView.isSelected = false
|
||||
|
||||
// parent message handling
|
||||
setParentMessageDataOnMessageItem(message)
|
||||
|
||||
binding.cardView.setOnLongClickListener { l: View? ->
|
||||
commonMessageInterface.onOpenMessageActionsDialog(message)
|
||||
true
|
||||
}
|
||||
|
||||
binding.cardView.setOnClickListener {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, cardLink!!.toUri())
|
||||
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(browserIntent)
|
||||
}
|
||||
|
||||
itemView.setTag(R.string.replyable_message_view_tag, message.replyable)
|
||||
|
||||
Reaction().showReactions(
|
||||
message,
|
||||
::clickOnReaction,
|
||||
::longClickOnReaction,
|
||||
binding.reactions,
|
||||
binding.messageTime.context,
|
||||
false,
|
||||
viewThemeUtils
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatInvalid")
|
||||
private fun showDeckCard(message: ChatMessage) {
|
||||
if (message.messageParameters != null && message.messageParameters!!.size > 0) {
|
||||
for (key in message.messageParameters!!.keys) {
|
||||
val individualHashMap: Map<String?, String?> = message.messageParameters!![key]!!
|
||||
if (individualHashMap["type"] == "deck-card") {
|
||||
cardName = individualHashMap["name"]
|
||||
stackName = individualHashMap["stackname"]
|
||||
boardName = individualHashMap["boardname"]
|
||||
cardLink = individualHashMap["link"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cardName?.isNotEmpty() == true) {
|
||||
val cardDescription = String.format(
|
||||
context.resources.getString(R.string.deck_card_description),
|
||||
stackName,
|
||||
boardName
|
||||
)
|
||||
binding.cardName.visibility = View.VISIBLE
|
||||
binding.cardDescription.visibility = View.VISIBLE
|
||||
binding.cardName.text = cardName
|
||||
binding.cardDescription.text = cardDescription
|
||||
}
|
||||
}
|
||||
|
||||
private fun longClickOnReaction(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.onLongClickReactions(chatMessage)
|
||||
}
|
||||
|
||||
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
|
||||
commonMessageInterface.onClickReaction(chatMessage, emoji)
|
||||
}
|
||||
|
||||
private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) {
|
||||
val actorName = message.actorDisplayName
|
||||
if (!actorName.isNullOrBlank()) {
|
||||
binding.messageAuthor.visibility = View.VISIBLE
|
||||
binding.messageAuthor.text = actorName
|
||||
binding.messageUserAvatar.setOnClickListener {
|
||||
(payload as? MessagePayload)?.profileBottomSheet?.showFor(message, itemView.context)
|
||||
}
|
||||
} else {
|
||||
binding.messageAuthor.setText(R.string.nc_nick_guest)
|
||||
}
|
||||
|
||||
if (!message.isGrouped && !message.isOneToOneConversation && !message.isFormerOneToOneConversation) {
|
||||
ChatMessageUtils().setAvatarOnMessage(binding.messageUserAvatar, message, viewThemeUtils)
|
||||
} else {
|
||||
if (message.isOneToOneConversation || message.isFormerOneToOneConversation) {
|
||||
binding.messageUserAvatar.visibility = View.GONE
|
||||
} else {
|
||||
binding.messageUserAvatar.visibility = View.INVISIBLE
|
||||
}
|
||||
binding.messageAuthor.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun colorizeMessageBubble(message: ChatMessage) {
|
||||
viewThemeUtils.talk.themeIncomingMessageBubble(bubble, message.isGrouped, message.isDeleted)
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
|
||||
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
|
||||
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))
|
||||
|
||||
viewThemeUtils.talk.themeParentMessage(
|
||||
parentChatMessage,
|
||||
message,
|
||||
binding.messageQuote.quotedChatMessageView
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.visibility =
|
||||
if (!message.isDeleted &&
|
||||
message.parentMessageId != null &&
|
||||
message.parentMessageId != chatActivity.conversationThreadId
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error when processing parent message in view holder", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
|
||||
this.commonMessageInterface = commonMessageInterface
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = IncomingDeckCardViewHolder::class.java.simpleName
|
||||
}
|
||||
}
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2017-2019 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.messages
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import autodagger.AutoInjector
|
||||
import coil.load
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.api.NcApi
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication.Companion.sharedApplication
|
||||
import ru.f7cloud.talk.chat.ChatActivity
|
||||
import ru.f7cloud.talk.chat.data.model.ChatMessage
|
||||
import ru.f7cloud.talk.databinding.ItemCustomIncomingLinkPreviewMessageBinding
|
||||
import ru.f7cloud.talk.ui.theme.ViewThemeUtils
|
||||
import ru.f7cloud.talk.utils.ApiUtils
|
||||
import ru.f7cloud.talk.utils.ChatMessageUtils
|
||||
import ru.f7cloud.talk.utils.DateUtils
|
||||
import ru.f7cloud.talk.utils.message.MessageUtils
|
||||
import ru.f7cloud.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(F7cloudTalkApplication::class)
|
||||
class IncomingLinkPreviewMessageViewHolder(incomingView: View, payload: Any) :
|
||||
MessageHolders.IncomingTextMessageViewHolder<ChatMessage>(incomingView, payload) {
|
||||
|
||||
private val binding: ItemCustomIncomingLinkPreviewMessageBinding =
|
||||
ItemCustomIncomingLinkPreviewMessageBinding.bind(itemView)
|
||||
|
||||
@Inject
|
||||
lateinit var context: Context
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var messageUtils: MessageUtils
|
||||
|
||||
@Inject
|
||||
lateinit var dateUtils: DateUtils
|
||||
|
||||
@Inject
|
||||
lateinit var ncApi: NcApi
|
||||
|
||||
lateinit var message: ChatMessage
|
||||
|
||||
lateinit var commonMessageInterface: CommonMessageInterface
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
this.message = message
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
|
||||
var processedMessageText = messageUtils.enrichChatMessageText(
|
||||
binding.messageText.context,
|
||||
message,
|
||||
true,
|
||||
viewThemeUtils
|
||||
)
|
||||
|
||||
processedMessageText = messageUtils.processMessageParameters(
|
||||
binding.messageText.context,
|
||||
viewThemeUtils,
|
||||
processedMessageText!!,
|
||||
message,
|
||||
itemView
|
||||
)
|
||||
|
||||
binding.messageText.text = processedMessageText
|
||||
|
||||
setAvatarAndAuthorOnMessageItem(message)
|
||||
|
||||
colorizeMessageBubble(message)
|
||||
|
||||
itemView.isSelected = false
|
||||
|
||||
// parent message handling
|
||||
setParentMessageDataOnMessageItem(message)
|
||||
|
||||
LinkPreview().showLink(
|
||||
message,
|
||||
ncApi,
|
||||
binding.referenceInclude,
|
||||
itemView.context
|
||||
)
|
||||
binding.referenceInclude.referenceWrapper.setOnLongClickListener { l: View? ->
|
||||
commonMessageInterface.onOpenMessageActionsDialog(message)
|
||||
true
|
||||
}
|
||||
|
||||
itemView.setTag(R.string.replyable_message_view_tag, message.replyable)
|
||||
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
val showThreadButton = chatActivity.conversationThreadId == null && message.isThread
|
||||
if (showThreadButton) {
|
||||
binding.reactions.threadButton.visibility = View.VISIBLE
|
||||
binding.reactions.threadButton.setContent {
|
||||
ThreadButtonComposable(
|
||||
onButtonClick = { openThread(message) }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
binding.reactions.threadButton.visibility = View.GONE
|
||||
}
|
||||
|
||||
Reaction().showReactions(
|
||||
message,
|
||||
::clickOnReaction,
|
||||
::longClickOnReaction,
|
||||
binding.reactions,
|
||||
binding.messageTime.context,
|
||||
false,
|
||||
viewThemeUtils
|
||||
)
|
||||
}
|
||||
|
||||
private fun longClickOnReaction(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.onLongClickReactions(chatMessage)
|
||||
}
|
||||
|
||||
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
|
||||
commonMessageInterface.onClickReaction(chatMessage, emoji)
|
||||
}
|
||||
|
||||
private fun openThread(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.openThread(chatMessage)
|
||||
}
|
||||
|
||||
private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) {
|
||||
val actorName = message.actorDisplayName
|
||||
if (!actorName.isNullOrBlank()) {
|
||||
binding.messageAuthor.visibility = View.VISIBLE
|
||||
binding.messageAuthor.text = actorName
|
||||
binding.messageUserAvatar.setOnClickListener {
|
||||
(payload as? MessagePayload)?.profileBottomSheet?.showFor(message, itemView.context)
|
||||
}
|
||||
} else {
|
||||
binding.messageAuthor.setText(R.string.nc_nick_guest)
|
||||
}
|
||||
|
||||
if (!message.isGrouped && !message.isOneToOneConversation && !message.isFormerOneToOneConversation) {
|
||||
ChatMessageUtils().setAvatarOnMessage(binding.messageUserAvatar, message, viewThemeUtils)
|
||||
} else {
|
||||
if (message.isOneToOneConversation || message.isFormerOneToOneConversation) {
|
||||
binding.messageUserAvatar.visibility = View.GONE
|
||||
} else {
|
||||
binding.messageUserAvatar.visibility = View.INVISIBLE
|
||||
}
|
||||
binding.messageAuthor.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun colorizeMessageBubble(message: ChatMessage) {
|
||||
viewThemeUtils.talk.themeIncomingMessageBubble(bubble, message.isGrouped, message.isDeleted)
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
|
||||
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
|
||||
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))
|
||||
|
||||
viewThemeUtils.talk.themeParentMessage(
|
||||
parentChatMessage,
|
||||
message,
|
||||
binding.messageQuote.quotedChatMessageView
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.visibility =
|
||||
if (!message.isDeleted &&
|
||||
message.parentMessageId != null &&
|
||||
message.parentMessageId != chatActivity.conversationThreadId
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error when processing parent message in view holder", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
|
||||
this.commonMessageInterface = commonMessageInterface
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = IncomingLinkPreviewMessageViewHolder::class.java.simpleName
|
||||
}
|
||||
}
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2021 Tim Krüger <t@timkrueger.me>
|
||||
* SPDX-FileCopyrightText: 2021 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.messages
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.core.net.toUri
|
||||
import autodagger.AutoInjector
|
||||
import coil.load
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication.Companion.sharedApplication
|
||||
import ru.f7cloud.talk.chat.ChatActivity
|
||||
import ru.f7cloud.talk.chat.data.model.ChatMessage
|
||||
import ru.f7cloud.talk.databinding.ItemCustomIncomingLocationMessageBinding
|
||||
import ru.f7cloud.talk.ui.theme.ViewThemeUtils
|
||||
import ru.f7cloud.talk.utils.ApiUtils
|
||||
import ru.f7cloud.talk.utils.ChatMessageUtils
|
||||
import ru.f7cloud.talk.utils.DateUtils
|
||||
import ru.f7cloud.talk.utils.UriUtils
|
||||
import ru.f7cloud.talk.utils.message.MessageUtils
|
||||
import ru.f7cloud.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
|
||||
|
||||
@AutoInjector(F7cloudTalkApplication::class)
|
||||
class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) :
|
||||
MessageHolders.IncomingTextMessageViewHolder<ChatMessage>(incomingView, payload) {
|
||||
private val binding: ItemCustomIncomingLocationMessageBinding =
|
||||
ItemCustomIncomingLocationMessageBinding.bind(itemView)
|
||||
|
||||
var locationLon: String? = ""
|
||||
var locationLat: String? = ""
|
||||
var locationName: String? = ""
|
||||
var locationGeoLink: String? = ""
|
||||
|
||||
@Inject
|
||||
lateinit var context: Context
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var messageUtils: MessageUtils
|
||||
|
||||
@Inject
|
||||
lateinit var dateUtils: DateUtils
|
||||
|
||||
lateinit var commonMessageInterface: CommonMessageInterface
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
|
||||
setAvatarAndAuthorOnMessageItem(message)
|
||||
|
||||
colorizeMessageBubble(message)
|
||||
|
||||
itemView.isSelected = false
|
||||
|
||||
val textSize = context.resources!!.getDimension(R.dimen.chat_text_size)
|
||||
binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
||||
binding.messageText.text = message.text
|
||||
|
||||
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
|
||||
// parent message handling
|
||||
setParentMessageDataOnMessageItem(message)
|
||||
|
||||
// geo-location
|
||||
setLocationDataOnMessageItem(message)
|
||||
|
||||
Reaction().showReactions(
|
||||
message,
|
||||
::clickOnReaction,
|
||||
::longClickOnReaction,
|
||||
binding.reactions,
|
||||
binding.messageText.context,
|
||||
false,
|
||||
viewThemeUtils
|
||||
)
|
||||
}
|
||||
|
||||
private fun longClickOnReaction(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.onLongClickReactions(chatMessage)
|
||||
}
|
||||
|
||||
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
|
||||
commonMessageInterface.onClickReaction(chatMessage, emoji)
|
||||
}
|
||||
|
||||
private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) {
|
||||
val actorName = message.actorDisplayName
|
||||
if (!actorName.isNullOrBlank()) {
|
||||
binding.messageAuthor.visibility = View.VISIBLE
|
||||
binding.messageAuthor.text = actorName
|
||||
binding.messageUserAvatar.setOnClickListener {
|
||||
(payload as? MessagePayload)?.profileBottomSheet?.showFor(message, itemView.context)
|
||||
}
|
||||
} else {
|
||||
binding.messageAuthor.setText(R.string.nc_nick_guest)
|
||||
}
|
||||
|
||||
if (!message.isGrouped && !message.isOneToOneConversation && !message.isFormerOneToOneConversation) {
|
||||
ChatMessageUtils().setAvatarOnMessage(binding.messageUserAvatar, message, viewThemeUtils)
|
||||
} else {
|
||||
if (message.isOneToOneConversation || message.isFormerOneToOneConversation) {
|
||||
binding.messageUserAvatar.visibility = View.GONE
|
||||
} else {
|
||||
binding.messageUserAvatar.visibility = View.INVISIBLE
|
||||
}
|
||||
binding.messageAuthor.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun colorizeMessageBubble(message: ChatMessage) {
|
||||
viewThemeUtils.talk.themeIncomingMessageBubble(bubble, message.isGrouped, message.isDeleted)
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
|
||||
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
|
||||
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))
|
||||
|
||||
viewThemeUtils.talk.themeParentMessage(
|
||||
parentChatMessage,
|
||||
message,
|
||||
binding.messageQuote.quotedChatMessageView
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.visibility =
|
||||
if (!message.isDeleted &&
|
||||
message.parentMessageId != null &&
|
||||
message.parentMessageId != chatActivity.conversationThreadId
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error when processing parent message in view holder", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled", "ClickableViewAccessibility")
|
||||
private fun setLocationDataOnMessageItem(message: ChatMessage) {
|
||||
if (message.messageParameters != null && message.messageParameters!!.size > 0) {
|
||||
for (key in message.messageParameters!!.keys) {
|
||||
val individualHashMap: Map<String?, String?> = message.messageParameters!![key]!!
|
||||
if (individualHashMap["type"] == "geo-location") {
|
||||
locationLon = individualHashMap["longitude"]
|
||||
locationLat = individualHashMap["latitude"]
|
||||
locationName = individualHashMap["name"]
|
||||
locationGeoLink = individualHashMap["id"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.webview.settings.javaScriptEnabled = true
|
||||
|
||||
binding.webview.webViewClient = object : WebViewClient() {
|
||||
@Deprecated("Use shouldOverrideUrlLoading(WebView view, WebResourceRequest request)")
|
||||
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean =
|
||||
if (url != null && UriUtils.hasHttpProtocolPrefixed(url)) {
|
||||
view?.context?.startActivity(Intent(Intent.ACTION_VIEW, url.toUri()))
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
val urlStringBuffer = StringBuffer("file:///android_asset/leafletMapMessagePreview.html")
|
||||
urlStringBuffer.append(
|
||||
"?mapProviderUrl=" + URLEncoder.encode(context.getString(R.string.osm_tile_server_url))
|
||||
)
|
||||
urlStringBuffer.append(
|
||||
"&mapProviderAttribution=" + URLEncoder.encode(context.getString(R.string.osm_tile_server_attributation))
|
||||
)
|
||||
urlStringBuffer.append("&locationLat=" + URLEncoder.encode(locationLat))
|
||||
urlStringBuffer.append("&locationLon=" + URLEncoder.encode(locationLon))
|
||||
urlStringBuffer.append("&locationName=" + URLEncoder.encode(locationName))
|
||||
urlStringBuffer.append("&locationGeoLink=" + URLEncoder.encode(locationGeoLink))
|
||||
|
||||
binding.webview.loadUrl(urlStringBuffer.toString())
|
||||
|
||||
binding.webview.setOnTouchListener(object : View.OnTouchListener {
|
||||
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
|
||||
when (event?.action) {
|
||||
MotionEvent.ACTION_UP -> openGeoLink()
|
||||
}
|
||||
|
||||
return v?.onTouchEvent(event) ?: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun openGeoLink() {
|
||||
if (!locationGeoLink.isNullOrEmpty()) {
|
||||
val geoLinkWithMarker = addMarkerToGeoLink(locationGeoLink!!)
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, geoLinkWithMarker.toUri())
|
||||
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(browserIntent)
|
||||
} else {
|
||||
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
|
||||
Log.e(TAG, "locationGeoLink was null or empty")
|
||||
}
|
||||
}
|
||||
|
||||
private fun addMarkerToGeoLink(locationGeoLink: String): String = locationGeoLink.replace("geo:", "geo:0,0?q=")
|
||||
|
||||
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
|
||||
this.commonMessageInterface = commonMessageInterface
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "LocInMessageView"
|
||||
}
|
||||
}
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.messages
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import autodagger.AutoInjector
|
||||
import coil.load
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.api.NcApi
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication.Companion.sharedApplication
|
||||
import ru.f7cloud.talk.chat.ChatActivity
|
||||
import ru.f7cloud.talk.chat.data.model.ChatMessage
|
||||
import ru.f7cloud.talk.databinding.ItemCustomIncomingPollMessageBinding
|
||||
import ru.f7cloud.talk.polls.ui.PollMainDialogFragment
|
||||
import ru.f7cloud.talk.ui.theme.ViewThemeUtils
|
||||
import ru.f7cloud.talk.utils.ApiUtils
|
||||
import ru.f7cloud.talk.utils.ChatMessageUtils
|
||||
import ru.f7cloud.talk.utils.DateUtils
|
||||
import ru.f7cloud.talk.utils.message.MessageUtils
|
||||
import ru.f7cloud.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(F7cloudTalkApplication::class)
|
||||
class IncomingPollMessageViewHolder(incomingView: View, payload: Any) :
|
||||
MessageHolders.IncomingTextMessageViewHolder<ChatMessage>(incomingView, payload) {
|
||||
|
||||
private val binding: ItemCustomIncomingPollMessageBinding = ItemCustomIncomingPollMessageBinding.bind(itemView)
|
||||
|
||||
@Inject
|
||||
lateinit var context: Context
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var messageUtils: MessageUtils
|
||||
|
||||
@Inject
|
||||
lateinit var dateUtils: DateUtils
|
||||
|
||||
@Inject
|
||||
lateinit var ncApi: NcApi
|
||||
|
||||
lateinit var message: ChatMessage
|
||||
|
||||
lateinit var commonMessageInterface: CommonMessageInterface
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
this.message = message
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
|
||||
setAvatarAndAuthorOnMessageItem(message)
|
||||
|
||||
colorizeMessageBubble(message)
|
||||
|
||||
itemView.isSelected = false
|
||||
|
||||
// parent message handling
|
||||
setParentMessageDataOnMessageItem(message)
|
||||
|
||||
setPollPreview(message)
|
||||
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
Thread().showThreadPreview(
|
||||
chatActivity,
|
||||
message,
|
||||
threadBinding = binding.threadTitleWrapper,
|
||||
reactionsBinding = binding.reactions,
|
||||
openThread = { openThread(message) }
|
||||
)
|
||||
|
||||
Reaction().showReactions(
|
||||
message,
|
||||
::clickOnReaction,
|
||||
::longClickOnReaction,
|
||||
binding.reactions,
|
||||
binding.messageTime.context,
|
||||
false,
|
||||
viewThemeUtils
|
||||
)
|
||||
}
|
||||
|
||||
private fun longClickOnReaction(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.onLongClickReactions(chatMessage)
|
||||
}
|
||||
|
||||
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
|
||||
commonMessageInterface.onClickReaction(chatMessage, emoji)
|
||||
}
|
||||
|
||||
private fun openThread(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.openThread(chatMessage)
|
||||
}
|
||||
|
||||
private fun setPollPreview(message: ChatMessage) {
|
||||
var pollId: String? = null
|
||||
var pollName: String? = null
|
||||
|
||||
if (message.messageParameters != null && message.messageParameters!!.size > 0) {
|
||||
for (key in message.messageParameters!!.keys) {
|
||||
val individualHashMap: Map<String?, String?> = message.messageParameters!![key]!!
|
||||
if (individualHashMap["type"] == "talk-poll") {
|
||||
pollId = individualHashMap["id"]
|
||||
pollName = individualHashMap["name"].toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pollId != null && pollName != null) {
|
||||
binding.messagePollTitle.text = pollName
|
||||
|
||||
val roomToken = (payload as? MessagePayload)!!.roomToken
|
||||
val isOwnerOrModerator = (payload as? MessagePayload)!!.isOwnerOrModerator ?: false
|
||||
|
||||
binding.bubble.setOnClickListener {
|
||||
val pollVoteDialog = PollMainDialogFragment.newInstance(
|
||||
message.activeUser!!,
|
||||
roomToken,
|
||||
isOwnerOrModerator,
|
||||
pollId,
|
||||
pollName
|
||||
)
|
||||
pollVoteDialog.show(
|
||||
(binding.messagePollIcon.context as ChatActivity).supportFragmentManager,
|
||||
TAG
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) {
|
||||
val actorName = message.actorDisplayName
|
||||
if (!actorName.isNullOrBlank()) {
|
||||
binding.messageAuthor.visibility = View.VISIBLE
|
||||
binding.messageAuthor.text = actorName
|
||||
binding.messageUserAvatar.setOnClickListener {
|
||||
(payload as? MessagePayload)?.profileBottomSheet?.showFor(message, itemView.context)
|
||||
}
|
||||
} else {
|
||||
binding.messageAuthor.setText(R.string.nc_nick_guest)
|
||||
}
|
||||
|
||||
if (!message.isGrouped && !message.isOneToOneConversation && !message.isFormerOneToOneConversation) {
|
||||
ChatMessageUtils().setAvatarOnMessage(binding.messageUserAvatar, message, viewThemeUtils)
|
||||
} else {
|
||||
if (message.isOneToOneConversation || message.isFormerOneToOneConversation) {
|
||||
binding.messageUserAvatar.visibility = View.GONE
|
||||
} else {
|
||||
binding.messageUserAvatar.visibility = View.INVISIBLE
|
||||
}
|
||||
binding.messageAuthor.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun colorizeMessageBubble(message: ChatMessage) {
|
||||
viewThemeUtils.talk.themeIncomingMessageBubble(bubble, message.isGrouped, message.isDeleted)
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
|
||||
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
|
||||
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))
|
||||
|
||||
viewThemeUtils.talk.themeParentMessage(
|
||||
parentChatMessage,
|
||||
message,
|
||||
binding.messageQuote.quotedChatMessageView
|
||||
)
|
||||
binding.messageQuote.quotedChatMessageView.visibility =
|
||||
if (!message.isDeleted &&
|
||||
message.parentMessageId != null &&
|
||||
message.parentMessageId != chatActivity.conversationThreadId
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error when processing parent message in view holder", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
|
||||
this.commonMessageInterface = commonMessageInterface
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = IncomingPollMessageViewHolder::class.java.simpleName
|
||||
}
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022-2023 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2021-2022 Tim Krüger <t@timkrueger.me>
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.messages;
|
||||
|
||||
import android.text.Spanned;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import com.google.android.material.card.MaterialCardView;
|
||||
import ru.f7cloud.talk.R;
|
||||
import ru.f7cloud.talk.chat.data.model.ChatMessage;
|
||||
import ru.f7cloud.talk.databinding.ItemCustomIncomingPreviewMessageBinding;
|
||||
import ru.f7cloud.talk.databinding.ItemThreadTitleBinding;
|
||||
import ru.f7cloud.talk.databinding.ReactionsInsideMessageBinding;
|
||||
import ru.f7cloud.talk.utils.TextMatchers;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.emoji2.widget.EmojiTextView;
|
||||
|
||||
public class IncomingPreviewMessageViewHolder extends PreviewMessageViewHolder {
|
||||
private final ItemCustomIncomingPreviewMessageBinding binding;
|
||||
|
||||
public IncomingPreviewMessageViewHolder(View itemView, Object payload) {
|
||||
super(itemView, payload);
|
||||
binding = ItemCustomIncomingPreviewMessageBinding.bind(itemView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(@NonNull ChatMessage message) {
|
||||
super.onBind(message);
|
||||
if(!message.isVoiceMessage()
|
||||
&& !Objects.equals(message.getMessage(), "{file}")
|
||||
) {
|
||||
Spanned processedMessageText = null;
|
||||
binding.bubble.setBackgroundResource(R.drawable.shape_grouped_incoming_message);
|
||||
if (viewThemeUtils != null ) {
|
||||
processedMessageText = messageUtils.enrichChatMessageText(
|
||||
binding.messageCaption.getContext(),
|
||||
message,
|
||||
true,
|
||||
viewThemeUtils);
|
||||
viewThemeUtils.talk.themeIncomingMessageBubble(binding.bubble, true, false,
|
||||
false);
|
||||
}
|
||||
|
||||
if (processedMessageText != null) {
|
||||
processedMessageText = messageUtils.processMessageParameters(
|
||||
binding.messageCaption.getContext(),
|
||||
viewThemeUtils,
|
||||
processedMessageText,
|
||||
message,
|
||||
binding.bubble);
|
||||
}
|
||||
binding.bubble.setOnClickListener(null);
|
||||
|
||||
float textSize = 0;
|
||||
if (context != null) {
|
||||
textSize = context.getResources().getDimension(R.dimen.chat_text_size);
|
||||
}
|
||||
HashMap<String, HashMap<String, String>> messageParameters = message.getMessageParameters();
|
||||
if (
|
||||
(messageParameters == null || messageParameters.size() <= 0) &&
|
||||
TextMatchers.isMessageWithSingleEmoticonOnly(message.getText())
|
||||
) {
|
||||
textSize = (float) (textSize * IncomingTextMessageViewHolder.TEXT_SIZE_MULTIPLIER);
|
||||
itemView.setSelected(true);
|
||||
}
|
||||
binding.messageCaption.setVisibility(View.VISIBLE);
|
||||
binding.messageCaption.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
|
||||
binding.messageCaption.setText(processedMessageText);
|
||||
} else {
|
||||
binding.bubble.setBackground(null);
|
||||
binding.messageCaption.setVisibility(View.GONE);
|
||||
}
|
||||
binding.messageAuthor.setText(message.getActorDisplayName());
|
||||
binding.messageText.setTextColor(ContextCompat.getColor(binding.messageText.getContext(),
|
||||
R.color.no_emphasis_text));
|
||||
binding.messageTime.setTextColor(ContextCompat.getColor(binding.messageText.getContext(),
|
||||
R.color.no_emphasis_text));
|
||||
|
||||
binding.messageEditIndicator.setTextColor(ContextCompat.getColor(binding.messageText.getContext(),
|
||||
R.color.no_emphasis_text));
|
||||
|
||||
if(!message.isThread()) {
|
||||
binding.threadTitleWrapperContainer.setVisibility(View.GONE);
|
||||
} else {
|
||||
binding.threadTitleWrapperContainer.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public android.widget.TextView getMessageEditIndicator() {
|
||||
return binding.messageEditIndicator;
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public EmojiTextView getMessageText() {
|
||||
return binding.messageText;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public EmojiTextView getMessageCaption() {
|
||||
return binding.messageCaption;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProgressBar getProgressBar() {
|
||||
return binding.progressBar;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public View getPreviewContainer() {
|
||||
return binding.previewContainer;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public MaterialCardView getPreviewContactContainer() {
|
||||
return binding.contactContainer;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ImageView getPreviewContactPhoto() {
|
||||
return binding.contactPhoto;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public EmojiTextView getPreviewContactName() {
|
||||
return binding.contactName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProgressBar getPreviewContactProgressBar() {
|
||||
return binding.contactProgressBar;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactionsInsideMessageBinding getReactionsBinding(){ return binding.reactions; }
|
||||
|
||||
@Override
|
||||
public ItemThreadTitleBinding getThreadsBinding(){ return binding.threadTitleWrapper; }
|
||||
}
|
||||
+469
@@ -0,0 +1,469 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2021-2023 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.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 ru.f7cloud.talk.adapters.messages
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckBox
|
||||
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 ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication.Companion.sharedApplication
|
||||
import ru.f7cloud.talk.chat.ChatActivity
|
||||
import ru.f7cloud.talk.chat.data.ChatMessageRepository
|
||||
import ru.f7cloud.talk.chat.data.model.ChatMessage
|
||||
import ru.f7cloud.talk.data.user.model.User
|
||||
import ru.f7cloud.talk.databinding.ItemCustomIncomingTextMessageBinding
|
||||
import ru.f7cloud.talk.models.json.chat.ChatUtils
|
||||
import ru.f7cloud.talk.ui.theme.ViewThemeUtils
|
||||
import ru.f7cloud.talk.utils.ApiUtils
|
||||
import ru.f7cloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability
|
||||
import ru.f7cloud.talk.utils.ChatMessageUtils
|
||||
import ru.f7cloud.talk.utils.DateUtils
|
||||
import ru.f7cloud.talk.utils.SpreedFeatures
|
||||
import ru.f7cloud.talk.utils.TextMatchers
|
||||
import ru.f7cloud.talk.utils.database.user.CurrentUserProviderOld
|
||||
import ru.f7cloud.talk.utils.message.MessageCheckboxUtils.addCheckboxLine
|
||||
import ru.f7cloud.talk.utils.message.MessageCheckboxUtils.addPlainTextLine
|
||||
import ru.f7cloud.talk.utils.message.MessageCheckboxUtils.matchCheckbox
|
||||
import ru.f7cloud.talk.utils.message.MessageCheckboxUtils.updateMessageWithCheckboxStates
|
||||
import ru.f7cloud.talk.utils.message.MessageUtils
|
||||
import ru.f7cloud.talk.utils.preferences.AppPreferences
|
||||
import com.stfalcon.chatkit.messages.MessageHolders
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(F7cloudTalkApplication::class)
|
||||
class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
|
||||
MessageHolders.IncomingTextMessageViewHolder<ChatMessage>(itemView, payload) {
|
||||
|
||||
private val binding: ItemCustomIncomingTextMessageBinding = ItemCustomIncomingTextMessageBinding.bind(itemView)
|
||||
|
||||
@Inject
|
||||
lateinit var context: Context
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var messageUtils: MessageUtils
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
@Inject
|
||||
lateinit var dateUtils: DateUtils
|
||||
|
||||
@Inject
|
||||
lateinit var currentUserProvider: CurrentUserProviderOld
|
||||
|
||||
lateinit var commonMessageInterface: CommonMessageInterface
|
||||
|
||||
@Inject
|
||||
lateinit var chatRepository: ChatMessageRepository
|
||||
|
||||
private var job: Job? = null
|
||||
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
setAvatarAndAuthorOnMessageItem(message)
|
||||
colorizeMessageBubble(message)
|
||||
itemView.isSelected = false
|
||||
val user = currentUserProvider.currentUser.blockingGet()
|
||||
val hasCheckboxes = processCheckboxes(
|
||||
message,
|
||||
user
|
||||
)
|
||||
processMessage(message, hasCheckboxes)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun processMessage(message: ChatMessage, hasCheckboxes: Boolean) {
|
||||
var textSize = context.resources!!.getDimension(R.dimen.chat_text_size)
|
||||
if (!hasCheckboxes) {
|
||||
binding.messageText.visibility = View.VISIBLE
|
||||
binding.checkboxContainer.visibility = View.GONE
|
||||
var processedMessageText = messageUtils.enrichChatMessageText(
|
||||
binding.messageText.context,
|
||||
message,
|
||||
true,
|
||||
viewThemeUtils
|
||||
)
|
||||
|
||||
val spansFromString: Array<Any> = processedMessageText!!.getSpans(
|
||||
0,
|
||||
processedMessageText.length,
|
||||
Any::class.java
|
||||
)
|
||||
|
||||
if (spansFromString.isNotEmpty()) {
|
||||
binding.bubble.layoutParams.apply {
|
||||
width = FlexboxLayout.LayoutParams.MATCH_PARENT
|
||||
}
|
||||
binding.messageText.layoutParams.apply {
|
||||
width = FlexboxLayout.LayoutParams.MATCH_PARENT
|
||||
}
|
||||
} else {
|
||||
binding.bubble.layoutParams.apply {
|
||||
width = FlexboxLayout.LayoutParams.WRAP_CONTENT
|
||||
}
|
||||
binding.messageText.layoutParams.apply {
|
||||
width = FlexboxLayout.LayoutParams.WRAP_CONTENT
|
||||
}
|
||||
}
|
||||
|
||||
processedMessageText = messageUtils.processMessageParameters(
|
||||
binding.messageText.context,
|
||||
viewThemeUtils,
|
||||
processedMessageText,
|
||||
message,
|
||||
itemView
|
||||
)
|
||||
val messageParameters = message.messageParameters
|
||||
if (
|
||||
(messageParameters == null || messageParameters.size <= 0) &&
|
||||
TextMatchers.isMessageWithSingleEmoticonOnly(message.text)
|
||||
) {
|
||||
textSize = (textSize * TEXT_SIZE_MULTIPLIER).toFloat()
|
||||
itemView.isSelected = true
|
||||
binding.messageAuthor.visibility = View.GONE
|
||||
}
|
||||
binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
||||
binding.messageText.text = processedMessageText
|
||||
// just for debugging:
|
||||
// binding.messageText.text =
|
||||
// SpannableStringBuilder(processedMessageText).append(" (" + message.jsonMessageId + ")")
|
||||
} else {
|
||||
binding.checkboxContainer.visibility = View.VISIBLE
|
||||
binding.messageText.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (message.lastEditTimestamp != 0L && !message.isDeleted) {
|
||||
binding.messageEditIndicator.visibility = View.VISIBLE
|
||||
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.lastEditTimestamp!!)
|
||||
} else {
|
||||
binding.messageEditIndicator.visibility = View.GONE
|
||||
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
}
|
||||
viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT)
|
||||
|
||||
// parent message handling
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
binding.messageQuote.quotedChatMessageView.visibility =
|
||||
if (!message.isDeleted &&
|
||||
message.parentMessageId != null &&
|
||||
message.parentMessageId != chatActivity.conversationThreadId
|
||||
) {
|
||||
processParentMessage(message)
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.setOnLongClickListener { l: View? ->
|
||||
commonMessageInterface.onOpenMessageActionsDialog(message)
|
||||
true
|
||||
}
|
||||
|
||||
itemView.setTag(R.string.replyable_message_view_tag, message.replyable)
|
||||
|
||||
Thread().showThreadPreview(
|
||||
chatActivity,
|
||||
message,
|
||||
threadBinding = binding.threadTitleWrapper,
|
||||
reactionsBinding = binding.reactions,
|
||||
openThread = { openThread(message) }
|
||||
)
|
||||
|
||||
Reaction().showReactions(
|
||||
message,
|
||||
::clickOnReaction,
|
||||
::longClickOnReaction,
|
||||
binding.reactions,
|
||||
binding.messageText.context,
|
||||
false,
|
||||
viewThemeUtils
|
||||
)
|
||||
}
|
||||
|
||||
private fun processCheckboxes(chatMessage: ChatMessage, user: User): Boolean {
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
val message = chatMessage.message ?: return false
|
||||
val checkBoxContainer = binding.checkboxContainer
|
||||
checkBoxContainer.removeAllViews()
|
||||
|
||||
val isEditable = hasSpreedFeatureCapability(
|
||||
user.capabilities?.spreedCapability!!,
|
||||
SpreedFeatures.EDIT_MESSAGES
|
||||
) &&
|
||||
!chatMessage.createdAt.before(Date(System.currentTimeMillis() - AGE_THRESHOLD_FOR_EDIT_MESSAGE))
|
||||
|
||||
val checkboxList = mutableListOf<CheckBox>()
|
||||
val spaceInPx = getBottomPaddingPx()
|
||||
var hasCheckbox = false
|
||||
|
||||
message.lines().forEach { line ->
|
||||
if (addCheckboxOrTextView(
|
||||
line.trimEnd(),
|
||||
chatMessage,
|
||||
user,
|
||||
isEditable,
|
||||
checkBoxContainer,
|
||||
checkboxList,
|
||||
spaceInPx,
|
||||
chatActivity
|
||||
)
|
||||
) {
|
||||
hasCheckbox = true
|
||||
}
|
||||
}
|
||||
return hasCheckbox
|
||||
}
|
||||
|
||||
fun getBottomPaddingPx(): Int =
|
||||
TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
PADDING_FOUR.toFloat(),
|
||||
context.resources.displayMetrics
|
||||
).toInt()
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun addCheckboxOrTextView(
|
||||
line: String,
|
||||
chatMessage: ChatMessage,
|
||||
user: User,
|
||||
isEditable: Boolean,
|
||||
container: ViewGroup,
|
||||
checkboxList: MutableList<CheckBox>,
|
||||
spaceInPx: Int,
|
||||
chatActivity: ChatActivity
|
||||
): Boolean {
|
||||
val match = matchCheckbox(line)
|
||||
return if (match != null) {
|
||||
val isChecked = match.groupValues[CHECKED_GROUP_INDEX].equals("X", true)
|
||||
val taskText = match.groupValues[TASK_TEXT_GROUP_INDEX].trim()
|
||||
val checkBox = addCheckboxLine(
|
||||
context = chatActivity,
|
||||
container = container,
|
||||
chatMessage = chatMessage,
|
||||
taskText = taskText,
|
||||
isChecked = isChecked,
|
||||
isEnabled = (
|
||||
chatMessage.actorType == "bots" ||
|
||||
chatActivity.userAllowedByPrivilages(chatMessage)
|
||||
) &&
|
||||
isEditable,
|
||||
isIncomingMessage = true,
|
||||
messageUtils = messageUtils,
|
||||
viewThemeUtils = viewThemeUtils,
|
||||
linkColorRes = R.color.no_emphasis_text,
|
||||
paddingPx = spaceInPx
|
||||
) { _, _ ->
|
||||
updateCheckboxStates(chatMessage, user, checkboxList)
|
||||
}
|
||||
checkboxList.add(checkBox)
|
||||
true
|
||||
} else if (line.isNotBlank()) {
|
||||
addPlainTextLine(
|
||||
context = container.context,
|
||||
container = container,
|
||||
chatMessage = chatMessage,
|
||||
text = line,
|
||||
isIncomingMessage = true,
|
||||
messageUtils = messageUtils,
|
||||
viewThemeUtils = viewThemeUtils,
|
||||
linkColorRes = R.color.no_emphasis_text,
|
||||
paddingPx = spaceInPx
|
||||
)
|
||||
false
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCheckboxStates(chatMessage: ChatMessage, user: User, checkboxes: List<CheckBox>) {
|
||||
job = CoroutineScope(Dispatchers.Main).launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val apiVersion: Int = ApiUtils.getChatApiVersion(
|
||||
user.capabilities?.spreedCapability!!,
|
||||
intArrayOf(1)
|
||||
)
|
||||
val updatedMessage = updateMessageWithCheckboxStates(chatMessage.message!!, checkboxes)
|
||||
val messageParameters = chatMessage.messageParameters
|
||||
val messageToSend = if (!messageParameters.isNullOrEmpty()) {
|
||||
val parsedMessage = ChatUtils.getParsedMessage(updatedMessage, messageParameters) ?: updatedMessage
|
||||
messageUtils.processEditMessageParameters(
|
||||
messageParameters,
|
||||
chatMessage,
|
||||
parsedMessage
|
||||
).toString()
|
||||
} else {
|
||||
updatedMessage
|
||||
}
|
||||
|
||||
chatRepository.editChatMessage(
|
||||
user.getCredentials(),
|
||||
ApiUtils.getUrlForChatMessage(apiVersion, user.baseUrl!!, chatMessage.token!!, chatMessage.id),
|
||||
messageToSend
|
||||
).collect { result ->
|
||||
withContext(Dispatchers.Main) {
|
||||
if (result.isSuccess) {
|
||||
val editedMessage = result.getOrNull()?.ocs?.data!!.parentMessage!!
|
||||
Log.d(TAG, "EditedMessage: $editedMessage")
|
||||
binding.messageEditIndicator.apply {
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
binding.messageTime.text =
|
||||
dateUtils.getLocalTimeStringFromTimestamp(editedMessage.lastEditTimestamp!!)
|
||||
} else {
|
||||
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun longClickOnReaction(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.onLongClickReactions(chatMessage)
|
||||
}
|
||||
|
||||
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
|
||||
commonMessageInterface.onClickReaction(chatMessage, emoji)
|
||||
}
|
||||
|
||||
private fun openThread(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.openThread(chatMessage)
|
||||
}
|
||||
|
||||
private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) {
|
||||
val actorName = message.actorDisplayName
|
||||
if (!actorName.isNullOrBlank()) {
|
||||
binding.messageAuthor.visibility = View.VISIBLE
|
||||
binding.messageAuthor.text = actorName
|
||||
binding.messageUserAvatar.setOnClickListener {
|
||||
(payload as? MessagePayload)?.profileBottomSheet?.showFor(message, itemView.context)
|
||||
}
|
||||
} else {
|
||||
binding.messageAuthor.setText(R.string.nc_nick_guest)
|
||||
}
|
||||
|
||||
if (!message.isGrouped && !message.isOneToOneConversation && !message.isFormerOneToOneConversation) {
|
||||
ChatMessageUtils().setAvatarOnMessage(binding.messageUserAvatar, message, viewThemeUtils)
|
||||
} else {
|
||||
if (message.isOneToOneConversation || message.isFormerOneToOneConversation) {
|
||||
binding.messageUserAvatar.visibility = View.GONE
|
||||
} else {
|
||||
binding.messageUserAvatar.visibility = View.INVISIBLE
|
||||
}
|
||||
binding.messageAuthor.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun colorizeMessageBubble(message: ChatMessage) {
|
||||
viewThemeUtils.talk.themeIncomingMessageBubble(bubble, message.isGrouped, message.isDeleted)
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught")
|
||||
private fun processParentMessage(message: ChatMessage) {
|
||||
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
|
||||
)
|
||||
|
||||
viewThemeUtils.talk.themeParentMessage(
|
||||
parentChatMessage,
|
||||
message,
|
||||
binding.messageQuote.quotedChatMessageView,
|
||||
R.color.high_emphasis_text
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.setOnClickListener {
|
||||
chatActivity.jumpToQuotedMessage(parentChatMessage)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error when processing parent message in view holder", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
|
||||
this.commonMessageInterface = commonMessageInterface
|
||||
}
|
||||
|
||||
override fun viewDetached() {
|
||||
super.viewDetached()
|
||||
job?.cancel()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TEXT_SIZE_MULTIPLIER = 2.5
|
||||
private val TAG = IncomingTextMessageViewHolder::class.java.simpleName
|
||||
private const val AGE_THRESHOLD_FOR_EDIT_MESSAGE: Long = 86400000
|
||||
private const val CHECKED_GROUP_INDEX = 1
|
||||
private const val TASK_TEXT_GROUP_INDEX = 2
|
||||
private const val PADDING_FOUR = 4
|
||||
}
|
||||
}
|
||||
+390
@@ -0,0 +1,390 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Christian Reiner <foss@christian-reiner.info>
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2021 Tim Krüger <t@timkrueger.me>
|
||||
* SPDX-FileCopyrightText: 2021 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.messages
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.SeekBar
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import autodagger.AutoInjector
|
||||
import coil.load
|
||||
import com.nextcloud.android.common.ui.theme.utils.ColorRole
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication.Companion.sharedApplication
|
||||
import ru.f7cloud.talk.chat.ChatActivity
|
||||
import ru.f7cloud.talk.chat.data.model.ChatMessage
|
||||
import ru.f7cloud.talk.databinding.ItemCustomIncomingVoiceMessageBinding
|
||||
import ru.f7cloud.talk.ui.theme.ViewThemeUtils
|
||||
import ru.f7cloud.talk.utils.ApiUtils
|
||||
import ru.f7cloud.talk.utils.ChatMessageUtils
|
||||
import ru.f7cloud.talk.utils.DateUtils
|
||||
import ru.f7cloud.talk.utils.message.MessageUtils
|
||||
import ru.f7cloud.talk.utils.preferences.AppPreferences
|
||||
import com.stfalcon.chatkit.messages.MessageHolders
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.ExecutionException
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(F7cloudTalkApplication::class)
|
||||
class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) :
|
||||
MessageHolders.IncomingTextMessageViewHolder<ChatMessage>(incomingView, payload) {
|
||||
|
||||
private val binding: ItemCustomIncomingVoiceMessageBinding = ItemCustomIncomingVoiceMessageBinding.bind(itemView)
|
||||
|
||||
@JvmField
|
||||
@Inject
|
||||
var context: Context? = null
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var messageUtils: MessageUtils
|
||||
|
||||
@Inject
|
||||
lateinit var dateUtils: DateUtils
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
lateinit var message: ChatMessage
|
||||
|
||||
lateinit var voiceMessageInterface: VoiceMessageInterface
|
||||
lateinit var commonMessageInterface: CommonMessageInterface
|
||||
private var isBound = false
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
if (isBound) {
|
||||
handleIsPlayingVoiceMessageState(message)
|
||||
return
|
||||
}
|
||||
|
||||
this.message = message
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
|
||||
val filename = message.selectedIndividualHashMap!!["name"]
|
||||
val retrieved = appPreferences.getWaveFormFromFile(filename)
|
||||
if (retrieved.isNotEmpty() &&
|
||||
message.voiceMessageFloatArray == null ||
|
||||
message.voiceMessageFloatArray?.isEmpty() == true
|
||||
) {
|
||||
message.voiceMessageFloatArray = retrieved.toFloatArray()
|
||||
binding.seekbar.setWaveData(message.voiceMessageFloatArray!!)
|
||||
}
|
||||
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
|
||||
setAvatarAndAuthorOnMessageItem(message)
|
||||
|
||||
colorizeMessageBubble(message)
|
||||
|
||||
itemView.isSelected = false
|
||||
|
||||
// parent message handling
|
||||
setParentMessageDataOnMessageItem(message)
|
||||
|
||||
updateDownloadState(message)
|
||||
binding.seekbar.max = MAX
|
||||
viewThemeUtils.talk.themeWaveFormSeekBar(binding.seekbar)
|
||||
viewThemeUtils.platform.colorCircularProgressBar(binding.progressBar, ColorRole.ON_SURFACE_VARIANT)
|
||||
|
||||
showVoiceMessageDuration(message)
|
||||
if (message.isDownloadingVoiceMessage) {
|
||||
showVoiceMessageLoading()
|
||||
} else {
|
||||
if (message.voiceMessageFloatArray == null || message.voiceMessageFloatArray!!.isEmpty()) {
|
||||
binding.seekbar.setWaveData(FloatArray(0))
|
||||
} else {
|
||||
binding.seekbar.setWaveData(message.voiceMessageFloatArray!!)
|
||||
}
|
||||
binding.progressBar.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (message.resetVoiceMessage) {
|
||||
resetVoiceMessage(message)
|
||||
}
|
||||
|
||||
binding.seekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||
if (fromUser) {
|
||||
voiceMessageInterface.updateMediaPlayerProgressBySlider(message, progress)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
(voiceMessageInterface as ChatActivity).chatViewModel.voiceMessagePlayBackUIFlow.onEach { speed ->
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.playbackSpeedControlBtn.setSpeed(speed)
|
||||
}
|
||||
}.collect()
|
||||
}
|
||||
|
||||
binding.playbackSpeedControlBtn.setSpeed(appPreferences.getPreferredPlayback(message.actorId))
|
||||
|
||||
Reaction().showReactions(
|
||||
message,
|
||||
::clickOnReaction,
|
||||
::longClickOnReaction,
|
||||
binding.reactions,
|
||||
binding.messageTime.context,
|
||||
false,
|
||||
viewThemeUtils
|
||||
)
|
||||
|
||||
isBound = true
|
||||
}
|
||||
|
||||
private fun showVoiceMessageDuration(message: ChatMessage) {
|
||||
if (message.voiceMessageDuration > 0) {
|
||||
binding.voiceMessageDuration.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.voiceMessageDuration.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetVoiceMessage(chatMessage: ChatMessage) {
|
||||
binding.playPauseBtn.visibility = View.VISIBLE
|
||||
binding.playPauseBtn.icon = ContextCompat.getDrawable(
|
||||
context!!,
|
||||
R.drawable.ic_baseline_play_arrow_voice_message_24
|
||||
)
|
||||
binding.seekbar.progress = SEEKBAR_START
|
||||
chatMessage.resetVoiceMessage = false
|
||||
chatMessage.voiceMessagePlayedSeconds = 0
|
||||
showVoiceMessageDuration(message)
|
||||
}
|
||||
|
||||
private fun longClickOnReaction(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.onLongClickReactions(chatMessage)
|
||||
}
|
||||
|
||||
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
|
||||
commonMessageInterface.onClickReaction(chatMessage, emoji)
|
||||
}
|
||||
|
||||
private fun handleIsPlayingVoiceMessageState(message: ChatMessage) {
|
||||
colorizeMessageBubble(message)
|
||||
if (message.isPlayingVoiceMessage) {
|
||||
showPlayButton()
|
||||
binding.playPauseBtn.icon = ContextCompat.getDrawable(
|
||||
context!!,
|
||||
R.drawable.ic_baseline_pause_voice_message_24
|
||||
)
|
||||
|
||||
val d = message.voiceMessageDuration.toLong()
|
||||
val t = message.voiceMessagePlayedSeconds.toLong()
|
||||
binding.voiceMessageDuration.text = android.text.format.DateUtils.formatElapsedTime(d - t)
|
||||
binding.voiceMessageDuration.visibility = View.VISIBLE
|
||||
binding.seekbar.progress = message.voiceMessageSeekbarProgress
|
||||
} else {
|
||||
showVoiceMessageDuration(message)
|
||||
binding.playPauseBtn.visibility = View.VISIBLE
|
||||
binding.playPauseBtn.icon = ContextCompat.getDrawable(
|
||||
context!!,
|
||||
R.drawable.ic_baseline_play_arrow_voice_message_24
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDownloadState(message: ChatMessage) {
|
||||
// check if download worker is already running
|
||||
val fileId = message.selectedIndividualHashMap!!["id"]
|
||||
val workers = WorkManager.getInstance(context!!).getWorkInfosByTag(fileId!!)
|
||||
|
||||
try {
|
||||
for (workInfo in workers.get()) {
|
||||
if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) {
|
||||
showVoiceMessageLoading()
|
||||
WorkManager.getInstance(context!!).getWorkInfoByIdLiveData(workInfo.id)
|
||||
.observeForever { info: WorkInfo? ->
|
||||
showStatus(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: ExecutionException) {
|
||||
Log.e(TAG, "Error when checking if worker already exists", e)
|
||||
} catch (e: InterruptedException) {
|
||||
Log.e(TAG, "Error when checking if worker already exists", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showStatus(info: WorkInfo?) {
|
||||
if (info != null) {
|
||||
when (info.state) {
|
||||
WorkInfo.State.RUNNING -> {
|
||||
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 -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showPlayButton() {
|
||||
binding.playPauseBtn.visibility = View.VISIBLE
|
||||
binding.progressBar.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun showVoiceMessageLoading() {
|
||||
binding.playPauseBtn.visibility = View.GONE
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) {
|
||||
val actorName = message.actorDisplayName
|
||||
if (!actorName.isNullOrBlank()) {
|
||||
binding.messageAuthor.visibility = View.VISIBLE
|
||||
binding.messageAuthor.text = actorName
|
||||
binding.messageUserAvatar.setOnClickListener {
|
||||
(payload as? MessagePayload)?.profileBottomSheet?.showFor(message, itemView.context)
|
||||
}
|
||||
} else {
|
||||
binding.messageAuthor.setText(R.string.nc_nick_guest)
|
||||
}
|
||||
|
||||
if (!message.isGrouped && !message.isOneToOneConversation && !message.isFormerOneToOneConversation) {
|
||||
ChatMessageUtils().setAvatarOnMessage(binding.messageUserAvatar, message, viewThemeUtils)
|
||||
} else {
|
||||
if (message.isOneToOneConversation || message.isFormerOneToOneConversation) {
|
||||
binding.messageUserAvatar.visibility = View.GONE
|
||||
} else {
|
||||
binding.messageUserAvatar.visibility = View.INVISIBLE
|
||||
}
|
||||
binding.messageAuthor.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun colorizeMessageBubble(message: ChatMessage) {
|
||||
viewThemeUtils.talk.themeIncomingMessageBubble(
|
||||
bubble,
|
||||
message.isGrouped,
|
||||
message.isDeleted,
|
||||
message.wasPlayedVoiceMessage
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
|
||||
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
|
||||
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))
|
||||
|
||||
viewThemeUtils.talk.themeParentMessage(
|
||||
parentChatMessage,
|
||||
message,
|
||||
binding.messageQuote.quotedChatMessageView
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.visibility =
|
||||
if (!message.isDeleted &&
|
||||
message.parentMessageId != null &&
|
||||
message.parentMessageId != chatActivity.conversationThreadId
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error when processing parent message in view holder", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
fun assignVoiceMessageInterface(voiceMessageInterface: VoiceMessageInterface) {
|
||||
this.voiceMessageInterface = voiceMessageInterface
|
||||
}
|
||||
|
||||
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
|
||||
this.commonMessageInterface = commonMessageInterface
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "VoiceInMessageView"
|
||||
private const val SEEKBAR_START: Int = 0
|
||||
private const val MAX: Int = 100
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.messages
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import coil.load
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.api.NcApi
|
||||
import ru.f7cloud.talk.chat.data.model.ChatMessage
|
||||
import ru.f7cloud.talk.databinding.ReferenceInsideMessageBinding
|
||||
import ru.f7cloud.talk.models.json.opengraph.OpenGraphOverall
|
||||
import ru.f7cloud.talk.utils.ApiUtils
|
||||
import io.reactivex.Observer
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
|
||||
class LinkPreview {
|
||||
|
||||
fun showLink(message: ChatMessage, ncApi: NcApi, binding: ReferenceInsideMessageBinding, context: Context) {
|
||||
binding.referenceName.text = ""
|
||||
binding.referenceDescription.text = ""
|
||||
binding.referenceLink.text = ""
|
||||
binding.referenceThumbImage.setImageDrawable(null)
|
||||
|
||||
if (!message.extractedUrlToPreview.isNullOrEmpty()) {
|
||||
val credentials: String = ApiUtils.getCredentials(message.activeUser?.username, message.activeUser?.token)!!
|
||||
val openGraphLink = ApiUtils.getUrlForOpenGraph(message.activeUser?.baseUrl!!)
|
||||
ncApi.getOpenGraph(
|
||||
credentials,
|
||||
openGraphLink,
|
||||
message.extractedUrlToPreview
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(object : Observer<OpenGraphOverall> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onNext(openGraphOverall: OpenGraphOverall) {
|
||||
val reference = openGraphOverall.ocs?.data?.references?.entries?.iterator()?.next()?.value
|
||||
|
||||
if (reference != null) {
|
||||
val referenceName = reference.openGraphObject?.name
|
||||
if (!referenceName.isNullOrEmpty()) {
|
||||
binding.referenceName.visibility = View.VISIBLE
|
||||
binding.referenceName.text = referenceName
|
||||
} else {
|
||||
binding.referenceName.visibility = View.GONE
|
||||
}
|
||||
|
||||
val referenceDescription = reference.openGraphObject?.description
|
||||
if (!referenceDescription.isNullOrEmpty()) {
|
||||
binding.referenceDescription.visibility = View.VISIBLE
|
||||
binding.referenceDescription.text = referenceDescription
|
||||
} else {
|
||||
binding.referenceDescription.visibility = View.GONE
|
||||
}
|
||||
|
||||
val referenceLink = reference.openGraphObject?.link
|
||||
if (!referenceLink.isNullOrEmpty()) {
|
||||
binding.referenceLink.visibility = View.VISIBLE
|
||||
binding.referenceLink.text = referenceLink.replace(HTTPS_PROTOCOL, "")
|
||||
} else {
|
||||
binding.referenceLink.visibility = View.GONE
|
||||
}
|
||||
|
||||
val referenceThumbUrl = reference.openGraphObject?.thumb
|
||||
var backgroundId = R.drawable.link_text_background
|
||||
if (!referenceThumbUrl.isNullOrEmpty()) {
|
||||
binding.referenceThumbImage.visibility = View.VISIBLE
|
||||
binding.referenceThumbImage.load(referenceThumbUrl)
|
||||
} else {
|
||||
backgroundId = R.drawable.link_text_no_preview_background
|
||||
binding.referenceThumbImage.visibility = View.GONE
|
||||
}
|
||||
binding.referenceMetadataContainer.background = ContextCompat.getDrawable(
|
||||
binding.referenceMetadataContainer.context,
|
||||
backgroundId
|
||||
)
|
||||
|
||||
binding.referenceWrapper.setOnClickListener {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, referenceLink!!.toUri())
|
||||
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(browserIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
Log.e(TAG, "failed to get openGraph data", e)
|
||||
binding.referenceName.visibility = View.GONE
|
||||
binding.referenceDescription.visibility = View.GONE
|
||||
binding.referenceLink.visibility = View.GONE
|
||||
binding.referenceThumbImage.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
// unused atm
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = LinkPreview::class.java.simpleName
|
||||
private const val HTTPS_PROTOCOL = "https://"
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.messages
|
||||
|
||||
import ru.f7cloud.talk.ui.bottom.sheet.ProfileBottomSheet
|
||||
|
||||
data class MessagePayload(
|
||||
var roomToken: String,
|
||||
val isOwnerOrModerator: Boolean?,
|
||||
val profileBottomSheet: ProfileBottomSheet
|
||||
)
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Christian Reiner <foss@christian-reiner.info>
|
||||
* SPDX-FileCopyrightText: 2024 Sowjanya Kota<sowjanya.kch@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package ru.f7cloud.talk.adapters.messages
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import autodagger.AutoInjector
|
||||
import coil.load
|
||||
import com.nextcloud.android.common.ui.theme.utils.ColorRole
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.api.NcApi
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication.Companion.sharedApplication
|
||||
import ru.f7cloud.talk.chat.ChatActivity
|
||||
import ru.f7cloud.talk.chat.data.model.ChatMessage
|
||||
import ru.f7cloud.talk.databinding.ItemCustomOutcomingDeckCardMessageBinding
|
||||
import ru.f7cloud.talk.models.json.chat.ReadStatus
|
||||
import ru.f7cloud.talk.ui.theme.ViewThemeUtils
|
||||
import ru.f7cloud.talk.utils.ApiUtils
|
||||
import ru.f7cloud.talk.utils.DateUtils
|
||||
import ru.f7cloud.talk.utils.message.MessageUtils
|
||||
import ru.f7cloud.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(F7cloudTalkApplication::class)
|
||||
class OutcomingDeckCardViewHolder(outcomingView: View) :
|
||||
MessageHolders.OutcomingTextMessageViewHolder<ChatMessage>(outcomingView),
|
||||
AdjustableMessageHolderInterface {
|
||||
|
||||
override val binding: ItemCustomOutcomingDeckCardMessageBinding = ItemCustomOutcomingDeckCardMessageBinding.bind(
|
||||
itemView
|
||||
)
|
||||
|
||||
@Inject
|
||||
lateinit var context: Context
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var messageUtils: MessageUtils
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
@Inject
|
||||
lateinit var ncApi: NcApi
|
||||
|
||||
lateinit var message: ChatMessage
|
||||
|
||||
@Inject
|
||||
lateinit var dateUtils: DateUtils
|
||||
|
||||
lateinit var commonMessageInterface: CommonMessageInterface
|
||||
|
||||
var stackName: String? = null
|
||||
var cardName: String? = null
|
||||
var boardName: String? = null
|
||||
var cardLink: String? = null
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
this.message = message
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT)
|
||||
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
|
||||
colorizeMessageBubble(message)
|
||||
|
||||
binding.cardView.findViewById<ImageView>(R.id.deckCardImage)?.let {
|
||||
viewThemeUtils.platform.colorImageView(it, ColorRole.SECONDARY)
|
||||
}
|
||||
|
||||
itemView.isSelected = false
|
||||
|
||||
showDeckCard(message)
|
||||
|
||||
// parent message handling
|
||||
setParentMessageDataOnMessageItem(message)
|
||||
|
||||
val readStatusDrawableInt = when (message.readStatus) {
|
||||
ReadStatus.READ -> R.drawable.ic_check_all
|
||||
ReadStatus.SENT -> R.drawable.ic_check
|
||||
else -> null
|
||||
}
|
||||
|
||||
val readStatusContentDescriptionString = when (message.readStatus) {
|
||||
ReadStatus.READ -> context.resources?.getString(R.string.nc_message_read)
|
||||
ReadStatus.SENT -> context.resources?.getString(R.string.nc_message_sent)
|
||||
else -> null
|
||||
}
|
||||
|
||||
readStatusDrawableInt?.let { drawableInt ->
|
||||
AppCompatResources.getDrawable(context, drawableInt)?.let {
|
||||
binding.checkMark.setImageDrawable(it)
|
||||
viewThemeUtils.talk.themeMessageCheckMark(binding.checkMark)
|
||||
}
|
||||
}
|
||||
|
||||
binding.checkMark.contentDescription = readStatusContentDescriptionString
|
||||
|
||||
binding.cardView.setOnClickListener {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, cardLink!!.toUri())
|
||||
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(browserIntent)
|
||||
}
|
||||
|
||||
itemView.setTag(R.string.replyable_message_view_tag, message.replyable)
|
||||
|
||||
Reaction().showReactions(
|
||||
message,
|
||||
::clickOnReaction,
|
||||
::longClickOnReaction,
|
||||
binding.reactions,
|
||||
binding.messageTime.context,
|
||||
true,
|
||||
viewThemeUtils
|
||||
)
|
||||
}
|
||||
|
||||
private fun showDeckCard(message: ChatMessage) {
|
||||
if (message.messageParameters != null && message.messageParameters!!.size > 0) {
|
||||
for (key in message.messageParameters!!.keys) {
|
||||
val individualHashMap: Map<String?, String?> = message.messageParameters!![key]!!
|
||||
if (individualHashMap["type"] == "deck-card") {
|
||||
cardName = individualHashMap["name"]
|
||||
stackName = individualHashMap["stackname"]
|
||||
boardName = individualHashMap["boardname"]
|
||||
cardLink = individualHashMap["link"]
|
||||
}
|
||||
}
|
||||
}
|
||||
val cardDescription = String.format(
|
||||
context.resources.getString(R.string.deck_card_description),
|
||||
stackName,
|
||||
boardName
|
||||
)
|
||||
|
||||
binding.cardName.visibility = View.VISIBLE
|
||||
binding.cardDescription.visibility = View.VISIBLE
|
||||
binding.cardName.text = cardName
|
||||
binding.cardDescription.text = cardDescription
|
||||
}
|
||||
|
||||
private fun longClickOnReaction(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.onLongClickReactions(chatMessage)
|
||||
}
|
||||
|
||||
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
|
||||
commonMessageInterface.onClickReaction(chatMessage, emoji)
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
|
||||
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
|
||||
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))
|
||||
|
||||
viewThemeUtils.talk.themeParentMessage(
|
||||
parentChatMessage,
|
||||
message,
|
||||
binding.messageQuote.quotedChatMessageView
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.visibility =
|
||||
if (!message.isDeleted &&
|
||||
message.parentMessageId != null &&
|
||||
message.parentMessageId != chatActivity.conversationThreadId
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error when processing parent message in view holder", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun colorizeMessageBubble(message: ChatMessage) {
|
||||
viewThemeUtils.talk.themeOutgoingMessageBubble(bubble, message.isGrouped, message.isDeleted)
|
||||
}
|
||||
|
||||
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
|
||||
this.commonMessageInterface = commonMessageInterface
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = OutcomingDeckCardViewHolder::class.java.simpleName
|
||||
}
|
||||
}
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Christian Reiner <foss@christian-reiner.info>
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2017-2019 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.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 autodagger.AutoInjector
|
||||
import coil.load
|
||||
import com.nextcloud.android.common.ui.theme.utils.ColorRole
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.api.NcApi
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication.Companion.sharedApplication
|
||||
import ru.f7cloud.talk.chat.ChatActivity
|
||||
import ru.f7cloud.talk.chat.data.model.ChatMessage
|
||||
import ru.f7cloud.talk.databinding.ItemCustomOutcomingLinkPreviewMessageBinding
|
||||
import ru.f7cloud.talk.models.json.chat.ReadStatus
|
||||
import ru.f7cloud.talk.ui.theme.ViewThemeUtils
|
||||
import ru.f7cloud.talk.utils.ApiUtils
|
||||
import ru.f7cloud.talk.utils.DateUtils
|
||||
import ru.f7cloud.talk.utils.message.MessageUtils
|
||||
import ru.f7cloud.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(F7cloudTalkApplication::class)
|
||||
class OutcomingLinkPreviewMessageViewHolder(outcomingView: View, payload: Any) :
|
||||
MessageHolders.OutcomingTextMessageViewHolder<ChatMessage>(outcomingView, payload),
|
||||
AdjustableMessageHolderInterface {
|
||||
|
||||
override val binding: ItemCustomOutcomingLinkPreviewMessageBinding =
|
||||
ItemCustomOutcomingLinkPreviewMessageBinding.bind(itemView)
|
||||
|
||||
@Inject
|
||||
lateinit var context: Context
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var messageUtils: MessageUtils
|
||||
|
||||
@Inject
|
||||
lateinit var dateUtils: DateUtils
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
@Inject
|
||||
lateinit var ncApi: NcApi
|
||||
|
||||
lateinit var message: ChatMessage
|
||||
|
||||
lateinit var commonMessageInterface: CommonMessageInterface
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
this.message = message
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT)
|
||||
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
|
||||
colorizeMessageBubble(message)
|
||||
var processedMessageText =
|
||||
messageUtils.enrichChatMessageText(binding.messageText.context, message, false, viewThemeUtils)
|
||||
processedMessageText = messageUtils.processMessageParameters(
|
||||
binding.messageText.context,
|
||||
viewThemeUtils,
|
||||
processedMessageText!!,
|
||||
message,
|
||||
itemView
|
||||
)
|
||||
|
||||
binding.messageText.text = processedMessageText
|
||||
|
||||
itemView.isSelected = false
|
||||
|
||||
// parent message handling
|
||||
setParentMessageDataOnMessageItem(message)
|
||||
|
||||
val readStatusDrawableInt = when (message.readStatus) {
|
||||
ReadStatus.READ -> R.drawable.ic_check_all
|
||||
ReadStatus.SENT -> R.drawable.ic_check
|
||||
else -> null
|
||||
}
|
||||
|
||||
val readStatusContentDescriptionString = when (message.readStatus) {
|
||||
ReadStatus.READ -> context.resources?.getString(R.string.nc_message_read)
|
||||
ReadStatus.SENT -> context.resources?.getString(R.string.nc_message_sent)
|
||||
else -> null
|
||||
}
|
||||
|
||||
readStatusDrawableInt?.let { drawableInt ->
|
||||
AppCompatResources.getDrawable(context, drawableInt)?.let {
|
||||
binding.checkMark.setImageDrawable(it)
|
||||
viewThemeUtils.talk.themeMessageCheckMark(binding.checkMark)
|
||||
}
|
||||
}
|
||||
|
||||
binding.checkMark.contentDescription = readStatusContentDescriptionString
|
||||
|
||||
LinkPreview().showLink(
|
||||
message,
|
||||
ncApi,
|
||||
binding.referenceInclude,
|
||||
itemView.context
|
||||
)
|
||||
binding.referenceInclude.referenceWrapper.setOnLongClickListener { l: View? ->
|
||||
commonMessageInterface.onOpenMessageActionsDialog(message)
|
||||
true
|
||||
}
|
||||
|
||||
itemView.setTag(R.string.replyable_message_view_tag, message.replyable)
|
||||
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
val showThreadButton = chatActivity.conversationThreadId == null && message.isThread
|
||||
if (showThreadButton) {
|
||||
binding.reactions.threadButton.visibility = View.VISIBLE
|
||||
binding.reactions.threadButton.setContent {
|
||||
ThreadButtonComposable(
|
||||
onButtonClick = { openThread(message) }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
binding.reactions.threadButton.visibility = View.GONE
|
||||
}
|
||||
|
||||
Reaction().showReactions(
|
||||
message,
|
||||
::clickOnReaction,
|
||||
::longClickOnReaction,
|
||||
binding.reactions,
|
||||
binding.messageTime.context,
|
||||
true,
|
||||
viewThemeUtils
|
||||
)
|
||||
}
|
||||
|
||||
private fun longClickOnReaction(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.onLongClickReactions(chatMessage)
|
||||
}
|
||||
|
||||
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
|
||||
commonMessageInterface.onClickReaction(chatMessage, emoji)
|
||||
}
|
||||
|
||||
private fun openThread(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.openThread(chatMessage)
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
|
||||
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
|
||||
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.themeParentMessage(
|
||||
parentChatMessage,
|
||||
message,
|
||||
binding.messageQuote.quotedChatMessageView
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.visibility =
|
||||
if (!message.isDeleted &&
|
||||
message.parentMessageId != null &&
|
||||
message.parentMessageId != chatActivity.conversationThreadId
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error when processing parent message in view holder", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun colorizeMessageBubble(message: ChatMessage) {
|
||||
viewThemeUtils.talk.themeOutgoingMessageBubble(bubble, message.isGrouped, message.isDeleted)
|
||||
}
|
||||
|
||||
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
|
||||
this.commonMessageInterface = commonMessageInterface
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = OutcomingLinkPreviewMessageViewHolder::class.java.simpleName
|
||||
}
|
||||
}
|
||||
+292
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Christian Reiner <foss@christian-reiner.info>
|
||||
* SPDX-FileCopyrightText: 2021 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.messages
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.net.toUri
|
||||
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 ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication.Companion.sharedApplication
|
||||
import ru.f7cloud.talk.chat.ChatActivity
|
||||
import ru.f7cloud.talk.chat.data.model.ChatMessage
|
||||
import ru.f7cloud.talk.databinding.ItemCustomOutcomingLocationMessageBinding
|
||||
import ru.f7cloud.talk.models.json.chat.ReadStatus
|
||||
import ru.f7cloud.talk.ui.theme.ViewThemeUtils
|
||||
import ru.f7cloud.talk.utils.ApiUtils
|
||||
import ru.f7cloud.talk.utils.DateUtils
|
||||
import ru.f7cloud.talk.utils.UriUtils
|
||||
import ru.f7cloud.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
|
||||
|
||||
@AutoInjector(F7cloudTalkApplication::class)
|
||||
class OutcomingLocationMessageViewHolder(incomingView: View) :
|
||||
MessageHolders.OutcomingTextMessageViewHolder<ChatMessage>(incomingView),
|
||||
AdjustableMessageHolderInterface {
|
||||
|
||||
override val binding: ItemCustomOutcomingLocationMessageBinding =
|
||||
ItemCustomOutcomingLocationMessageBinding.bind(itemView)
|
||||
private val realView: View = itemView
|
||||
|
||||
var locationLon: String? = ""
|
||||
var locationLat: String? = ""
|
||||
var locationName: String? = ""
|
||||
var locationGeoLink: String? = ""
|
||||
|
||||
@Inject
|
||||
lateinit var context: Context
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var messageUtils: MessageUtils
|
||||
|
||||
@Inject
|
||||
lateinit var dateUtils: DateUtils
|
||||
|
||||
lateinit var commonMessageInterface: CommonMessageInterface
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT)
|
||||
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
|
||||
realView.isSelected = false
|
||||
val layoutParams = binding.messageTime.layoutParams as FlexboxLayout.LayoutParams
|
||||
layoutParams.isWrapBefore = false
|
||||
|
||||
val textSize = context.resources.getDimension(R.dimen.chat_text_size)
|
||||
|
||||
colorizeMessageBubble(message)
|
||||
binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
||||
binding.messageTime.layoutParams = layoutParams
|
||||
|
||||
binding.messageText.text = message.text
|
||||
|
||||
// parent message handling
|
||||
setParentMessageDataOnMessageItem(message)
|
||||
|
||||
val readStatusDrawableInt = when (message.readStatus) {
|
||||
ReadStatus.READ -> R.drawable.ic_check_all
|
||||
ReadStatus.SENT -> R.drawable.ic_check
|
||||
else -> null
|
||||
}
|
||||
|
||||
val readStatusContentDescriptionString = when (message.readStatus) {
|
||||
ReadStatus.READ -> context.resources?.getString(R.string.nc_message_read)
|
||||
ReadStatus.SENT -> context.resources?.getString(R.string.nc_message_sent)
|
||||
else -> null
|
||||
}
|
||||
|
||||
readStatusDrawableInt?.let { drawableInt ->
|
||||
AppCompatResources.getDrawable(context, drawableInt)?.let {
|
||||
binding.checkMark.setImageDrawable(it)
|
||||
viewThemeUtils.talk.themeMessageCheckMark(binding.checkMark)
|
||||
}
|
||||
}
|
||||
|
||||
binding.checkMark.contentDescription = readStatusContentDescriptionString
|
||||
|
||||
// geo-location
|
||||
setLocationDataOnMessageItem(message)
|
||||
|
||||
Reaction().showReactions(
|
||||
message,
|
||||
::clickOnReaction,
|
||||
::longClickOnReaction,
|
||||
binding.reactions,
|
||||
binding.messageText.context,
|
||||
true,
|
||||
viewThemeUtils
|
||||
)
|
||||
}
|
||||
|
||||
private fun longClickOnReaction(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.onLongClickReactions(chatMessage)
|
||||
}
|
||||
|
||||
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
|
||||
commonMessageInterface.onClickReaction(chatMessage, emoji)
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled", "ClickableViewAccessibility")
|
||||
private fun setLocationDataOnMessageItem(message: ChatMessage) {
|
||||
if (message.messageParameters != null && message.messageParameters!!.size > 0) {
|
||||
for (key in message.messageParameters!!.keys) {
|
||||
val individualHashMap: Map<String?, String?> = message.messageParameters!![key]!!
|
||||
if (individualHashMap["type"] == "geo-location") {
|
||||
locationLon = individualHashMap["longitude"]
|
||||
locationLat = individualHashMap["latitude"]
|
||||
locationName = individualHashMap["name"]
|
||||
locationGeoLink = individualHashMap["id"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.webview.settings.javaScriptEnabled = true
|
||||
|
||||
binding.webview.webViewClient = object : WebViewClient() {
|
||||
@Deprecated("Use shouldOverrideUrlLoading(WebView view, WebResourceRequest request)")
|
||||
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean =
|
||||
if (url != null && UriUtils.hasHttpProtocolPrefixed(url)) {
|
||||
view?.context?.startActivity(Intent(Intent.ACTION_VIEW, url.toUri()))
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
val urlStringBuffer = StringBuffer("file:///android_asset/leafletMapMessagePreview.html")
|
||||
urlStringBuffer.append(
|
||||
"?mapProviderUrl=" + URLEncoder.encode(context.getString(R.string.osm_tile_server_url))
|
||||
)
|
||||
urlStringBuffer.append(
|
||||
"&mapProviderAttribution=" + URLEncoder.encode(
|
||||
context.getString(
|
||||
R.string
|
||||
.osm_tile_server_attributation
|
||||
)
|
||||
)
|
||||
)
|
||||
urlStringBuffer.append("&locationLat=" + URLEncoder.encode(locationLat))
|
||||
urlStringBuffer.append("&locationLon=" + URLEncoder.encode(locationLon))
|
||||
urlStringBuffer.append("&locationName=" + URLEncoder.encode(locationName))
|
||||
urlStringBuffer.append("&locationGeoLink=" + URLEncoder.encode(locationGeoLink))
|
||||
|
||||
binding.webview.loadUrl(urlStringBuffer.toString())
|
||||
|
||||
binding.webview.setOnTouchListener(object : View.OnTouchListener {
|
||||
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
|
||||
when (event?.action) {
|
||||
MotionEvent.ACTION_UP -> openGeoLink()
|
||||
}
|
||||
|
||||
return v?.onTouchEvent(event) ?: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
|
||||
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
|
||||
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.themeParentMessage(
|
||||
parentChatMessage,
|
||||
message,
|
||||
binding.messageQuote.quotedChatMessageView
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.visibility =
|
||||
if (!message.isDeleted &&
|
||||
message.parentMessageId != null &&
|
||||
message.parentMessageId != chatActivity.conversationThreadId
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error when processing parent message in view holder", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun colorizeMessageBubble(message: ChatMessage) {
|
||||
viewThemeUtils.talk.themeOutgoingMessageBubble(bubble, message.isGrouped, message.isDeleted)
|
||||
}
|
||||
|
||||
private fun openGeoLink() {
|
||||
if (!locationGeoLink.isNullOrEmpty()) {
|
||||
val geoLinkWithMarker = addMarkerToGeoLink(locationGeoLink!!)
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, geoLinkWithMarker.toUri())
|
||||
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(browserIntent)
|
||||
} else {
|
||||
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
|
||||
Log.e(TAG, "locationGeoLink was null or empty")
|
||||
}
|
||||
}
|
||||
|
||||
private fun addMarkerToGeoLink(locationGeoLink: String): String = locationGeoLink.replace("geo:", "geo:0,0?q=")
|
||||
|
||||
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
|
||||
this.commonMessageInterface = commonMessageInterface
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "LocOutMessageView"
|
||||
private const val HALF_ALPHA_INT: Int = 255 / 2
|
||||
private val ALPHA_60_INT: Int = (255 * 0.6).roundToInt()
|
||||
}
|
||||
}
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.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 autodagger.AutoInjector
|
||||
import coil.load
|
||||
import com.nextcloud.android.common.ui.theme.utils.ColorRole
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.api.NcApi
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication.Companion.sharedApplication
|
||||
import ru.f7cloud.talk.chat.ChatActivity
|
||||
import ru.f7cloud.talk.chat.data.model.ChatMessage
|
||||
import ru.f7cloud.talk.databinding.ItemCustomOutcomingPollMessageBinding
|
||||
import ru.f7cloud.talk.models.json.chat.ReadStatus
|
||||
import ru.f7cloud.talk.polls.ui.PollMainDialogFragment
|
||||
import ru.f7cloud.talk.ui.theme.ViewThemeUtils
|
||||
import ru.f7cloud.talk.utils.ApiUtils
|
||||
import ru.f7cloud.talk.utils.DateUtils
|
||||
import ru.f7cloud.talk.utils.message.MessageUtils
|
||||
import ru.f7cloud.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(F7cloudTalkApplication::class)
|
||||
class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) :
|
||||
MessageHolders.OutcomingTextMessageViewHolder<ChatMessage>(outcomingView, payload),
|
||||
AdjustableMessageHolderInterface {
|
||||
|
||||
override val binding: ItemCustomOutcomingPollMessageBinding = ItemCustomOutcomingPollMessageBinding.bind(itemView)
|
||||
|
||||
@Inject
|
||||
lateinit var context: Context
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var messageUtils: MessageUtils
|
||||
|
||||
@Inject
|
||||
lateinit var dateUtils: DateUtils
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
@Inject
|
||||
lateinit var ncApi: NcApi
|
||||
|
||||
lateinit var message: ChatMessage
|
||||
|
||||
lateinit var commonMessageInterface: CommonMessageInterface
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
this.message = message
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT)
|
||||
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
|
||||
colorizeMessageBubble(message)
|
||||
|
||||
itemView.isSelected = false
|
||||
|
||||
// parent message handling
|
||||
setParentMessageDataOnMessageItem(message)
|
||||
|
||||
val readStatusDrawableInt = when (message.readStatus) {
|
||||
ReadStatus.READ -> R.drawable.ic_check_all
|
||||
ReadStatus.SENT -> R.drawable.ic_check
|
||||
else -> null
|
||||
}
|
||||
|
||||
val readStatusContentDescriptionString = when (message.readStatus) {
|
||||
ReadStatus.READ -> context.resources?.getString(R.string.nc_message_read)
|
||||
ReadStatus.SENT -> context.resources?.getString(R.string.nc_message_sent)
|
||||
else -> null
|
||||
}
|
||||
|
||||
readStatusDrawableInt?.let { drawableInt ->
|
||||
AppCompatResources.getDrawable(context, drawableInt)?.let {
|
||||
binding.checkMark.setImageDrawable(it)
|
||||
viewThemeUtils.talk.themeMessageCheckMark(binding.checkMark)
|
||||
}
|
||||
}
|
||||
|
||||
binding.checkMark.contentDescription = readStatusContentDescriptionString
|
||||
|
||||
setPollPreview(message)
|
||||
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
Thread().showThreadPreview(
|
||||
chatActivity,
|
||||
message,
|
||||
threadBinding = binding.threadTitleWrapper,
|
||||
reactionsBinding = binding.reactions,
|
||||
openThread = { openThread(message) }
|
||||
)
|
||||
|
||||
Reaction().showReactions(
|
||||
message,
|
||||
::clickOnReaction,
|
||||
::longClickOnReaction,
|
||||
binding.reactions,
|
||||
binding.messageTime.context,
|
||||
true,
|
||||
viewThemeUtils
|
||||
)
|
||||
}
|
||||
|
||||
private fun longClickOnReaction(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.onLongClickReactions(chatMessage)
|
||||
}
|
||||
|
||||
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
|
||||
commonMessageInterface.onClickReaction(chatMessage, emoji)
|
||||
}
|
||||
|
||||
private fun openThread(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.openThread(chatMessage)
|
||||
}
|
||||
|
||||
private fun setPollPreview(message: ChatMessage) {
|
||||
var pollId: String? = null
|
||||
var pollName: String? = null
|
||||
|
||||
if (message.messageParameters != null && message.messageParameters!!.size > 0) {
|
||||
for (key in message.messageParameters!!.keys) {
|
||||
val individualHashMap: Map<String?, String?> = message.messageParameters!![key]!!
|
||||
if (individualHashMap["type"] == "talk-poll") {
|
||||
pollId = individualHashMap["id"]
|
||||
pollName = individualHashMap["name"].toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pollId != null && pollName != null) {
|
||||
binding.messagePollTitle.text = pollName
|
||||
|
||||
val roomToken = (payload as? MessagePayload)!!.roomToken
|
||||
val isOwnerOrModerator = (payload as? MessagePayload)!!.isOwnerOrModerator ?: false
|
||||
|
||||
binding.bubble.setOnClickListener {
|
||||
val pollVoteDialog = PollMainDialogFragment.newInstance(
|
||||
message.activeUser!!,
|
||||
roomToken,
|
||||
isOwnerOrModerator,
|
||||
pollId,
|
||||
pollName
|
||||
)
|
||||
pollVoteDialog.show(
|
||||
(binding.messagePollIcon.context as ChatActivity).supportFragmentManager,
|
||||
TAG
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
|
||||
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
|
||||
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.themeParentMessage(
|
||||
parentChatMessage,
|
||||
message,
|
||||
binding.messageQuote.quotedChatMessageView
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.visibility =
|
||||
if (!message.isDeleted &&
|
||||
message.parentMessageId != null &&
|
||||
message.parentMessageId != chatActivity.conversationThreadId
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error when processing parent message in view holder", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun colorizeMessageBubble(message: ChatMessage) {
|
||||
viewThemeUtils.talk.themeOutgoingMessageBubble(bubble, message.isGrouped, message.isDeleted)
|
||||
}
|
||||
|
||||
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
|
||||
this.commonMessageInterface = commonMessageInterface
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = OutcomingPollMessageViewHolder::class.java.simpleName
|
||||
}
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2021 Tim Krüger <t@timkrueger.me>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.messages;
|
||||
|
||||
import android.text.Spanned;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.google.android.material.card.MaterialCardView;
|
||||
import ru.f7cloud.talk.R;
|
||||
import ru.f7cloud.talk.databinding.ItemCustomOutcomingPreviewMessageBinding;
|
||||
import ru.f7cloud.talk.databinding.ItemThreadTitleBinding;
|
||||
import ru.f7cloud.talk.databinding.ReactionsInsideMessageBinding;
|
||||
import ru.f7cloud.talk.chat.data.model.ChatMessage;
|
||||
import ru.f7cloud.talk.models.json.chat.ReadStatus;
|
||||
import ru.f7cloud.talk.utils.TextMatchers;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.emoji2.widget.EmojiTextView;
|
||||
|
||||
public class OutcomingPreviewMessageViewHolder extends PreviewMessageViewHolder {
|
||||
|
||||
private final ItemCustomOutcomingPreviewMessageBinding binding;
|
||||
|
||||
public OutcomingPreviewMessageViewHolder(View itemView) {
|
||||
super(itemView, null);
|
||||
binding = ItemCustomOutcomingPreviewMessageBinding.bind(itemView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(@NonNull ChatMessage message) {
|
||||
super.onBind(message);
|
||||
if(!message.isVoiceMessage()
|
||||
&& !Objects.equals(message.getMessage(), "{file}")
|
||||
) {
|
||||
Spanned processedMessageText = null;
|
||||
binding.bubble.setBackgroundResource(R.drawable.shape_grouped_outcoming_message);
|
||||
if (viewThemeUtils != null) {
|
||||
processedMessageText = messageUtils.enrichChatMessageText(
|
||||
binding.messageCaption.getContext(),
|
||||
message,
|
||||
false,
|
||||
viewThemeUtils);
|
||||
viewThemeUtils.talk.themeOutgoingMessageBubble(binding.bubble, true, false,
|
||||
false);
|
||||
}
|
||||
|
||||
if (processedMessageText != null) {
|
||||
processedMessageText = messageUtils.processMessageParameters(
|
||||
binding.messageCaption.getContext(),
|
||||
viewThemeUtils,
|
||||
processedMessageText,
|
||||
message,
|
||||
binding.bubble);
|
||||
}
|
||||
binding.bubble.setOnClickListener(null);
|
||||
|
||||
float textSize = 0;
|
||||
if (context != null) {
|
||||
textSize = context.getResources().getDimension(R.dimen.chat_text_size);
|
||||
}
|
||||
HashMap<String, HashMap<String, String>> messageParameters = message.getMessageParameters();
|
||||
if (
|
||||
(messageParameters == null || messageParameters.size() <= 0) &&
|
||||
TextMatchers.isMessageWithSingleEmoticonOnly(message.getText())
|
||||
) {
|
||||
textSize = (float)(textSize * IncomingTextMessageViewHolder.TEXT_SIZE_MULTIPLIER);
|
||||
itemView.setSelected(true);
|
||||
}
|
||||
binding.messageCaption.setVisibility(View.VISIBLE);
|
||||
binding.messageCaption.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
|
||||
binding.messageCaption.setText(processedMessageText);
|
||||
} else {
|
||||
binding.bubble.setBackground(null);
|
||||
binding.messageCaption.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
binding.messageText.setTextColor(ContextCompat.getColor(binding.messageText.getContext(),
|
||||
R.color.no_emphasis_text));
|
||||
binding.messageTime.setTextColor(ContextCompat.getColor(binding.messageText.getContext(),
|
||||
R.color.no_emphasis_text));
|
||||
|
||||
binding.messageEditIndicator.setTextColor(ContextCompat.getColor(binding.messageText.getContext(),
|
||||
R.color.no_emphasis_text));
|
||||
|
||||
binding.checkMark.setVisibility(View.GONE);
|
||||
Integer readStatusDrawableInt = null;
|
||||
String readStatusContentDescriptionString = null;
|
||||
if (message.getReadStatus() == ReadStatus.READ) {
|
||||
readStatusDrawableInt = R.drawable.ic_check_all;
|
||||
readStatusContentDescriptionString =
|
||||
binding.checkMark.getContext().getString(R.string.nc_message_read);
|
||||
} else if (message.getReadStatus() == ReadStatus.SENT) {
|
||||
readStatusDrawableInt = R.drawable.ic_check;
|
||||
readStatusContentDescriptionString =
|
||||
binding.checkMark.getContext().getString(R.string.nc_message_sent);
|
||||
}
|
||||
|
||||
if (readStatusDrawableInt != null) {
|
||||
binding.checkMark.setVisibility(View.VISIBLE);
|
||||
binding.checkMark.setImageDrawable(ContextCompat.getDrawable(binding.checkMark.getContext(),
|
||||
readStatusDrawableInt));
|
||||
if (viewThemeUtils != null) {
|
||||
viewThemeUtils.talk.themeMessageCheckMark(binding.checkMark);
|
||||
}
|
||||
}
|
||||
binding.checkMark.setContentDescription(readStatusContentDescriptionString);
|
||||
|
||||
|
||||
if(!message.isThread()) {
|
||||
binding.threadTitleWrapperContainer.setVisibility(View.GONE);
|
||||
} else {
|
||||
binding.threadTitleWrapperContainer.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public EmojiTextView getMessageText() {
|
||||
return binding.messageText;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProgressBar getProgressBar() {
|
||||
return binding.progressBar;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public View getPreviewContainer() {
|
||||
return binding.previewContainer;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public MaterialCardView getPreviewContactContainer() {
|
||||
return binding.contactContainer;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ImageView getPreviewContactPhoto() {
|
||||
return binding.contactPhoto;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public EmojiTextView getPreviewContactName() {
|
||||
return binding.contactName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProgressBar getPreviewContactProgressBar() {
|
||||
return binding.contactProgressBar;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactionsInsideMessageBinding getReactionsBinding() { return binding.reactions; }
|
||||
|
||||
@Override
|
||||
public ItemThreadTitleBinding getThreadsBinding(){ return binding.threadTitleWrapper; }
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public EmojiTextView getMessageCaption() { return binding.messageCaption; }
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public TextView getMessageEditIndicator() {
|
||||
return binding.messageEditIndicator;
|
||||
}
|
||||
}
|
||||
+486
@@ -0,0 +1,486 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Christian Reiner <foss@christian-reiner.info>
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2021 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.messages
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckBox
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
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 ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication.Companion.sharedApplication
|
||||
import ru.f7cloud.talk.chat.ChatActivity
|
||||
import ru.f7cloud.talk.chat.data.ChatMessageRepository
|
||||
import ru.f7cloud.talk.chat.data.model.ChatMessage
|
||||
import ru.f7cloud.talk.data.database.model.SendStatus
|
||||
import ru.f7cloud.talk.data.network.NetworkMonitor
|
||||
import ru.f7cloud.talk.data.user.model.User
|
||||
import ru.f7cloud.talk.databinding.ItemCustomOutcomingTextMessageBinding
|
||||
import ru.f7cloud.talk.models.json.chat.ChatUtils
|
||||
import ru.f7cloud.talk.models.json.chat.ReadStatus
|
||||
import ru.f7cloud.talk.models.json.conversations.ConversationEnums
|
||||
import ru.f7cloud.talk.ui.theme.ViewThemeUtils
|
||||
import ru.f7cloud.talk.utils.ApiUtils
|
||||
import ru.f7cloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability
|
||||
import ru.f7cloud.talk.utils.DateUtils
|
||||
import ru.f7cloud.talk.utils.SpreedFeatures
|
||||
import ru.f7cloud.talk.utils.TextMatchers
|
||||
import ru.f7cloud.talk.utils.database.user.CurrentUserProviderOld
|
||||
import ru.f7cloud.talk.utils.message.MessageCheckboxUtils.addCheckboxLine
|
||||
import ru.f7cloud.talk.utils.message.MessageCheckboxUtils.addPlainTextLine
|
||||
import ru.f7cloud.talk.utils.message.MessageCheckboxUtils.matchCheckbox
|
||||
import ru.f7cloud.talk.utils.message.MessageCheckboxUtils.updateMessageWithCheckboxStates
|
||||
import ru.f7cloud.talk.utils.message.MessageUtils
|
||||
import com.stfalcon.chatkit.messages.MessageHolders.OutcomingTextMessageViewHolder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(F7cloudTalkApplication::class)
|
||||
class OutcomingTextMessageViewHolder(itemView: View) :
|
||||
OutcomingTextMessageViewHolder<ChatMessage>(itemView),
|
||||
AdjustableMessageHolderInterface {
|
||||
|
||||
override val binding: ItemCustomOutcomingTextMessageBinding = ItemCustomOutcomingTextMessageBinding.bind(itemView)
|
||||
private val realView: View = itemView
|
||||
|
||||
@Inject
|
||||
lateinit var context: Context
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var messageUtils: MessageUtils
|
||||
|
||||
@Inject
|
||||
lateinit var dateUtils: DateUtils
|
||||
|
||||
@Inject
|
||||
lateinit var networkMonitor: NetworkMonitor
|
||||
|
||||
lateinit var commonMessageInterface: CommonMessageInterface
|
||||
|
||||
@Inject
|
||||
lateinit var chatRepository: ChatMessageRepository
|
||||
|
||||
@Inject
|
||||
lateinit var currentUserProvider: CurrentUserProviderOld
|
||||
|
||||
private var job: Job? = null
|
||||
|
||||
@Suppress("Detekt.LongMethod")
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
val user = currentUserProvider.currentUser.blockingGet()
|
||||
val hasCheckboxes = processCheckboxes(
|
||||
message,
|
||||
user
|
||||
)
|
||||
processMessage(message, hasCheckboxes)
|
||||
}
|
||||
|
||||
@Suppress("Detekt.LongMethod")
|
||||
private fun processMessage(message: ChatMessage, hasCheckboxes: Boolean) {
|
||||
var isBubbled = true
|
||||
val layoutParams = binding.messageTime.layoutParams as FlexboxLayout.LayoutParams
|
||||
var textSize = context.resources.getDimension(R.dimen.chat_text_size)
|
||||
if (!hasCheckboxes) {
|
||||
realView.isSelected = false
|
||||
layoutParams.isWrapBefore = false
|
||||
|
||||
binding.messageText.visibility = View.VISIBLE
|
||||
binding.checkboxContainer.visibility = View.GONE
|
||||
|
||||
var processedMessageText = messageUtils.enrichChatMessageText(
|
||||
binding.messageText.context,
|
||||
message,
|
||||
false,
|
||||
viewThemeUtils
|
||||
)
|
||||
|
||||
val spansFromString: Array<Any> = processedMessageText!!.getSpans(
|
||||
0,
|
||||
processedMessageText.length,
|
||||
Any::class.java
|
||||
)
|
||||
|
||||
if (spansFromString.isNotEmpty()) {
|
||||
binding.bubble.layoutParams.apply {
|
||||
width = FlexboxLayout.LayoutParams.MATCH_PARENT
|
||||
}
|
||||
binding.messageText.layoutParams.apply {
|
||||
width = FlexboxLayout.LayoutParams.MATCH_PARENT
|
||||
}
|
||||
} else {
|
||||
binding.bubble.layoutParams.apply {
|
||||
width = FlexboxLayout.LayoutParams.WRAP_CONTENT
|
||||
}
|
||||
binding.messageText.layoutParams.apply {
|
||||
width = FlexboxLayout.LayoutParams.WRAP_CONTENT
|
||||
}
|
||||
}
|
||||
|
||||
processedMessageText = messageUtils.processMessageParameters(
|
||||
binding.messageText.context,
|
||||
viewThemeUtils,
|
||||
processedMessageText,
|
||||
message,
|
||||
itemView
|
||||
)
|
||||
|
||||
if (
|
||||
(message.messageParameters == null || message.messageParameters!!.size <= 0) &&
|
||||
TextMatchers.isMessageWithSingleEmoticonOnly(message.text)
|
||||
) {
|
||||
textSize = (textSize * TEXT_SIZE_MULTIPLIER).toFloat()
|
||||
layoutParams.isWrapBefore = true
|
||||
realView.isSelected = true
|
||||
isBubbled = false
|
||||
}
|
||||
|
||||
binding.messageTime.layoutParams = layoutParams
|
||||
viewThemeUtils.platform.colorTextView(binding.messageText, ColorRole.ON_SURFACE_VARIANT)
|
||||
binding.messageText.text = processedMessageText
|
||||
// just for debugging:
|
||||
// binding.messageText.text =
|
||||
// SpannableStringBuilder(processedMessageText).append(" (" + message.jsonMessageId + ")")
|
||||
} else {
|
||||
binding.messageText.visibility = View.GONE
|
||||
binding.checkboxContainer.visibility = View.VISIBLE
|
||||
}
|
||||
binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
||||
if (message.lastEditTimestamp != 0L && !message.isDeleted) {
|
||||
binding.messageEditIndicator.visibility = View.VISIBLE
|
||||
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.lastEditTimestamp!!)
|
||||
} else {
|
||||
binding.messageEditIndicator.visibility = View.GONE
|
||||
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
}
|
||||
viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT)
|
||||
setBubbleOnChatMessage(message)
|
||||
|
||||
// parent message handling
|
||||
val chatActivity = commonMessageInterface as? ChatActivity
|
||||
binding.messageQuote.quotedChatMessageView.visibility =
|
||||
if (chatActivity != null &&
|
||||
!message.isDeleted &&
|
||||
message.parentMessageId != null &&
|
||||
message.parentMessageId != chatActivity.conversationThreadId
|
||||
) {
|
||||
processParentMessage(message)
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.setOnLongClickListener { l: View? ->
|
||||
commonMessageInterface.onOpenMessageActionsDialog(message)
|
||||
true
|
||||
}
|
||||
|
||||
binding.checkMark.visibility = View.INVISIBLE
|
||||
binding.sendingProgress.visibility = View.GONE
|
||||
|
||||
if (message.sendStatus == SendStatus.FAILED) {
|
||||
updateStatus(R.drawable.baseline_error_outline_24, context.resources?.getString(R.string.nc_message_failed))
|
||||
} else if (message.isTemporary) {
|
||||
updateStatus(R.drawable.baseline_schedule_24, context.resources?.getString(R.string.nc_message_sending))
|
||||
} else if (message.readStatus == ReadStatus.READ) {
|
||||
updateStatus(R.drawable.ic_check_all, context.resources?.getString(R.string.nc_message_read))
|
||||
} else if (message.readStatus == ReadStatus.SENT) {
|
||||
updateStatus(R.drawable.ic_check, context.resources?.getString(R.string.nc_message_sent))
|
||||
}
|
||||
|
||||
chatActivity?.lifecycleScope?.launch {
|
||||
if (message.isTemporary && !networkMonitor.isOnline.value) {
|
||||
updateStatus(
|
||||
R.drawable.ic_signal_wifi_off_white_24dp,
|
||||
context.resources?.getString(R.string.nc_message_offline)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
itemView.setTag(R.string.replyable_message_view_tag, message.replyable)
|
||||
|
||||
if (chatActivity != null) {
|
||||
Thread().showThreadPreview(
|
||||
chatActivity,
|
||||
message,
|
||||
threadBinding = binding.threadTitleWrapper,
|
||||
reactionsBinding = binding.reactions,
|
||||
openThread = { openThread(message) }
|
||||
)
|
||||
}
|
||||
|
||||
Reaction().showReactions(
|
||||
message,
|
||||
::clickOnReaction,
|
||||
::longClickOnReaction,
|
||||
binding.reactions,
|
||||
context,
|
||||
true,
|
||||
viewThemeUtils,
|
||||
isBubbled
|
||||
)
|
||||
}
|
||||
|
||||
private fun processCheckboxes(chatMessage: ChatMessage, user: User): Boolean {
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
val message = chatMessage.message ?: return false
|
||||
val checkBoxContainer = binding.checkboxContainer
|
||||
|
||||
val isOlderThanTwentyFourHours = chatMessage
|
||||
.createdAt
|
||||
.before(
|
||||
Date(System.currentTimeMillis() - AGE_THRESHOLD_FOR_EDIT_MESSAGE)
|
||||
)
|
||||
val messageIsEditable = hasSpreedFeatureCapability(
|
||||
user.capabilities?.spreedCapability!!,
|
||||
SpreedFeatures.EDIT_MESSAGES
|
||||
) &&
|
||||
!isOlderThanTwentyFourHours
|
||||
|
||||
val isNoLimitOnNoteToSelf = hasSpreedFeatureCapability(
|
||||
user.capabilities?.spreedCapability!!,
|
||||
SpreedFeatures.EDIT_MESSAGES_NOTE_TO_SELF
|
||||
) &&
|
||||
chatActivity.currentConversation?.type == ConversationEnums.ConversationType.NOTE_TO_SELF
|
||||
|
||||
checkBoxContainer.removeAllViews()
|
||||
return renderCheckboxLines(
|
||||
chatMessage,
|
||||
user,
|
||||
message,
|
||||
messageIsEditable || isNoLimitOnNoteToSelf,
|
||||
checkBoxContainer
|
||||
)
|
||||
}
|
||||
|
||||
private fun renderCheckboxLines(
|
||||
chatMessage: ChatMessage,
|
||||
user: User,
|
||||
message: String,
|
||||
editable: Boolean,
|
||||
checkBoxContainer: ViewGroup
|
||||
): Boolean {
|
||||
val checkboxList = mutableListOf<CheckBox>()
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
var hasCheckbox = false
|
||||
val marginPx = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
PADDING_FOUR_FLOAT,
|
||||
context.resources.displayMetrics
|
||||
).toInt()
|
||||
|
||||
message.lines().forEach { line ->
|
||||
val trimmed = line.trimEnd()
|
||||
val match = matchCheckbox(trimmed)
|
||||
if (match != null) {
|
||||
hasCheckbox = true
|
||||
val isChecked = match.groupValues[CHECKED_GROUP_INDEX].equals("X", true)
|
||||
val taskText = match.groupValues[TASK_TEXT_GROUP_INDEX].trim()
|
||||
val checkBox = addCheckboxLine(
|
||||
context = chatActivity,
|
||||
container = checkBoxContainer,
|
||||
chatMessage = chatMessage,
|
||||
taskText = taskText,
|
||||
isChecked = isChecked,
|
||||
isEnabled = editable,
|
||||
isIncomingMessage = false,
|
||||
messageUtils = messageUtils,
|
||||
viewThemeUtils = viewThemeUtils,
|
||||
linkColorRes = R.color.no_emphasis_text,
|
||||
paddingPx = marginPx
|
||||
) { _, _ ->
|
||||
updateCheckboxStates(chatMessage, user, checkboxList)
|
||||
}
|
||||
checkboxList.add(checkBox)
|
||||
} else if (trimmed.isNotBlank()) {
|
||||
addPlainTextLine(
|
||||
context = checkBoxContainer.context,
|
||||
container = checkBoxContainer,
|
||||
chatMessage = chatMessage,
|
||||
text = trimmed,
|
||||
isIncomingMessage = false,
|
||||
messageUtils = messageUtils,
|
||||
viewThemeUtils = viewThemeUtils,
|
||||
linkColorRes = R.color.no_emphasis_text,
|
||||
paddingPx = marginPx
|
||||
)
|
||||
}
|
||||
}
|
||||
return hasCheckbox
|
||||
}
|
||||
|
||||
private fun updateCheckboxStates(chatMessage: ChatMessage, user: User, checkboxes: List<CheckBox>) {
|
||||
job = CoroutineScope(Dispatchers.Main).launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val apiVersion: Int = ApiUtils.getChatApiVersion(
|
||||
user.capabilities?.spreedCapability!!,
|
||||
intArrayOf(1)
|
||||
)
|
||||
val updatedMessage = updateMessageWithCheckboxStates(chatMessage.message!!, checkboxes)
|
||||
val messageParameters = chatMessage.messageParameters
|
||||
val messageToSend = if (!messageParameters.isNullOrEmpty()) {
|
||||
val parsedMessage = ChatUtils.getParsedMessage(updatedMessage, messageParameters) ?: updatedMessage
|
||||
messageUtils.processEditMessageParameters(
|
||||
messageParameters,
|
||||
chatMessage,
|
||||
parsedMessage
|
||||
).toString()
|
||||
} else {
|
||||
updatedMessage
|
||||
}
|
||||
|
||||
chatRepository.editChatMessage(
|
||||
user.getCredentials(),
|
||||
ApiUtils.getUrlForChatMessage(apiVersion, user.baseUrl!!, chatMessage.token!!, chatMessage.id),
|
||||
messageToSend
|
||||
).collect { result ->
|
||||
withContext(Dispatchers.Main) {
|
||||
if (result.isSuccess) {
|
||||
val editedMessage = result.getOrNull()?.ocs?.data!!.parentMessage!!
|
||||
Log.d(TAG, "EditedMessage: $editedMessage")
|
||||
binding.messageEditIndicator.apply {
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
binding.messageTime.text =
|
||||
dateUtils.getLocalTimeStringFromTimestamp(editedMessage.lastEditTimestamp!!)
|
||||
} else {
|
||||
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStatus(readStatusDrawableInt: Int, description: String?) {
|
||||
binding.sendingProgress.visibility = View.GONE
|
||||
binding.checkMark.visibility = View.VISIBLE
|
||||
readStatusDrawableInt.let { drawableInt ->
|
||||
ResourcesCompat.getDrawable(context.resources, drawableInt, null)?.let {
|
||||
binding.checkMark.setImageDrawable(it)
|
||||
viewThemeUtils.talk.themeMessageCheckMark(binding.checkMark)
|
||||
}
|
||||
}
|
||||
binding.checkMark.contentDescription = description
|
||||
}
|
||||
|
||||
private fun longClickOnReaction(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.onLongClickReactions(chatMessage)
|
||||
}
|
||||
|
||||
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
|
||||
commonMessageInterface.onClickReaction(chatMessage, emoji)
|
||||
}
|
||||
|
||||
private fun openThread(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.openThread(chatMessage)
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught")
|
||||
private fun processParentMessage(message: ChatMessage) {
|
||||
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.themeParentMessage(
|
||||
parentChatMessage,
|
||||
message,
|
||||
binding.messageQuote.quotedChatMessageView
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.setOnClickListener {
|
||||
chatActivity.jumpToQuotedMessage(parentChatMessage)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error when processing parent message in view holder", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setBubbleOnChatMessage(message: ChatMessage) {
|
||||
viewThemeUtils.talk.themeOutgoingMessageBubble(bubble, message.isGrouped, message.isDeleted)
|
||||
}
|
||||
|
||||
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
|
||||
this.commonMessageInterface = commonMessageInterface
|
||||
}
|
||||
|
||||
override fun viewDetached() {
|
||||
super.viewDetached()
|
||||
job?.cancel()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TEXT_SIZE_MULTIPLIER = 2.5
|
||||
private val TAG = OutcomingTextMessageViewHolder::class.java.simpleName
|
||||
private const val AGE_THRESHOLD_FOR_EDIT_MESSAGE: Long = 86400000
|
||||
private const val CHECKED_GROUP_INDEX = 1
|
||||
private const val TASK_TEXT_GROUP_INDEX = 2
|
||||
private const val PADDING_FOUR_FLOAT = 4f
|
||||
}
|
||||
}
|
||||
+401
@@ -0,0 +1,401 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Christian Reiner <foss@christian-reiner.info>
|
||||
* SPDX-FileCopyrightText: 2023 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2023 Julius Linus <juliuslinus1@gmail.com>
|
||||
* SPDX-FileCopyrightText: 2021 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.messages
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.SeekBar
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import autodagger.AutoInjector
|
||||
import coil.load
|
||||
import com.nextcloud.android.common.ui.theme.utils.ColorRole
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication.Companion.sharedApplication
|
||||
import ru.f7cloud.talk.chat.ChatActivity
|
||||
import ru.f7cloud.talk.chat.data.model.ChatMessage
|
||||
import ru.f7cloud.talk.databinding.ItemCustomOutcomingVoiceMessageBinding
|
||||
import ru.f7cloud.talk.models.json.chat.ReadStatus
|
||||
import ru.f7cloud.talk.ui.theme.ViewThemeUtils
|
||||
import ru.f7cloud.talk.utils.ApiUtils
|
||||
import ru.f7cloud.talk.utils.DateUtils
|
||||
import ru.f7cloud.talk.utils.message.MessageUtils
|
||||
import ru.f7cloud.talk.utils.preferences.AppPreferences
|
||||
import com.stfalcon.chatkit.messages.MessageHolders
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.ExecutionException
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(F7cloudTalkApplication::class)
|
||||
@Suppress("Detekt.TooManyFunctions")
|
||||
class OutcomingVoiceMessageViewHolder(outcomingView: View) :
|
||||
MessageHolders.OutcomingTextMessageViewHolder<ChatMessage>(outcomingView),
|
||||
AdjustableMessageHolderInterface {
|
||||
|
||||
override val binding: ItemCustomOutcomingVoiceMessageBinding = ItemCustomOutcomingVoiceMessageBinding.bind(itemView)
|
||||
|
||||
@JvmField
|
||||
@Inject
|
||||
var context: Context? = null
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var messageUtils: MessageUtils
|
||||
|
||||
@Inject
|
||||
lateinit var dateUtils: DateUtils
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
lateinit var message: ChatMessage
|
||||
|
||||
lateinit var handler: Handler
|
||||
|
||||
lateinit var voiceMessageInterface: VoiceMessageInterface
|
||||
lateinit var commonMessageInterface: CommonMessageInterface
|
||||
private var isBound = false
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
if (isBound) {
|
||||
handleIsPlayingVoiceMessageState(message)
|
||||
return
|
||||
}
|
||||
|
||||
this.message = message
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT)
|
||||
|
||||
val filename = message.selectedIndividualHashMap!!["name"]
|
||||
val retrieved = appPreferences.getWaveFormFromFile(filename)
|
||||
if (retrieved.isNotEmpty() &&
|
||||
message.voiceMessageFloatArray == null ||
|
||||
message.voiceMessageFloatArray?.isEmpty() == true
|
||||
) {
|
||||
message.voiceMessageFloatArray = retrieved.toFloatArray()
|
||||
binding.seekbar.setWaveData(message.voiceMessageFloatArray!!)
|
||||
}
|
||||
|
||||
binding.seekbar.max = MAX
|
||||
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
|
||||
colorizeMessageBubble(message)
|
||||
|
||||
itemView.isSelected = false
|
||||
|
||||
// parent message handling
|
||||
setParentMessageDataOnMessageItem(message)
|
||||
|
||||
updateDownloadState(message)
|
||||
viewThemeUtils.talk.themeWaveFormSeekBar(binding.seekbar)
|
||||
viewThemeUtils.platform.colorCircularProgressBar(binding.progressBar, ColorRole.ON_SURFACE_VARIANT)
|
||||
|
||||
showVoiceMessageDuration(message)
|
||||
|
||||
handleIsDownloadingVoiceMessageState(message)
|
||||
|
||||
handleResetVoiceMessageState(message)
|
||||
|
||||
binding.seekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||
if (fromUser) {
|
||||
voiceMessageInterface.updateMediaPlayerProgressBySlider(message, progress)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
setReadStatus(message.readStatus)
|
||||
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
(voiceMessageInterface as ChatActivity).chatViewModel.voiceMessagePlayBackUIFlow.onEach { speed ->
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.playbackSpeedControlBtn.setSpeed(speed)
|
||||
}
|
||||
}.collect()
|
||||
}
|
||||
|
||||
binding.playbackSpeedControlBtn.setSpeed(appPreferences.getPreferredPlayback(message.actorId))
|
||||
|
||||
Reaction().showReactions(
|
||||
message,
|
||||
::clickOnReaction,
|
||||
::longClickOnReaction,
|
||||
binding.reactions,
|
||||
binding.messageTime.context,
|
||||
true,
|
||||
viewThemeUtils
|
||||
)
|
||||
isBound = true
|
||||
}
|
||||
|
||||
private fun setReadStatus(readStatus: Enum<ReadStatus>) {
|
||||
val readStatusDrawableInt = when (readStatus) {
|
||||
ReadStatus.READ -> R.drawable.ic_check_all
|
||||
ReadStatus.SENT -> R.drawable.ic_check
|
||||
else -> null
|
||||
}
|
||||
|
||||
val readStatusContentDescriptionString = when (readStatus) {
|
||||
ReadStatus.READ -> context?.resources?.getString(R.string.nc_message_read)
|
||||
ReadStatus.SENT -> context?.resources?.getString(R.string.nc_message_sent)
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (readStatusDrawableInt != null) {
|
||||
AppCompatResources.getDrawable(context!!, readStatusDrawableInt)?.let {
|
||||
binding.checkMark.setImageDrawable(it)
|
||||
viewThemeUtils.talk.themeMessageCheckMark(binding.checkMark)
|
||||
}
|
||||
binding.checkMark.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.checkMark.visibility = View.GONE
|
||||
}
|
||||
|
||||
binding.checkMark.contentDescription = readStatusContentDescriptionString
|
||||
}
|
||||
|
||||
private fun longClickOnReaction(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.onLongClickReactions(chatMessage)
|
||||
}
|
||||
|
||||
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
|
||||
commonMessageInterface.onClickReaction(chatMessage, emoji)
|
||||
}
|
||||
|
||||
private fun handleResetVoiceMessageState(message: ChatMessage) {
|
||||
if (message.resetVoiceMessage) {
|
||||
binding.playPauseBtn.visibility = View.VISIBLE
|
||||
binding.playPauseBtn.icon = ContextCompat.getDrawable(
|
||||
context!!,
|
||||
R.drawable.ic_baseline_play_arrow_voice_message_24
|
||||
)
|
||||
binding.seekbar.progress = SEEKBAR_START
|
||||
message.voiceMessagePlayedSeconds = 0
|
||||
showVoiceMessageDuration(message)
|
||||
message.resetVoiceMessage = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun showVoiceMessageDuration(message: ChatMessage) {
|
||||
if (message.voiceMessageDuration > 0) {
|
||||
binding.voiceMessageDuration.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.voiceMessageDuration.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleIsDownloadingVoiceMessageState(message: ChatMessage) {
|
||||
if (message.isDownloadingVoiceMessage) {
|
||||
showVoiceMessageLoading()
|
||||
} else {
|
||||
if (message.voiceMessageFloatArray == null || message.voiceMessageFloatArray!!.isEmpty()) {
|
||||
binding.seekbar.setWaveData(FloatArray(0))
|
||||
} else {
|
||||
binding.seekbar.setWaveData(message.voiceMessageFloatArray!!)
|
||||
}
|
||||
binding.progressBar.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleIsPlayingVoiceMessageState(message: ChatMessage) {
|
||||
colorizeMessageBubble(message)
|
||||
if (message.isPlayingVoiceMessage) {
|
||||
showPlayButton()
|
||||
binding.playPauseBtn.icon = ContextCompat.getDrawable(
|
||||
context!!,
|
||||
R.drawable.ic_baseline_pause_voice_message_24
|
||||
)
|
||||
|
||||
val d = message.voiceMessageDuration.toLong()
|
||||
val t = message.voiceMessagePlayedSeconds.toLong()
|
||||
binding.voiceMessageDuration.text = android.text.format.DateUtils.formatElapsedTime(d - t)
|
||||
binding.voiceMessageDuration.visibility = View.VISIBLE
|
||||
binding.seekbar.progress = message.voiceMessageSeekbarProgress
|
||||
} else {
|
||||
showVoiceMessageDuration(message)
|
||||
binding.playPauseBtn.visibility = View.VISIBLE
|
||||
binding.playPauseBtn.icon = ContextCompat.getDrawable(
|
||||
context!!,
|
||||
R.drawable.ic_baseline_play_arrow_voice_message_24
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDownloadState(message: ChatMessage) {
|
||||
// check if download worker is already running
|
||||
val fileId = message.selectedIndividualHashMap!!["id"]
|
||||
val workers = WorkManager.getInstance(context!!).getWorkInfosByTag(fileId!!)
|
||||
|
||||
try {
|
||||
for (workInfo in workers.get()) {
|
||||
if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) {
|
||||
showVoiceMessageLoading()
|
||||
WorkManager.getInstance(context!!).getWorkInfoByIdLiveData(workInfo.id)
|
||||
.observeForever { info: WorkInfo? ->
|
||||
updateDownloadState(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: ExecutionException) {
|
||||
Log.e(TAG, "Error when checking if worker already exists", e)
|
||||
} catch (e: InterruptedException) {
|
||||
Log.e(TAG, "Error when checking if worker already exists", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDownloadState(info: WorkInfo?) {
|
||||
if (info != null) {
|
||||
when (info.state) {
|
||||
WorkInfo.State.RUNNING -> {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showPlayButton() {
|
||||
binding.playPauseBtn.visibility = View.VISIBLE
|
||||
binding.progressBar.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun showVoiceMessageLoading() {
|
||||
binding.playPauseBtn.visibility = View.GONE
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
|
||||
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
|
||||
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.themeParentMessage(
|
||||
parentChatMessage,
|
||||
message,
|
||||
binding.messageQuote.quotedChatMessageView
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.visibility =
|
||||
if (!message.isDeleted &&
|
||||
message.parentMessageId != null &&
|
||||
message.parentMessageId != chatActivity.conversationThreadId
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error when processing parent message in view holder", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun colorizeMessageBubble(message: ChatMessage) {
|
||||
viewThemeUtils.talk.themeOutgoingMessageBubble(
|
||||
bubble,
|
||||
message.isGrouped,
|
||||
message.isDeleted,
|
||||
message.wasPlayedVoiceMessage
|
||||
)
|
||||
}
|
||||
|
||||
fun assignVoiceMessageInterface(voiceMessageInterface: VoiceMessageInterface) {
|
||||
this.voiceMessageInterface = voiceMessageInterface
|
||||
}
|
||||
|
||||
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
|
||||
this.commonMessageInterface = commonMessageInterface
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "VoiceOutMessageView"
|
||||
private const val SEEKBAR_START: Int = 0
|
||||
private const val MAX = 100
|
||||
}
|
||||
}
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.messages
|
||||
|
||||
import ru.f7cloud.talk.chat.data.model.ChatMessage
|
||||
|
||||
interface PreviewMessageInterface {
|
||||
fun onPreviewMessageLongClick(chatMessage: ChatMessage)
|
||||
}
|
||||
Vendored
+368
@@ -0,0 +1,368 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2021-2022 Tim Krüger <t@timkrueger.me>
|
||||
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2021 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.messages
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Handler
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.emoji2.widget.EmojiTextView
|
||||
import autodagger.AutoInjector
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.nextcloud.android.common.ui.theme.utils.ColorRole
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication.Companion.sharedApplication
|
||||
import ru.f7cloud.talk.chat.ChatActivity
|
||||
import ru.f7cloud.talk.chat.data.model.ChatMessage
|
||||
import ru.f7cloud.talk.data.user.model.User
|
||||
import ru.f7cloud.talk.databinding.ItemThreadTitleBinding
|
||||
import ru.f7cloud.talk.databinding.ReactionsInsideMessageBinding
|
||||
import ru.f7cloud.talk.extensions.loadChangelogBotAvatar
|
||||
import ru.f7cloud.talk.extensions.loadFederatedUserAvatar
|
||||
import ru.f7cloud.talk.filebrowser.models.BrowserFile
|
||||
import ru.f7cloud.talk.filebrowser.webdav.ReadFilesystemOperation
|
||||
import ru.f7cloud.talk.ui.theme.ViewThemeUtils
|
||||
import ru.f7cloud.talk.users.UserManager
|
||||
import ru.f7cloud.talk.utils.DateUtils
|
||||
import ru.f7cloud.talk.utils.DisplayUtils
|
||||
import ru.f7cloud.talk.utils.DrawableUtils.getDrawableResourceIdForMimeType
|
||||
import ru.f7cloud.talk.utils.FileViewerUtils
|
||||
import ru.f7cloud.talk.utils.FileViewerUtils.ProgressUi
|
||||
import ru.f7cloud.talk.utils.message.MessageUtils
|
||||
import com.stfalcon.chatkit.messages.MessageHolders.IncomingImageMessageViewHolder
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.SingleObserver
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import okhttp3.OkHttpClient
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(F7cloudTalkApplication::class)
|
||||
abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) :
|
||||
IncomingImageMessageViewHolder<ChatMessage>(itemView, payload) {
|
||||
@JvmField
|
||||
@Inject
|
||||
var context: Context? = null
|
||||
|
||||
@JvmField
|
||||
@Inject
|
||||
var viewThemeUtils: ViewThemeUtils? = null
|
||||
|
||||
@Inject
|
||||
lateinit var dateUtils: DateUtils
|
||||
|
||||
@Inject
|
||||
lateinit var messageUtils: MessageUtils
|
||||
|
||||
@Inject
|
||||
lateinit var userManager: UserManager
|
||||
|
||||
@JvmField
|
||||
@Inject
|
||||
var okHttpClient: OkHttpClient? = null
|
||||
open var progressBar: ProgressBar? = null
|
||||
open var reactionsBinding: ReactionsInsideMessageBinding? = null
|
||||
open var threadsBinding: ItemThreadTitleBinding? = null
|
||||
var fileViewerUtils: FileViewerUtils? = null
|
||||
var clickView: View? = null
|
||||
|
||||
lateinit var commonMessageInterface: CommonMessageInterface
|
||||
var previewMessageInterface: PreviewMessageInterface? = null
|
||||
|
||||
private var placeholder: Drawable? = null
|
||||
|
||||
init {
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@Suppress("NestedBlockDepth", "ComplexMethod", "LongMethod")
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
image.minimumHeight = DisplayUtils.convertDpToPixel(MIN_IMAGE_HEIGHT, context!!).toInt()
|
||||
|
||||
if (message.lastEditTimestamp != 0L && !message.isDeleted) {
|
||||
time.text = dateUtils.getLocalTimeStringFromTimestamp(message.lastEditTimestamp!!)
|
||||
messageEditIndicator.visibility = View.VISIBLE
|
||||
} else {
|
||||
time.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
messageEditIndicator.visibility = View.GONE
|
||||
}
|
||||
|
||||
viewThemeUtils!!.platform.colorCircularProgressBar(progressBar!!, ColorRole.PRIMARY)
|
||||
clickView = image
|
||||
messageText.visibility = View.VISIBLE
|
||||
if (message.getCalculateMessageType() === ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE) {
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
fileViewerUtils = FileViewerUtils(chatActivity, message.activeUser!!)
|
||||
val fileName = message.selectedIndividualHashMap!![KEY_NAME]
|
||||
|
||||
messageText.text = fileName
|
||||
|
||||
if (message.activeUser != null &&
|
||||
message.activeUser!!.username != null &&
|
||||
message.activeUser!!.baseUrl != null
|
||||
) {
|
||||
clickView!!.setOnClickListener { v: View? ->
|
||||
fileViewerUtils!!.openFile(
|
||||
message,
|
||||
ProgressUi(progressBar, messageText, image)
|
||||
)
|
||||
}
|
||||
clickView!!.setOnLongClickListener {
|
||||
previewMessageInterface!!.onPreviewMessageLongClick(message)
|
||||
true
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "failed to set click listener because activeUser, username or baseUrl were null")
|
||||
}
|
||||
fileViewerUtils!!.resumeToUpdateViewsByProgress(
|
||||
message.selectedIndividualHashMap!![KEY_NAME]!!,
|
||||
message.selectedIndividualHashMap!![KEY_ID]!!,
|
||||
message.selectedIndividualHashMap!![KEY_MIMETYPE],
|
||||
message.openWhenDownloaded,
|
||||
ProgressUi(progressBar, messageText, image)
|
||||
)
|
||||
} else if (message.getCalculateMessageType() === ChatMessage.MessageType.SINGLE_LINK_GIPHY_MESSAGE) {
|
||||
messageText.text = "GIPHY"
|
||||
DisplayUtils.setClickableString("GIPHY", "https://giphy.com", messageText)
|
||||
} else if (message.getCalculateMessageType() === ChatMessage.MessageType.SINGLE_LINK_TENOR_MESSAGE) {
|
||||
messageText.text = "Tenor"
|
||||
DisplayUtils.setClickableString("Tenor", "https://tenor.com", messageText)
|
||||
} else {
|
||||
if (message.messageType == ChatMessage.MessageType.SINGLE_LINK_IMAGE_MESSAGE.name) {
|
||||
clickView!!.setOnClickListener {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, message.imageUrl!!.toUri())
|
||||
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context!!.startActivity(browserIntent)
|
||||
}
|
||||
} else {
|
||||
clickView!!.setOnClickListener(null)
|
||||
}
|
||||
messageText.text = ""
|
||||
}
|
||||
itemView.setTag(R.string.replyable_message_view_tag, message.replyable)
|
||||
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
Thread().showThreadPreview(
|
||||
chatActivity,
|
||||
message,
|
||||
threadBinding = threadsBinding!!,
|
||||
reactionsBinding = reactionsBinding!!,
|
||||
openThread = { openThread(message) }
|
||||
)
|
||||
|
||||
val paddingSide = DisplayUtils.convertDpToPixel(HORIZONTAL_REACTION_PADDING, context!!).toInt()
|
||||
Reaction().showReactions(
|
||||
message,
|
||||
::clickOnReaction,
|
||||
::longClickOnReaction,
|
||||
reactionsBinding!!,
|
||||
messageText.context,
|
||||
true,
|
||||
viewThemeUtils!!,
|
||||
hasBubbleBackground(message)
|
||||
)
|
||||
reactionsBinding!!.reactionsEmojiWrapper.setPadding(paddingSide, 0, paddingSide, 0)
|
||||
|
||||
if (userAvatar != null) {
|
||||
if (message.isGrouped || message.isOneToOneConversation) {
|
||||
if (message.isOneToOneConversation) {
|
||||
userAvatar.visibility = View.GONE
|
||||
} else {
|
||||
userAvatar.visibility = View.INVISIBLE
|
||||
}
|
||||
} else {
|
||||
userAvatar.visibility = View.VISIBLE
|
||||
userAvatar.setOnClickListener { v: View ->
|
||||
if (payload is MessagePayload) {
|
||||
(payload as MessagePayload).profileBottomSheet.showFor(
|
||||
message,
|
||||
v.context
|
||||
)
|
||||
}
|
||||
}
|
||||
if (ACTOR_TYPE_BOTS == message.actorType && ACTOR_ID_CHANGELOG == message.actorId) {
|
||||
userAvatar.loadChangelogBotAvatar()
|
||||
} else if (message.actorType == "federated_users") {
|
||||
userAvatar.loadFederatedUserAvatar(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messageCaption.setOnClickListener(null)
|
||||
messageCaption.setOnLongClickListener {
|
||||
previewMessageInterface!!.onPreviewMessageLongClick(message)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun longClickOnReaction(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.onLongClickReactions(chatMessage)
|
||||
}
|
||||
|
||||
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
|
||||
commonMessageInterface.onClickReaction(chatMessage, emoji)
|
||||
}
|
||||
|
||||
private fun openThread(chatMessage: ChatMessage) {
|
||||
commonMessageInterface.openThread(chatMessage)
|
||||
}
|
||||
|
||||
override fun getPayloadForImageLoader(message: ChatMessage?): Any? {
|
||||
if (message!!.selectedIndividualHashMap!!.containsKey(KEY_CONTACT_NAME)) {
|
||||
previewContainer.visibility = View.GONE
|
||||
previewContactContainer.visibility = View.VISIBLE
|
||||
previewContactName.text = message.selectedIndividualHashMap!![KEY_CONTACT_NAME]
|
||||
progressBar = previewContactProgressBar
|
||||
messageText.visibility = View.INVISIBLE
|
||||
clickView = previewContactContainer
|
||||
viewThemeUtils!!.talk.colorContactChatItemBackground(previewContactContainer)
|
||||
viewThemeUtils!!.talk.colorContactChatItemName(previewContactName)
|
||||
viewThemeUtils!!.platform.colorCircularProgressBar(
|
||||
previewContactProgressBar!!,
|
||||
ColorRole.ON_PRIMARY_CONTAINER
|
||||
)
|
||||
|
||||
if (message.selectedIndividualHashMap!!.containsKey(KEY_CONTACT_PHOTO)) {
|
||||
image = previewContactPhoto
|
||||
placeholder = getDrawableFromContactDetails(
|
||||
context,
|
||||
message.selectedIndividualHashMap!![KEY_CONTACT_PHOTO]
|
||||
)
|
||||
} else {
|
||||
image = previewContactPhoto
|
||||
image.setImageDrawable(ContextCompat.getDrawable(context!!, R.drawable.ic_mimetype_text_vcard))
|
||||
}
|
||||
} else {
|
||||
previewContainer.visibility = View.VISIBLE
|
||||
previewContactContainer.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (message.selectedIndividualHashMap!!.containsKey(KEY_MIMETYPE)) {
|
||||
val mimetype = message.selectedIndividualHashMap!![KEY_MIMETYPE]
|
||||
val drawableResourceId = getDrawableResourceIdForMimeType(mimetype)
|
||||
var drawable = ContextCompat.getDrawable(context!!, drawableResourceId)
|
||||
if (drawable != null &&
|
||||
(
|
||||
drawableResourceId == R.drawable.ic_mimetype_folder ||
|
||||
drawableResourceId == R.drawable.ic_mimetype_package_x_generic
|
||||
)
|
||||
) {
|
||||
drawable = viewThemeUtils?.platform?.tintDrawable(context!!, drawable)
|
||||
}
|
||||
placeholder = drawable
|
||||
} else {
|
||||
fetchFileInformation(
|
||||
"/" + message.selectedIndividualHashMap!![KEY_PATH],
|
||||
message.activeUser
|
||||
)
|
||||
}
|
||||
|
||||
return placeholder
|
||||
}
|
||||
|
||||
private fun getDrawableFromContactDetails(context: Context?, base64: String?): Drawable? {
|
||||
var drawable: Drawable? = null
|
||||
if (base64 != "") {
|
||||
val inputStream = ByteArrayInputStream(
|
||||
Base64.decode(base64!!.toByteArray(), Base64.DEFAULT)
|
||||
)
|
||||
drawable = Drawable.createFromResourceStream(
|
||||
context!!.resources,
|
||||
null,
|
||||
inputStream,
|
||||
null,
|
||||
null
|
||||
)
|
||||
try {
|
||||
inputStream.close()
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "failed to close stream in getDrawableFromContactDetails", e)
|
||||
}
|
||||
}
|
||||
if (drawable == null) {
|
||||
drawable = ContextCompat.getDrawable(context!!, R.drawable.ic_mimetype_text_vcard)
|
||||
}
|
||||
return drawable
|
||||
}
|
||||
|
||||
private fun fetchFileInformation(url: String, activeUser: User?) {
|
||||
Single.fromCallable { ReadFilesystemOperation(okHttpClient, activeUser, url, 0) }
|
||||
.observeOn(Schedulers.io())
|
||||
.subscribe(object : SingleObserver<ReadFilesystemOperation> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onSuccess(readFilesystemOperation: ReadFilesystemOperation) {
|
||||
val davResponse = readFilesystemOperation.readRemotePath()
|
||||
if (davResponse.data != null) {
|
||||
val browserFileList = davResponse.data as List<BrowserFile>
|
||||
if (browserFileList.isNotEmpty()) {
|
||||
Handler(context!!.mainLooper).post {
|
||||
val resourceId = getDrawableResourceIdForMimeType(browserFileList[0].mimeType)
|
||||
placeholder = ContextCompat.getDrawable(context!!, resourceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
Log.e(TAG, "Error reading file information", e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
|
||||
this.commonMessageInterface = commonMessageInterface
|
||||
}
|
||||
|
||||
fun assignPreviewMessageInterface(previewMessageInterface: PreviewMessageInterface?) {
|
||||
this.previewMessageInterface = previewMessageInterface
|
||||
}
|
||||
|
||||
fun hasBubbleBackground(message: ChatMessage): Boolean = !message.isVoiceMessage && message.message != "{file}"
|
||||
|
||||
abstract val messageText: EmojiTextView
|
||||
abstract val messageCaption: EmojiTextView
|
||||
abstract val previewContainer: View
|
||||
abstract val previewContactContainer: MaterialCardView
|
||||
abstract val previewContactPhoto: ImageView
|
||||
abstract val previewContactName: EmojiTextView
|
||||
abstract val previewContactProgressBar: ProgressBar?
|
||||
abstract val messageEditIndicator: TextView
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PreviewMsgViewHolder"
|
||||
const val KEY_CONTACT_NAME = "contact-name"
|
||||
const val KEY_CONTACT_PHOTO = "contact-photo"
|
||||
const val KEY_MIMETYPE = "mimetype"
|
||||
const val KEY_ID = "id"
|
||||
const val KEY_PATH = "path"
|
||||
const val ACTOR_TYPE_BOTS = "bots"
|
||||
const val ACTOR_ID_CHANGELOG = "changelog"
|
||||
const val KEY_NAME = "name"
|
||||
const val MIN_IMAGE_HEIGHT = 100F
|
||||
const val HORIZONTAL_REACTION_PADDING = 8.0F
|
||||
}
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.messages
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import ru.f7cloud.talk.chat.data.model.ChatMessage
|
||||
import ru.f7cloud.talk.databinding.ReactionsInsideMessageBinding
|
||||
import ru.f7cloud.talk.ui.theme.ViewThemeUtils
|
||||
import ru.f7cloud.talk.utils.DisplayUtils
|
||||
import com.vanniktech.emoji.EmojiTextView
|
||||
import java.util.Locale
|
||||
|
||||
class Reaction {
|
||||
|
||||
fun showReactions(
|
||||
message: ChatMessage,
|
||||
clickOnReaction: (message: ChatMessage, emoji: String) -> Unit,
|
||||
longClickOnReaction: (message: ChatMessage) -> Unit,
|
||||
binding: ReactionsInsideMessageBinding,
|
||||
context: Context,
|
||||
isOutgoingMessage: Boolean,
|
||||
viewThemeUtils: ViewThemeUtils,
|
||||
isBubbled: Boolean = true
|
||||
) {
|
||||
binding.reactionsEmojiWrapper.removeAllViews()
|
||||
|
||||
if (message.reactions != null && message.reactions!!.isNotEmpty()) {
|
||||
binding.reactionsEmojiWrapper.visibility = View.VISIBLE
|
||||
|
||||
binding.reactionsEmojiWrapper.setOnLongClickListener {
|
||||
longClickOnReaction(message)
|
||||
true
|
||||
}
|
||||
|
||||
val paddingSide = DisplayUtils.convertDpToPixel(EMOJI_AND_AMOUNT_PADDING_SIDE, context).toInt()
|
||||
val paddingTop = DisplayUtils.convertDpToPixel(WRAPPER_PADDING_TOP, context).toInt()
|
||||
val paddingBottom = DisplayUtils.convertDpToPixel(WRAPPER_PADDING_BOTTOM, context).toInt()
|
||||
|
||||
for ((emoji, amount) in message.reactions!!) {
|
||||
val isSelfReaction = message.reactionsSelf != null &&
|
||||
message.reactionsSelf!!.isNotEmpty() &&
|
||||
message.reactionsSelf!!.contains(emoji)
|
||||
val textColor = viewThemeUtils.talk.getTextColor(isOutgoingMessage, isSelfReaction, binding)
|
||||
val emojiWithAmountWrapper = getEmojiWithAmountWrapperLayout(
|
||||
binding.reactionsEmojiWrapper.context,
|
||||
emoji,
|
||||
amount,
|
||||
EmojiWithAmountWrapperLayoutInfo(
|
||||
textColor,
|
||||
paddingSide,
|
||||
paddingTop,
|
||||
paddingBottom,
|
||||
viewThemeUtils,
|
||||
isOutgoingMessage,
|
||||
isSelfReaction
|
||||
),
|
||||
isBubbled
|
||||
)
|
||||
|
||||
emojiWithAmountWrapper.setOnClickListener {
|
||||
clickOnReaction(message, emoji)
|
||||
}
|
||||
emojiWithAmountWrapper.setOnLongClickListener {
|
||||
longClickOnReaction(message)
|
||||
false
|
||||
}
|
||||
|
||||
binding.reactionsEmojiWrapper.addView(emojiWithAmountWrapper)
|
||||
}
|
||||
} else {
|
||||
binding.reactionsEmojiWrapper.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun getEmojiWithAmountWrapperLayout(
|
||||
context: Context,
|
||||
emoji: String,
|
||||
amount: Int,
|
||||
layoutInfo: EmojiWithAmountWrapperLayoutInfo,
|
||||
isBubbled: Boolean
|
||||
): MaterialCardView {
|
||||
val amountParams = getAmountLayoutParams(context)
|
||||
val wrapperParams = getWrapperLayoutParams(context)
|
||||
|
||||
val emojiContainer = MaterialCardView(context)
|
||||
val emojiWithAmountWrapper = LinearLayout(context)
|
||||
emojiWithAmountWrapper.orientation = LinearLayout.HORIZONTAL
|
||||
|
||||
emojiWithAmountWrapper.addView(getEmojiTextView(context, emoji, amountParams))
|
||||
emojiWithAmountWrapper.addView(getReactionCount(context, layoutInfo.textColor, amount, amountParams))
|
||||
emojiWithAmountWrapper.layoutParams = wrapperParams
|
||||
|
||||
layoutInfo.viewThemeUtils.talk.setReactionsBackground(
|
||||
emojiContainer,
|
||||
layoutInfo.isOutgoingMessage,
|
||||
layoutInfo.isSelfReaction,
|
||||
isBubbled
|
||||
)
|
||||
|
||||
emojiWithAmountWrapper.setPaddingRelative(
|
||||
layoutInfo.paddingSide,
|
||||
layoutInfo.paddingTop,
|
||||
layoutInfo.paddingSide,
|
||||
layoutInfo.paddingBottom
|
||||
)
|
||||
|
||||
emojiContainer.addView(emojiWithAmountWrapper)
|
||||
val containerParams = getWrapperLayoutParams(context, REACTION_END_MARGIN)
|
||||
containerParams.marginStart = 0
|
||||
emojiContainer.layoutParams = containerParams
|
||||
emojiContainer.setStrokeWidth(DisplayUtils.convertDpToPixel(EMOJI_CONTAINER_STROKE_WIDTH, context).toInt())
|
||||
|
||||
return emojiContainer
|
||||
}
|
||||
|
||||
private fun getEmojiTextView(
|
||||
context: Context,
|
||||
emoji: String,
|
||||
layoutParams: LinearLayout.LayoutParams
|
||||
): EmojiTextView {
|
||||
val reactionEmoji = EmojiTextView(context)
|
||||
reactionEmoji.text = emoji
|
||||
reactionEmoji.layoutParams = layoutParams
|
||||
return reactionEmoji
|
||||
}
|
||||
|
||||
private fun getReactionCount(
|
||||
context: Context,
|
||||
textColor: Int,
|
||||
amount: Int,
|
||||
layoutParams: LinearLayout.LayoutParams
|
||||
): TextView {
|
||||
val reactionAmount = TextView(context)
|
||||
reactionAmount.setTextColor(textColor)
|
||||
reactionAmount.text = String.format(Locale.getDefault(), "%d", amount)
|
||||
reactionAmount.layoutParams = layoutParams
|
||||
return reactionAmount
|
||||
}
|
||||
|
||||
private fun getWrapperLayoutParams(
|
||||
context: Context,
|
||||
endMarginInDp: Float = EMOJI_END_MARGIN
|
||||
): LinearLayout
|
||||
.LayoutParams {
|
||||
val wrapperParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
wrapperParams.marginEnd = DisplayUtils.convertDpToPixel(endMarginInDp, context).toInt()
|
||||
return wrapperParams
|
||||
}
|
||||
|
||||
private fun getAmountLayoutParams(context: Context): LinearLayout.LayoutParams {
|
||||
val amountParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
amountParams.marginStart = DisplayUtils.convertDpToPixel(AMOUNT_START_MARGIN, context).toInt()
|
||||
return amountParams
|
||||
}
|
||||
|
||||
private data class EmojiWithAmountWrapperLayoutInfo(
|
||||
val textColor: Int,
|
||||
val paddingSide: Int,
|
||||
val paddingTop: Int,
|
||||
val paddingBottom: Int,
|
||||
val viewThemeUtils: ViewThemeUtils,
|
||||
val isOutgoingMessage: Boolean,
|
||||
val isSelfReaction: Boolean
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val AMOUNT_START_MARGIN: Float = 4F
|
||||
const val EMOJI_END_MARGIN: Float = 4F
|
||||
const val REACTION_END_MARGIN: Float = 6F
|
||||
const val EMOJI_AND_AMOUNT_PADDING_SIDE: Float = 4F
|
||||
const val WRAPPER_PADDING_TOP: Float = 2F
|
||||
const val WRAPPER_PADDING_BOTTOM: Float = 3F
|
||||
const val EMOJI_CONTAINER_STROKE_WIDTH: Float = 1.5F
|
||||
}
|
||||
}
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.messages
|
||||
|
||||
import ru.f7cloud.talk.chat.data.model.ChatMessage
|
||||
|
||||
interface SystemMessageInterface {
|
||||
fun expandSystemMessage(chatMessage: ChatMessage)
|
||||
fun collapseSystemMessages()
|
||||
}
|
||||
Vendored
+194
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.messages
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.TextPaint
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.style.ClickableSpan
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.ViewCompat
|
||||
import autodagger.AutoInjector
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication
|
||||
import ru.f7cloud.talk.application.F7cloudTalkApplication.Companion.sharedApplication
|
||||
import ru.f7cloud.talk.chat.data.model.ChatMessage
|
||||
import ru.f7cloud.talk.databinding.ItemSystemMessageBinding
|
||||
import ru.f7cloud.talk.utils.DateUtils
|
||||
import ru.f7cloud.talk.utils.DisplayUtils
|
||||
import ru.f7cloud.talk.utils.database.user.CurrentUserProviderOld
|
||||
import ru.f7cloud.talk.utils.preferences.AppPreferences
|
||||
import com.stfalcon.chatkit.messages.MessageHolders
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(F7cloudTalkApplication::class)
|
||||
class SystemMessageViewHolder(itemView: View) :
|
||||
MessageHolders
|
||||
.IncomingTextMessageViewHolder<ChatMessage>(itemView) {
|
||||
|
||||
private val binding: ItemSystemMessageBinding = ItemSystemMessageBinding.bind(itemView)
|
||||
|
||||
@Inject
|
||||
lateinit var currentUserProvider: CurrentUserProviderOld
|
||||
|
||||
@JvmField
|
||||
@Inject
|
||||
var appPreferences: AppPreferences? = null
|
||||
|
||||
@JvmField
|
||||
@Inject
|
||||
var context: Context? = null
|
||||
|
||||
@JvmField
|
||||
@Inject
|
||||
var dateUtils: DateUtils? = null
|
||||
protected var background: ViewGroup
|
||||
|
||||
lateinit var systemMessageInterface: SystemMessageInterface
|
||||
|
||||
init {
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
background = itemView.findViewById(R.id.container)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
val user = currentUserProvider.currentUser.blockingGet()
|
||||
val resources = itemView.resources
|
||||
val pressedColor: Int = resources.getColor(R.color.bg_message_list_incoming_bubble)
|
||||
val mentionColor: Int = resources.getColor(R.color.textColorMaxContrast)
|
||||
val bubbleDrawable = DisplayUtils.getMessageSelector(
|
||||
resources.getColor(R.color.transparent),
|
||||
resources.getColor(R.color.transparent),
|
||||
pressedColor,
|
||||
R.drawable.shape_grouped_incoming_message
|
||||
)
|
||||
ViewCompat.setBackground(background, bubbleDrawable)
|
||||
binding.messageText.setTextSize(
|
||||
TypedValue.COMPLEX_UNIT_PX,
|
||||
resources.getDimension(R.dimen.chat_system_message_text_size)
|
||||
)
|
||||
var messageString: Spannable = SpannableString(message.text)
|
||||
if (message.messageParameters != null && message.messageParameters!!.size > 0) {
|
||||
for (key in message.messageParameters!!.keys) {
|
||||
val individualMap: Map<String?, String?>? = message.messageParameters!![key]
|
||||
if (individualMap != null && individualMap.containsKey("name")) {
|
||||
var searchText: String? = if ("user" == individualMap["type"] ||
|
||||
"guest" == individualMap["type"] ||
|
||||
"call" == individualMap["type"]
|
||||
) {
|
||||
"@" + individualMap["name"]
|
||||
} else {
|
||||
individualMap["name"]
|
||||
}
|
||||
messageString =
|
||||
DisplayUtils.searchAndColor(
|
||||
messageString,
|
||||
searchText!!,
|
||||
mentionColor,
|
||||
resources.getDimensionPixelSize(R.dimen.chat_system_message_text_size)
|
||||
)
|
||||
if (individualMap["link"] != null) {
|
||||
val displayName = individualMap["name"] ?: ""
|
||||
val link = (user.baseUrl + individualMap["link"])
|
||||
val newStartIndex = messageString.indexOf(displayName)
|
||||
if (newStartIndex != -1) {
|
||||
val clickableSpan = object : ClickableSpan() {
|
||||
override fun onClick(widget: View) {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, link.toUri())
|
||||
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context?.startActivity(browserIntent)
|
||||
}
|
||||
|
||||
override fun updateDrawState(ds: TextPaint) {
|
||||
super.updateDrawState(ds)
|
||||
ds.color = mentionColor
|
||||
ds.isUnderlineText = false
|
||||
}
|
||||
}
|
||||
|
||||
messageString.setSpan(
|
||||
clickableSpan,
|
||||
newStartIndex,
|
||||
newStartIndex + displayName.length,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.systemMessageLayout.visibility = View.VISIBLE
|
||||
binding.similarMessagesHint.visibility = View.GONE
|
||||
if (message.expandableParent) {
|
||||
processExpandableParent(message, messageString)
|
||||
} else if (message.hiddenByCollapse) {
|
||||
binding.systemMessageLayout.visibility = View.GONE
|
||||
} else {
|
||||
binding.expandCollapseIcon.visibility = View.GONE
|
||||
binding.messageText.text = messageString
|
||||
binding.expandCollapseIcon.setImageDrawable(null)
|
||||
binding.systemMessageLayout.setOnClickListener(null)
|
||||
}
|
||||
|
||||
if (!message.expandableParent && message.lastItemOfExpandableGroup != 0) {
|
||||
binding.systemMessageLayout.setOnClickListener { systemMessageInterface.collapseSystemMessages() }
|
||||
binding.messageText.setOnClickListener { systemMessageInterface.collapseSystemMessages() }
|
||||
}
|
||||
|
||||
binding.messageTime.text = dateUtils!!.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
itemView.setTag(R.string.replyable_message_view_tag, message.replyable)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun processExpandableParent(message: ChatMessage, messageString: Spannable) {
|
||||
binding.expandCollapseIcon.visibility = View.VISIBLE
|
||||
|
||||
if (!message.isExpanded) {
|
||||
val similarMessages = sharedApplication!!.resources.getQuantityString(
|
||||
R.plurals.see_similar_system_messages,
|
||||
message.expandableChildrenAmount,
|
||||
message.expandableChildrenAmount
|
||||
)
|
||||
|
||||
binding.messageText.text = messageString
|
||||
binding.messageText.movementMethod = LinkMovementMethod.getInstance()
|
||||
binding.similarMessagesHint.visibility = View.VISIBLE
|
||||
binding.similarMessagesHint.text = similarMessages
|
||||
|
||||
binding.expandCollapseIcon.setImageDrawable(
|
||||
ContextCompat.getDrawable(context!!, R.drawable.baseline_unfold_more_24)
|
||||
)
|
||||
binding.systemMessageLayout.setOnClickListener { systemMessageInterface.expandSystemMessage(message) }
|
||||
binding.messageText.setOnClickListener { systemMessageInterface.expandSystemMessage(message) }
|
||||
} else {
|
||||
binding.messageText.text = messageString
|
||||
binding.similarMessagesHint.visibility = View.GONE
|
||||
binding.similarMessagesHint.text = ""
|
||||
|
||||
binding.expandCollapseIcon.setImageDrawable(
|
||||
ContextCompat.getDrawable(context!!, R.drawable.baseline_unfold_less_24)
|
||||
)
|
||||
binding.systemMessageLayout.setOnClickListener { systemMessageInterface.collapseSystemMessages() }
|
||||
binding.messageText.setOnClickListener { systemMessageInterface.collapseSystemMessages() }
|
||||
}
|
||||
}
|
||||
|
||||
fun assignSystemMessageInterface(systemMessageInterface: SystemMessageInterface) {
|
||||
this.systemMessageInterface = systemMessageInterface
|
||||
}
|
||||
}
|
||||
vendor/talk-android/app/src/main/java/ru/f7cloud/talk/adapters/messages/TalkMessagesListAdapter.java
Vendored
+86
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Christian Reiner <foss@christian-reiner.info>
|
||||
* SPDX-FileCopyrightText: 2020 Tobias Kaminsky <tobias.kaminsky@f7cloud.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.messages;
|
||||
|
||||
import ru.f7cloud.talk.chat.ChatActivity;
|
||||
import com.stfalcon.chatkit.commons.ImageLoader;
|
||||
import com.stfalcon.chatkit.commons.ViewHolder;
|
||||
import com.stfalcon.chatkit.commons.models.IMessage;
|
||||
import com.stfalcon.chatkit.messages.MessageHolders;
|
||||
import com.stfalcon.chatkit.messages.MessagesListAdapter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TalkMessagesListAdapter<M extends IMessage> extends MessagesListAdapter<M> {
|
||||
private final ChatActivity chatActivity;
|
||||
|
||||
public TalkMessagesListAdapter(
|
||||
String senderId,
|
||||
MessageHolders holders,
|
||||
ImageLoader imageLoader,
|
||||
ChatActivity chatActivity) {
|
||||
super(senderId, holders, imageLoader);
|
||||
this.chatActivity = chatActivity;
|
||||
}
|
||||
|
||||
public List<MessagesListAdapter.Wrapper> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ViewHolder holder, int position) {
|
||||
|
||||
if (holder instanceof IncomingTextMessageViewHolder holderInstance) {
|
||||
holderInstance.assignCommonMessageInterface(chatActivity);
|
||||
} else if (holder instanceof OutcomingTextMessageViewHolder holderInstance) {
|
||||
holderInstance.assignCommonMessageInterface(chatActivity);
|
||||
holderInstance.adjustIfNoteToSelf(chatActivity.getCurrentConversation());
|
||||
|
||||
} else if (holder instanceof IncomingLocationMessageViewHolder holderInstance) {
|
||||
holderInstance.assignCommonMessageInterface(chatActivity);
|
||||
} else if (holder instanceof OutcomingLocationMessageViewHolder holderInstance) {
|
||||
holderInstance.assignCommonMessageInterface(chatActivity);
|
||||
holderInstance.adjustIfNoteToSelf(chatActivity.getCurrentConversation());
|
||||
|
||||
} else if (holder instanceof IncomingLinkPreviewMessageViewHolder holderInstance) {
|
||||
holderInstance.assignCommonMessageInterface(chatActivity);
|
||||
} else if (holder instanceof OutcomingLinkPreviewMessageViewHolder holderInstance) {
|
||||
holderInstance.assignCommonMessageInterface(chatActivity);
|
||||
holderInstance.adjustIfNoteToSelf(chatActivity.getCurrentConversation());
|
||||
|
||||
} else if (holder instanceof IncomingVoiceMessageViewHolder holderInstance) {
|
||||
holderInstance.assignVoiceMessageInterface(chatActivity);
|
||||
holderInstance.assignCommonMessageInterface(chatActivity);
|
||||
} else if (holder instanceof OutcomingVoiceMessageViewHolder holderInstance) {
|
||||
holderInstance.assignVoiceMessageInterface(chatActivity);
|
||||
holderInstance.assignCommonMessageInterface(chatActivity);
|
||||
holderInstance.adjustIfNoteToSelf(chatActivity.getCurrentConversation());
|
||||
|
||||
} else if (holder instanceof PreviewMessageViewHolder holderInstance) {
|
||||
holderInstance.assignPreviewMessageInterface(chatActivity);
|
||||
holderInstance.assignCommonMessageInterface(chatActivity);
|
||||
|
||||
} else if (holder instanceof SystemMessageViewHolder holderInstance) {
|
||||
holderInstance.assignSystemMessageInterface(chatActivity);
|
||||
|
||||
} else if (holder instanceof IncomingDeckCardViewHolder holderInstance) {
|
||||
holderInstance.assignCommonMessageInterface(chatActivity);
|
||||
} else if (holder instanceof OutcomingDeckCardViewHolder holderInstance) {
|
||||
holderInstance.assignCommonMessageInterface(chatActivity);
|
||||
holderInstance.adjustIfNoteToSelf(chatActivity.getCurrentConversation());
|
||||
|
||||
} else if (holder instanceof IncomingPollMessageViewHolder holderInstance) {
|
||||
holderInstance.assignCommonMessageInterface(chatActivity);
|
||||
} else if (holder instanceof OutcomingPollMessageViewHolder holderInstance) {
|
||||
holderInstance.assignCommonMessageInterface(chatActivity);
|
||||
holderInstance.adjustIfNoteToSelf(chatActivity.getCurrentConversation());
|
||||
}
|
||||
|
||||
super.onBindViewHolder(holder, position);
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.messages
|
||||
|
||||
import android.view.View
|
||||
import ru.f7cloud.talk.R
|
||||
import ru.f7cloud.talk.chat.ChatActivity
|
||||
import ru.f7cloud.talk.databinding.ReactionsInsideMessageBinding
|
||||
import ru.f7cloud.talk.chat.data.model.ChatMessage
|
||||
import ru.f7cloud.talk.databinding.ItemThreadTitleBinding
|
||||
|
||||
class Thread {
|
||||
|
||||
fun showThreadPreview(
|
||||
chatActivity: ChatActivity,
|
||||
message: ChatMessage,
|
||||
threadBinding: ItemThreadTitleBinding,
|
||||
reactionsBinding: ReactionsInsideMessageBinding,
|
||||
openThread: (message: ChatMessage) -> Unit
|
||||
) {
|
||||
val isFirstMessageOfThreadInNormalChat = chatActivity.conversationThreadId == null && message.isThread
|
||||
if (isFirstMessageOfThreadInNormalChat) {
|
||||
threadBinding.threadTitleLayout.visibility = View.VISIBLE
|
||||
|
||||
threadBinding.threadTitleLayout.findViewById<androidx.emoji2.widget.EmojiTextView>(R.id.threadTitle).text =
|
||||
message.threadTitle
|
||||
|
||||
reactionsBinding.threadButton.visibility = View.VISIBLE
|
||||
|
||||
reactionsBinding.threadButton.setContent {
|
||||
ThreadButtonComposable(
|
||||
message.threadReplies ?: 0,
|
||||
onButtonClick = { openThread(message) }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
threadBinding.threadTitleLayout.visibility = View.GONE
|
||||
reactionsBinding.threadButton.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package ru.f7cloud.talk.adapters.messages
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ru.f7cloud.talk.R
|
||||
|
||||
@Composable
|
||||
fun ThreadButtonComposable(replyAmount: Int = 0, onButtonClick: () -> Unit = {}) {
|
||||
val replyAmountText = if (replyAmount == 0) {
|
||||
stringResource(R.string.thread_reply)
|
||||
} else {
|
||||
pluralStringResource(
|
||||
R.plurals.thread_replies,
|
||||
replyAmount,
|
||||
replyAmount
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
onClick = onButtonClick,
|
||||
modifier = Modifier
|
||||
.padding(0.dp)
|
||||
.height(24.dp),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
border = BorderStroke(1.dp, colorResource(R.color.nc_incoming_text_default)),
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
containerColor = colorResource(R.color.bg_message_list_incoming_bubble),
|
||||
contentColor = colorResource(R.color.nc_incoming_text_default)
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_reply),
|
||||
contentDescription = stringResource(R.string.open_thread),
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.padding(start = 5.dp, end = 2.dp),
|
||||
tint = colorResource(R.color.nc_incoming_text_default)
|
||||
)
|
||||
Text(
|
||||
text = replyAmountText,
|
||||
modifier = Modifier
|
||||
.padding(end = 6.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ThreadButtonPreviewMultipleReplies() {
|
||||
ThreadButtonComposable(2)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ThreadButtonPreviewOneReply() {
|
||||
ThreadButtonComposable(1)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ThreadButtonPreviewZeroReplies() {
|
||||
ThreadButtonComposable(0)
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2017-2019 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.messages;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import ru.f7cloud.talk.chat.data.model.ChatMessage;
|
||||
import com.stfalcon.chatkit.messages.MessageHolders;
|
||||
|
||||
public class UnreadNoticeMessageViewHolder extends MessageHolders.SystemMessageViewHolder<ChatMessage> {
|
||||
|
||||
public UnreadNoticeMessageViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
|
||||
public UnreadNoticeMessageViewHolder(View itemView, Object payload) {
|
||||
super(itemView, payload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void viewDetached() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void viewAttached() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void viewRecycled() {
|
||||
|
||||
}
|
||||
}
|
||||
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Christian Reiner <foss@christian-reiner.info>
|
||||
* SPDX-FileCopyrightText: 2021 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.adapters.messages
|
||||
|
||||
import ru.f7cloud.talk.chat.data.model.ChatMessage
|
||||
import ru.f7cloud.talk.ui.PlaybackSpeed
|
||||
|
||||
interface VoiceMessageInterface {
|
||||
fun updateMediaPlayerProgressBySlider(message: ChatMessage, progress: Int)
|
||||
fun registerMessageToObservePlaybackSpeedPreferences(userId: String, listener: (speed: PlaybackSpeed) -> Unit)
|
||||
}
|
||||
@@ -0,0 +1,643 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2021 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2021 Tim Krüger <t@timkrueger.me>
|
||||
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.api;
|
||||
|
||||
import ru.f7cloud.talk.models.json.capabilities.CapabilitiesOverall;
|
||||
import ru.f7cloud.talk.models.json.capabilities.RoomCapabilitiesOverall;
|
||||
import ru.f7cloud.talk.models.json.chat.ChatOverall;
|
||||
import ru.f7cloud.talk.models.json.chat.ChatOverallSingleMessage;
|
||||
import ru.f7cloud.talk.models.json.chat.ChatShareOverall;
|
||||
import ru.f7cloud.talk.models.json.chat.ChatShareOverviewOverall;
|
||||
import ru.f7cloud.talk.models.json.conversations.RoomOverall;
|
||||
import ru.f7cloud.talk.models.json.conversations.RoomsOverall;
|
||||
import ru.f7cloud.talk.models.json.generic.GenericOverall;
|
||||
import ru.f7cloud.talk.models.json.generic.Status;
|
||||
import ru.f7cloud.talk.models.json.hovercard.HoverCardOverall;
|
||||
import ru.f7cloud.talk.models.json.invitation.InvitationOverall;
|
||||
import ru.f7cloud.talk.models.json.mention.MentionOverall;
|
||||
import ru.f7cloud.talk.models.json.notifications.NotificationOverall;
|
||||
import ru.f7cloud.talk.models.json.opengraph.OpenGraphOverall;
|
||||
import ru.f7cloud.talk.models.json.participants.AddParticipantOverall;
|
||||
import ru.f7cloud.talk.models.json.participants.ParticipantsOverall;
|
||||
import ru.f7cloud.talk.models.json.participants.TalkBanOverall;
|
||||
import ru.f7cloud.talk.models.json.push.PushRegistrationOverall;
|
||||
import ru.f7cloud.talk.models.json.reactions.ReactionsOverall;
|
||||
import ru.f7cloud.talk.models.json.reminder.ReminderOverall;
|
||||
import ru.f7cloud.talk.models.json.search.ContactsByNumberOverall;
|
||||
import ru.f7cloud.talk.models.json.signaling.SignalingOverall;
|
||||
import ru.f7cloud.talk.models.json.signaling.settings.SignalingSettingsOverall;
|
||||
import ru.f7cloud.talk.models.json.status.StatusOverall;
|
||||
import ru.f7cloud.talk.models.json.unifiedsearch.UnifiedSearchOverall;
|
||||
import ru.f7cloud.talk.models.json.userprofile.UserProfileFieldsOverall;
|
||||
import ru.f7cloud.talk.models.json.userprofile.UserProfileOverall;
|
||||
import ru.f7cloud.talk.polls.repositories.model.PollOverall;
|
||||
import ru.f7cloud.talk.translate.repositories.model.LanguagesOverall;
|
||||
import ru.f7cloud.talk.translate.repositories.model.TranslationsOverall;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import io.reactivex.Observable;
|
||||
import kotlin.Unit;
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.ResponseBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Response;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.DELETE;
|
||||
import retrofit2.http.Field;
|
||||
import retrofit2.http.FieldMap;
|
||||
import retrofit2.http.FormUrlEncoded;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.HEAD;
|
||||
import retrofit2.http.Header;
|
||||
import retrofit2.http.Multipart;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.PUT;
|
||||
import retrofit2.http.Part;
|
||||
import retrofit2.http.Query;
|
||||
import retrofit2.http.QueryMap;
|
||||
import retrofit2.http.Url;
|
||||
|
||||
public interface NcApi {
|
||||
|
||||
/*
|
||||
QueryMap items are as follows:
|
||||
- "format" : "json"
|
||||
- "search" : ""
|
||||
- "perPage" : "200"
|
||||
- "itemType" : "call"
|
||||
|
||||
Server URL is: baseUrl + ocsApiVersion + /apps/files_sharing/api/v1/sharees
|
||||
|
||||
or if we're on 14 and up:
|
||||
|
||||
baseUrl + ocsApiVersion + "/core/autocomplete/get");
|
||||
|
||||
*/
|
||||
@GET
|
||||
Observable<ResponseBody> getContactsWithSearchParam(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Nullable @Query("shareTypes[]") List<String> listOfShareTypes,
|
||||
@QueryMap Map<String, Object> options);
|
||||
|
||||
|
||||
/*
|
||||
Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /room
|
||||
*/
|
||||
@GET
|
||||
Observable<RoomsOverall> getRooms(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Nullable @Query("includeStatus") Boolean includeStatus);
|
||||
|
||||
/*
|
||||
Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /room/roomToken
|
||||
*/
|
||||
@GET
|
||||
Observable<RoomOverall> getRoom(@Header("Authorization") String authorization, @Url String url);
|
||||
|
||||
/*
|
||||
QueryMap items are as follows:
|
||||
- "roomType" : ""
|
||||
- "invite" : ""
|
||||
|
||||
Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /room
|
||||
*/
|
||||
|
||||
@POST
|
||||
Observable<RoomOverall> createRoom(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@QueryMap Map<String, String> options);
|
||||
|
||||
/*
|
||||
QueryMap items are as follows:
|
||||
- "newParticipant" : "user"
|
||||
|
||||
Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /room/roomToken/participants
|
||||
*/
|
||||
@POST
|
||||
Observable<AddParticipantOverall> addParticipant(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@QueryMap Map<String,
|
||||
String> options);
|
||||
|
||||
@POST
|
||||
Observable<GenericOverall> resendParticipantInvitations(@Header("Authorization") String authorization,
|
||||
@Url String url);
|
||||
|
||||
// also used for removing a guest from a conversation
|
||||
@Deprecated
|
||||
@DELETE
|
||||
Observable<GenericOverall> removeParticipantFromConversation(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Query("participant") String participantId);
|
||||
|
||||
@DELETE
|
||||
Observable<GenericOverall> removeAttendeeFromConversation(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Query("attendeeId") Long attendeeId);
|
||||
|
||||
@Deprecated
|
||||
@POST
|
||||
Observable<GenericOverall> promoteUserToModerator(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Query("participant") String participantId);
|
||||
|
||||
@Deprecated
|
||||
@DELETE
|
||||
Observable<GenericOverall> demoteModeratorToUser(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Query("participant") String participantId);
|
||||
|
||||
@POST
|
||||
Observable<GenericOverall> promoteAttendeeToModerator(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Query("attendeeId") Long attendeeId);
|
||||
|
||||
@DELETE
|
||||
Observable<GenericOverall> demoteAttendeeFromModerator(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Query("attendeeId") Long attendeeId);
|
||||
|
||||
/*
|
||||
Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /room/roomToken/participants/self
|
||||
*/
|
||||
|
||||
@DELETE
|
||||
Observable<GenericOverall> removeSelfFromRoom(@Header("Authorization") String authorization, @Url String url);
|
||||
|
||||
@DELETE
|
||||
Observable<GenericOverall> deleteRoom(@Header("Authorization") String authorization, @Url String url);
|
||||
|
||||
/*
|
||||
Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /call/callToken
|
||||
*/
|
||||
@GET
|
||||
Observable<ParticipantsOverall> getPeersForCall(@Header("Authorization") String authorization, @Url String url);
|
||||
|
||||
@GET
|
||||
Observable<ParticipantsOverall> getPeersForCall(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@QueryMap Map<String, Boolean> fields);
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
Observable<RoomOverall> joinRoom(@Nullable @Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Nullable @Field("password") String password);
|
||||
|
||||
@DELETE
|
||||
Observable<GenericOverall> leaveRoom(@Nullable @Header("Authorization") String authorization, @Url String url);
|
||||
|
||||
/*
|
||||
Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /call/callToken
|
||||
*/
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
Observable<GenericOverall> joinCall(@Nullable @Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Field("flags") Integer inCall,
|
||||
@Field("silent") Boolean callWithoutNotification,
|
||||
@Nullable @Field("recordingConsent") Boolean recordingConsent);
|
||||
|
||||
/*
|
||||
Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /call/callToken
|
||||
*/
|
||||
@DELETE
|
||||
Observable<GenericOverall> leaveCall(@Nullable @Header("Authorization") String authorization, @Url String url,
|
||||
@Nullable @Query("all") Boolean all);
|
||||
|
||||
@GET
|
||||
Observable<SignalingSettingsOverall> getSignalingSettings(@Nullable @Header("Authorization") String authorization,
|
||||
@Url String url);
|
||||
|
||||
/*
|
||||
QueryMap items are as follows:
|
||||
- "messages" : "message"
|
||||
|
||||
Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /signaling
|
||||
*/
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
Observable<SignalingOverall> sendSignalingMessages(@Nullable @Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Field("messages") String messages);
|
||||
|
||||
/*
|
||||
Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /signaling
|
||||
*/
|
||||
@GET
|
||||
Observable<SignalingOverall> pullSignalingMessages(@Nullable @Header("Authorization") String authorization,
|
||||
@Url String url);
|
||||
|
||||
/*
|
||||
QueryMap items are as follows:
|
||||
- "format" : "json"
|
||||
|
||||
Server URL is: baseUrl + ocsApiVersion + "/cloud/user"
|
||||
*/
|
||||
|
||||
@GET
|
||||
Observable<UserProfileOverall> getUserProfile(@Header("Authorization") String authorization, @Url String url);
|
||||
|
||||
|
||||
@GET
|
||||
Observable<UserProfileOverall> getUserData(@Header("Authorization") String authorization, @Url String url);
|
||||
|
||||
@DELETE
|
||||
Observable<GenericOverall> revertStatus(@Header("Authentication") String authorization, @Url String url);
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT
|
||||
Observable<GenericOverall> setUserData(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Field("key") String key,
|
||||
@Field("value") String value);
|
||||
|
||||
|
||||
/*
|
||||
Server URL is: baseUrl + /status.php
|
||||
*/
|
||||
@GET
|
||||
Observable<Status> getServerStatus(@Url String url);
|
||||
|
||||
|
||||
/*
|
||||
QueryMap items are as follows:
|
||||
- "format" : "json"
|
||||
- "pushTokenHash" : ""
|
||||
- "devicePublicKey" : ""
|
||||
- "proxyServer" : ""
|
||||
|
||||
Server URL is: baseUrl + ocsApiVersion + "/apps/notifications/api/v2/push
|
||||
*/
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
Observable<PushRegistrationOverall> registerDeviceForNotificationsWithF7cloud(
|
||||
@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@FieldMap Map<String, String> options);
|
||||
|
||||
@DELETE
|
||||
Observable<GenericOverall> unregisterDeviceForNotificationsWithF7cloud(
|
||||
@Header("Authorization") String authorization,
|
||||
@Url String url);
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
Observable<Unit> registerDeviceForNotificationsWithPushProxy(@Url String url,
|
||||
@FieldMap Map<String, String> fields);
|
||||
|
||||
|
||||
/*
|
||||
QueryMap items are as follows:
|
||||
- "deviceIdentifier": "{{deviceIdentifier}}",
|
||||
- "deviceIdentifierSignature": "{{signature}}",
|
||||
- "userPublicKey": "{{userPublicKey}}"
|
||||
*/
|
||||
@DELETE
|
||||
Observable<Void> unregisterDeviceForNotificationsWithProxy(@Url String url,
|
||||
@QueryMap Map<String, String> fields);
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT
|
||||
Observable<Response<GenericOverall>> setPassword2(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Field("password") String password);
|
||||
|
||||
@GET
|
||||
Observable<CapabilitiesOverall> getCapabilities(@Header("Authorization") String authorization, @Url String url);
|
||||
|
||||
@GET
|
||||
Observable<CapabilitiesOverall> getCapabilities(@Url String url);
|
||||
|
||||
@GET
|
||||
Observable<RoomCapabilitiesOverall> getRoomCapabilities(@Header("Authorization") String authorization,
|
||||
@Url String url);
|
||||
|
||||
/*
|
||||
QueryMap items are as follows:
|
||||
- "lookIntoFuture": int (0 or 1),
|
||||
- "limit" : int, range 100-200,
|
||||
- "timeout": used with look into future, 30 default, 60 at most
|
||||
- "lastKnownMessageId", int, use one from X-Chat-Last-Given
|
||||
*/
|
||||
@GET
|
||||
Observable<Response<ChatOverall>> pullChatMessages(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@QueryMap Map<String, Integer> fields);
|
||||
|
||||
/*
|
||||
Fieldmap items are as follows:
|
||||
- "message": ,
|
||||
- "actorDisplayName"
|
||||
*/
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
Observable<ChatOverallSingleMessage> sendChatMessage(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Field("message") CharSequence message,
|
||||
@Field("actorDisplayName") String actorDisplayName,
|
||||
@Field("replyTo") Integer replyTo,
|
||||
@Field("silent") Boolean sendWithoutNotification,
|
||||
@Field("referenceId") String referenceId
|
||||
);
|
||||
|
||||
@GET
|
||||
Observable<Response<ChatShareOverall>> getSharedItems(
|
||||
@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Query("objectType") String objectType,
|
||||
@Nullable @Query("lastKnownMessageId") Integer lastKnownMessageId,
|
||||
@Nullable @Query("limit") Integer limit);
|
||||
|
||||
@GET
|
||||
Observable<Response<ChatShareOverviewOverall>> getSharedItemsOverview(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Nullable @Query("limit") Integer limit);
|
||||
|
||||
|
||||
@GET
|
||||
Observable<MentionOverall> getMentionAutocompleteSuggestions(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Query("search") String query,
|
||||
@Nullable @Query("limit") Integer limit,
|
||||
@QueryMap Map<String, String> fields);
|
||||
|
||||
@GET
|
||||
Observable<NotificationOverall> getNcNotification(@Header("Authorization") String authorization,
|
||||
@Url String url);
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
Observable<GenericOverall> setNotificationLevel(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Field("level") int level);
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT
|
||||
Observable<GenericOverall> setConversationReadOnly(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Field("state") int state);
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
Observable<GenericOverall> createRemoteShare(@Nullable @Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Field("path") String remotePath,
|
||||
@Field("shareWith") String roomToken,
|
||||
@Field("shareType") String shareType,
|
||||
@Field("talkMetaData") String talkMetaData);
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT
|
||||
Observable<GenericOverall> setLobbyForConversation(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Field("state") Integer state,
|
||||
@Field("timer") Long timer);
|
||||
|
||||
@POST
|
||||
Observable<ContactsByNumberOverall> searchContactsByPhoneNumber(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Body RequestBody search);
|
||||
|
||||
@PUT
|
||||
Observable<Response<GenericOverall>> uploadFile(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Body RequestBody body);
|
||||
|
||||
@HEAD
|
||||
Observable<Response<Void>> checkIfFileExists(@Header("Authorization") String authorization,
|
||||
@Url String url);
|
||||
|
||||
@GET
|
||||
Call<ResponseBody> downloadFile(@Header("Authorization") String authorization,
|
||||
@Url String url);
|
||||
|
||||
@DELETE
|
||||
Observable<ChatOverallSingleMessage> deleteChatMessage(@Header("Authorization") String authorization,
|
||||
@Url String url);
|
||||
|
||||
@DELETE
|
||||
Observable<GenericOverall> deleteAvatar(@Header("Authorization") String authorization, @Url String url);
|
||||
|
||||
@DELETE
|
||||
Observable<RoomOverall> deleteConversationAvatar(@Header("Authorization") String authorization, @Url String url);
|
||||
|
||||
|
||||
@Multipart
|
||||
@POST
|
||||
Observable<GenericOverall> uploadAvatar(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Part MultipartBody.Part attachment);
|
||||
|
||||
@Multipart
|
||||
@POST
|
||||
Observable<RoomOverall> uploadConversationAvatar(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Part MultipartBody.Part attachment);
|
||||
|
||||
@GET
|
||||
Observable<UserProfileFieldsOverall> getEditableUserProfileFields(@Header("Authorization") String authorization,
|
||||
@Url String url);
|
||||
|
||||
@GET
|
||||
Call<ResponseBody> downloadResizedImage(@Header("Authorization") String authorization,
|
||||
@Url String url);
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
Observable<GenericOverall> sendLocation(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Field("objectType") String objectType,
|
||||
@Field("objectId") String objectId,
|
||||
@Field("metaData") String metaData);
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
Observable<GenericOverall> notificationCalls(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Field("level") Integer level);
|
||||
|
||||
@DELETE
|
||||
Observable<GenericOverall> clearChatHistory(@Header("Authorization") String authorization, @Url String url);
|
||||
|
||||
@GET
|
||||
Observable<HoverCardOverall> hoverCard(@Header("Authorization") String authorization, @Url String url);
|
||||
|
||||
// Url is: /api/{apiVersion}/chat/{token}/read
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
Observable<GenericOverall> setChatReadMarker(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Nullable @Field("lastReadMessage") Integer lastReadMessage);
|
||||
|
||||
// Url is: /api/{apiVersion}/chat/{token}/read
|
||||
@DELETE
|
||||
Observable<GenericOverall> markRoomAsUnread(@Header("Authorization") String authorization, @Url String url);
|
||||
|
||||
@GET
|
||||
Observable<StatusOverall> status(@Header("Authorization") String authorization, @Url String url);
|
||||
|
||||
@GET
|
||||
Observable<StatusOverall> backupStatus(@Header("Authorization") String authorization, @Url String url);
|
||||
|
||||
@GET
|
||||
Observable<ResponseBody> getPredefinedStatuses(@Header("Authorization") String authorization, @Url String url);
|
||||
|
||||
@DELETE
|
||||
Observable<GenericOverall> statusDeleteMessage(@Header("Authorization") String authorization, @Url String url);
|
||||
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT
|
||||
Observable<GenericOverall> setPredefinedStatusMessage(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Field("messageId") String selectedPredefinedMessageId,
|
||||
@Field("clearAt") Long clearAt);
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT
|
||||
Observable<GenericOverall> setCustomStatusMessage(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Field("statusIcon") String statusIcon,
|
||||
@Field("message") String message,
|
||||
@Field("clearAt") Long clearAt);
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT
|
||||
Observable<GenericOverall> setStatusType(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Field("statusType") String statusType);
|
||||
|
||||
|
||||
@POST
|
||||
Observable<GenericOverall> sendReaction(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Query("reaction") String reaction);
|
||||
|
||||
@DELETE
|
||||
Observable<GenericOverall> deleteReaction(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Query("reaction") String reaction);
|
||||
|
||||
@GET
|
||||
Observable<ReactionsOverall> getReactions(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Query("reaction") String reaction);
|
||||
|
||||
@GET
|
||||
Observable<UnifiedSearchOverall> performUnifiedSearch(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Query("term") String term,
|
||||
@Query("from") String fromUrl,
|
||||
@Query("limit") Integer limit,
|
||||
@Query("cursor") Integer cursor);
|
||||
|
||||
@GET
|
||||
Observable<PollOverall> getPoll(@Header("Authorization") String authorization,
|
||||
@Url String url);
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
Observable<PollOverall> createPoll(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Query("question") String question,
|
||||
@Field("options[]") List<String> options,
|
||||
@Query("resultMode") Integer resultMode,
|
||||
@Query("maxVotes") Integer maxVotes);
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
Observable<PollOverall> votePoll(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Field("optionIds[]") List<Integer> optionIds);
|
||||
|
||||
@DELETE
|
||||
Observable<PollOverall> closePoll(@Header("Authorization") String authorization,
|
||||
@Url String url);
|
||||
|
||||
@GET
|
||||
Observable<OpenGraphOverall> getOpenGraph(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Query("reference") String urlToFindPreviewFor);
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
Observable<GenericOverall> startRecording(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Field("status") Integer status);
|
||||
|
||||
@DELETE
|
||||
Observable<GenericOverall> stopRecording(@Header("Authorization") String authorization,
|
||||
@Url String url);
|
||||
|
||||
@POST
|
||||
Observable<GenericOverall> requestAssistance(@Header("Authorization") String authorization,
|
||||
@Url String url);
|
||||
|
||||
@DELETE
|
||||
Observable<GenericOverall> withdrawRequestAssistance(@Header("Authorization") String authorization,
|
||||
@Url String url);
|
||||
|
||||
@POST
|
||||
Observable<GenericOverall> sendCommonPostRequest(@Header("Authorization") String authorization, @Url String url);
|
||||
|
||||
@DELETE
|
||||
Observable<GenericOverall> sendCommonDeleteRequest(@Header("Authorization") String authorization, @Url String url);
|
||||
|
||||
|
||||
@POST
|
||||
Observable<TranslationsOverall> translateMessage(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Query("text") String text,
|
||||
@Query("toLanguage") String toLanguage,
|
||||
@Nullable @Query("fromLanguage") String fromLanguage);
|
||||
|
||||
@GET
|
||||
Observable<LanguagesOverall> getLanguages(@Header("Authorization") String authorization,
|
||||
@Url String url);
|
||||
|
||||
@GET
|
||||
Observable<ReminderOverall> getReminder(@Header("Authorization") String authorization,
|
||||
@Url String url);
|
||||
|
||||
@DELETE
|
||||
Observable<GenericOverall> deleteReminder(@Header("Authorization") String authorization,
|
||||
@Url String url);
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
Observable<ReminderOverall> setReminder(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Field("timestamp") int timestamp);
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT
|
||||
Observable<GenericOverall> setRecordingConsent(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Field("recordingConsent") int recordingConsent);
|
||||
|
||||
@GET
|
||||
Observable<InvitationOverall> getInvitations(@Header("Authorization") String authorization,
|
||||
@Url String url);
|
||||
|
||||
@POST
|
||||
Observable<GenericOverall> acceptInvitation(@Header("Authorization") String authorization,
|
||||
@Url String url);
|
||||
|
||||
@DELETE
|
||||
Observable<GenericOverall> rejectInvitation(@Header("Authorization") String authorization,
|
||||
@Url String url);
|
||||
}
|
||||
+370
@@ -0,0 +1,370 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
|
||||
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package ru.f7cloud.talk.api
|
||||
|
||||
import ru.f7cloud.talk.conversationinfo.CreateRoomRequest
|
||||
import ru.f7cloud.talk.models.json.autocomplete.AutocompleteOverall
|
||||
import ru.f7cloud.talk.models.json.chat.ChatOverall
|
||||
import ru.f7cloud.talk.models.json.chat.ChatOverallSingleMessage
|
||||
import ru.f7cloud.talk.models.json.conversations.RoomOverall
|
||||
import ru.f7cloud.talk.models.json.conversations.RoomsOverall
|
||||
import ru.f7cloud.talk.models.json.generic.GenericOverall
|
||||
import ru.f7cloud.talk.models.json.invitation.InvitationOverall
|
||||
import ru.f7cloud.talk.models.json.participants.AddParticipantOverall
|
||||
import ru.f7cloud.talk.models.json.participants.TalkBan
|
||||
import ru.f7cloud.talk.models.json.participants.TalkBanOverall
|
||||
import ru.f7cloud.talk.models.json.profile.ProfileOverall
|
||||
import ru.f7cloud.talk.models.json.status.StatusOverall
|
||||
import ru.f7cloud.talk.models.json.testNotification.TestNotificationOverall
|
||||
import ru.f7cloud.talk.models.json.threads.ThreadOverall
|
||||
import ru.f7cloud.talk.models.json.threads.ThreadsOverall
|
||||
import ru.f7cloud.talk.models.json.userAbsence.UserAbsenceOverall
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.DELETE
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.Multipart
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.PUT
|
||||
import retrofit2.http.Part
|
||||
import retrofit2.http.Query
|
||||
import retrofit2.http.QueryMap
|
||||
import retrofit2.http.Url
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
interface NcApiCoroutines {
|
||||
@GET
|
||||
@JvmSuppressWildcards
|
||||
suspend fun getContactsWithSearchParam(
|
||||
@Header("Authorization") authorization: String?,
|
||||
@Url url: String?,
|
||||
@Query("shareTypes[]") listOfShareTypes: List<String>?,
|
||||
@QueryMap options: Map<String, Any>?
|
||||
): AutocompleteOverall
|
||||
|
||||
/*
|
||||
QueryMap items are as follows:
|
||||
- "roomType" : ""
|
||||
- "invite" : ""
|
||||
|
||||
Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /room
|
||||
*/
|
||||
@POST
|
||||
suspend fun createRoom(
|
||||
@Header("Authorization") authorization: String?,
|
||||
@Url url: String?,
|
||||
@QueryMap options: Map<String, String>?
|
||||
): RoomOverall
|
||||
|
||||
@POST
|
||||
suspend fun createRoomWithBody(
|
||||
@Header("Authorization") authorization: String?,
|
||||
@Url url: String?,
|
||||
@Body roomRequest: CreateRoomRequest
|
||||
): RoomOverall
|
||||
|
||||
/*
|
||||
QueryMap items are as follows:
|
||||
- "roomName" : "newName"
|
||||
|
||||
Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /room/roomToken
|
||||
*/
|
||||
@FormUrlEncoded
|
||||
@PUT
|
||||
suspend fun renameRoom(
|
||||
@Header("Authorization") authorization: String?,
|
||||
@Url url: String,
|
||||
@Field("roomName") roomName: String?
|
||||
): GenericOverall
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT
|
||||
suspend fun openConversation(
|
||||
@Header("Authorization") authorization: String?,
|
||||
@Url url: String,
|
||||
@Field("scope") scope: Int
|
||||
): GenericOverall
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT
|
||||
suspend fun setConversationDescription(
|
||||
@Header("Authorization") authorization: String?,
|
||||
@Url url: String,
|
||||
@Field("description") description: String?
|
||||
): GenericOverall
|
||||
|
||||
@POST
|
||||
suspend fun addParticipant(
|
||||
@Header("Authorization") authorization: String?,
|
||||
@Url url: String?,
|
||||
@QueryMap options: Map<String, String>?
|
||||
): AddParticipantOverall
|
||||
|
||||
@POST
|
||||
suspend fun makeRoomPublic(@Header("Authorization") authorization: String, @Url url: String): GenericOverall
|
||||
|
||||
@DELETE
|
||||
suspend fun makeRoomPrivate(@Header("Authorization") authorization: String, @Url url: String): GenericOverall
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT
|
||||
suspend fun setPassword(
|
||||
@Header("Authorization") authorization: String?,
|
||||
@Url url: String?,
|
||||
@Field("password") password: String?
|
||||
): GenericOverall
|
||||
|
||||
@Multipart
|
||||
@POST
|
||||
suspend fun uploadConversationAvatar(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Url url: String,
|
||||
@Part attachment: MultipartBody.Part
|
||||
): RoomOverall
|
||||
|
||||
@DELETE
|
||||
suspend fun deleteConversationAvatar(@Header("Authorization") authorization: String, @Url url: String): RoomOverall
|
||||
|
||||
@POST
|
||||
suspend fun archiveConversation(@Header("Authorization") authorization: String, @Url url: String): GenericOverall
|
||||
|
||||
@DELETE
|
||||
suspend fun unarchiveConversation(@Header("Authorization") authorization: String, @Url url: String): GenericOverall
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
suspend fun sendChatMessage(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Url url: String,
|
||||
@Field("message") message: String,
|
||||
@Field("actorDisplayName") actorDisplayName: String,
|
||||
@Field("replyTo") replyTo: Int,
|
||||
@Field("silent") sendWithoutNotification: Boolean,
|
||||
@Field("referenceId") referenceId: String,
|
||||
@Field("threadTitle") threadTitle: String?
|
||||
): ChatOverallSingleMessage
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT
|
||||
suspend fun editChatMessage(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Url url: String,
|
||||
@Field("message") message: String
|
||||
): ChatOverallSingleMessage
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
suspend fun banActor(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Url url: String,
|
||||
@Field("actorType") actorType: String,
|
||||
@Field("actorId") actorId: String,
|
||||
@Field("internalNote") internalNote: String
|
||||
): TalkBan
|
||||
|
||||
@GET
|
||||
suspend fun listBans(@Header("Authorization") authorization: String, @Url url: String): TalkBanOverall
|
||||
|
||||
@DELETE
|
||||
suspend fun unbanActor(@Header("Authorization") authorization: String, @Url url: String): GenericOverall
|
||||
|
||||
@POST
|
||||
suspend fun addConversationToFavorites(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Url url: String
|
||||
): GenericOverall
|
||||
|
||||
@POST
|
||||
suspend fun markConversationAsImportant(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Url url: String
|
||||
): GenericOverall
|
||||
|
||||
@DELETE
|
||||
suspend fun markConversationAsUnimportant(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Url url: String
|
||||
): GenericOverall
|
||||
|
||||
@DELETE
|
||||
suspend fun removeConversationFromFavorites(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Url url: String
|
||||
): GenericOverall
|
||||
|
||||
@POST
|
||||
suspend fun markConversationAsSensitive(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Url url: String
|
||||
): GenericOverall
|
||||
|
||||
@DELETE
|
||||
suspend fun markConversationAsInsensitive(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Url url: String
|
||||
): GenericOverall
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
suspend fun notificationCalls(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Url url: String,
|
||||
@Field("level") level: Int
|
||||
): GenericOverall
|
||||
|
||||
@POST
|
||||
suspend fun setReadStatusPrivacy(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Url url: String,
|
||||
@Body body: RequestBody
|
||||
): GenericOverall
|
||||
|
||||
@POST
|
||||
suspend fun setTypingStatusPrivacy(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Url url: String,
|
||||
@Body body: RequestBody
|
||||
): GenericOverall
|
||||
|
||||
@DELETE
|
||||
suspend fun clearChatHistory(@Header("Authorization") authorization: String, @Url url: String): GenericOverall
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT
|
||||
suspend fun setConversationReadOnly(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Url url: String,
|
||||
@Field("state") state: Int
|
||||
): GenericOverall
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
suspend fun setNotificationLevel(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Url url: String,
|
||||
@Field("level") level: Int
|
||||
): GenericOverall
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
suspend fun setMessageExpiration(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Url url: String,
|
||||
@Field("seconds") seconds: Int
|
||||
): GenericOverall
|
||||
|
||||
@GET
|
||||
suspend fun getOutOfOfficeStatusForUser(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Url url: String
|
||||
): UserAbsenceOverall
|
||||
|
||||
@POST
|
||||
suspend fun testPushNotifications(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Url url: String
|
||||
): TestNotificationOverall
|
||||
|
||||
@GET
|
||||
suspend fun getContextOfChatMessage(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Url url: String,
|
||||
@Query("limit") limit: Int,
|
||||
@Query("threadId") threadId: Int?
|
||||
): ChatOverall
|
||||
|
||||
@GET
|
||||
suspend fun getNoteToSelfRoom(@Header("Authorization") authorization: String, @Url url: String): RoomOverall
|
||||
|
||||
@GET
|
||||
suspend fun getProfile(@Header("Authorization") authorization: String, @Url url: String): ProfileOverall
|
||||
|
||||
@DELETE
|
||||
suspend fun unbindRoom(@Header("Authorization") authorization: String, @Url url: String): GenericOverall
|
||||
|
||||
@GET
|
||||
suspend fun getThreads(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Url url: String,
|
||||
@Query("limit") limit: Int?
|
||||
): ThreadsOverall
|
||||
|
||||
@GET
|
||||
suspend fun getThread(@Header("Authorization") authorization: String, @Url url: String): ThreadOverall
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
suspend fun setThreadNotificationLevel(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Url url: String,
|
||||
@Field("level") level: Int
|
||||
): ThreadOverall
|
||||
|
||||
@GET
|
||||
suspend fun getOpenConversations(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Url url: String,
|
||||
@Query("searchTerm") searchTerm: String?
|
||||
): RoomsOverall
|
||||
|
||||
@GET
|
||||
suspend fun getInvitations(@Header("Authorization") authorization: String, @Url url: String): InvitationOverall
|
||||
|
||||
@GET
|
||||
suspend fun status(@Header("Authorization") authorization: String, @Url url: String): StatusOverall
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
suspend fun pinMessage(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Url url: String,
|
||||
@Field("pinUntil") pinUntil: Int
|
||||
): ChatOverallSingleMessage
|
||||
|
||||
@DELETE
|
||||
suspend fun unPinMessage(@Header("Authorization") authorization: String, @Url url: String): ChatOverallSingleMessage
|
||||
|
||||
@DELETE
|
||||
suspend fun hidePinnedMessage(@Header("Authorization") authorization: String, @Url url: String): GenericOverall
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
@Suppress("LongParameterList")
|
||||
suspend fun sendScheduleChatMessage(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Url url: String,
|
||||
@Field("message") message: String,
|
||||
@Field("replyTo") replyTo: Int?,
|
||||
@Field("silent") sendWithoutNotification: Boolean,
|
||||
@Field("threadTitle") threadTitle: String?,
|
||||
@Field("threadId") threadId: Long?,
|
||||
@Field("sendAt") sendAt: Int?
|
||||
): ChatOverallSingleMessage
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
suspend fun updateScheduledMessage(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Url url: String,
|
||||
@Field("message") message: String,
|
||||
@Field("sendAt") sendAt: Int?,
|
||||
@Field("silent") sendWithoutNotification: Boolean
|
||||
): ChatOverallSingleMessage
|
||||
|
||||
@DELETE
|
||||
suspend fun deleteScheduleMessage(@Header("Authorization") authorization: String, @Url url: String): GenericOverall
|
||||
|
||||
@GET
|
||||
suspend fun getScheduledMessage(@Header("Authorization") authorization: String, @Url url: String): ChatOverall
|
||||
}
|
||||
Vendored
+275
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2022 Tim Krüger <t@timkrueger.me>
|
||||
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.application
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Build.VERSION_CODES.P
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.emoji2.bundled.BundledEmojiCompatConfig
|
||||
import androidx.emoji2.text.EmojiCompat
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.multidex.MultiDex
|
||||
import androidx.multidex.MultiDexApplication
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.PeriodicWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import autodagger.AutoComponent
|
||||
import autodagger.AutoInjector
|
||||
import coil.Coil
|
||||
import coil.ImageLoader
|
||||
import coil.decode.GifDecoder
|
||||
import coil.decode.ImageDecoderDecoder
|
||||
import coil.decode.SvgDecoder
|
||||
import coil.memory.MemoryCache
|
||||
import coil.util.DebugLogger
|
||||
import ru.f7cloud.talk.BuildConfig
|
||||
import ru.f7cloud.talk.account.AccountVerificationActivity
|
||||
import ru.f7cloud.talk.account.BrowserLoginActivity
|
||||
import ru.f7cloud.talk.dagger.modules.BusModule
|
||||
import ru.f7cloud.talk.dagger.modules.ContextModule
|
||||
import ru.f7cloud.talk.dagger.modules.DaosModule
|
||||
import ru.f7cloud.talk.dagger.modules.DatabaseModule
|
||||
import ru.f7cloud.talk.dagger.modules.ManagerModule
|
||||
import ru.f7cloud.talk.dagger.modules.RepositoryModule
|
||||
import ru.f7cloud.talk.dagger.modules.RestModule
|
||||
import ru.f7cloud.talk.dagger.modules.UtilsModule
|
||||
import ru.f7cloud.talk.dagger.modules.ViewModelModule
|
||||
import ru.f7cloud.talk.filebrowser.webdav.DavUtils
|
||||
import ru.f7cloud.talk.jobs.AccountRemovalWorker
|
||||
import ru.f7cloud.talk.jobs.CapabilitiesWorker
|
||||
import ru.f7cloud.talk.jobs.SignalingSettingsWorker
|
||||
import ru.f7cloud.talk.jobs.WebsocketConnectionsWorker
|
||||
import ru.f7cloud.talk.ui.theme.ThemeModule
|
||||
import ru.f7cloud.talk.utils.ClosedInterfaceImpl
|
||||
import ru.f7cloud.talk.utils.DeviceUtils
|
||||
import ru.f7cloud.talk.utils.NotificationUtils
|
||||
import ru.f7cloud.talk.utils.database.arbitrarystorage.ArbitraryStorageModule
|
||||
import ru.f7cloud.talk.utils.database.user.UserModule
|
||||
import ru.f7cloud.talk.utils.preferences.AppPreferences
|
||||
import com.vanniktech.emoji.EmojiManager
|
||||
import com.vanniktech.emoji.google.GoogleEmojiProvider
|
||||
import de.cotech.hw.SecurityKeyManager
|
||||
import de.cotech.hw.SecurityKeyManagerConfig
|
||||
import okhttp3.OkHttpClient
|
||||
import org.conscrypt.Conscrypt
|
||||
import org.webrtc.PeerConnectionFactory
|
||||
import java.security.Security
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@AutoComponent(
|
||||
modules = [
|
||||
BusModule::class,
|
||||
ContextModule::class,
|
||||
DatabaseModule::class,
|
||||
RestModule::class,
|
||||
UserModule::class,
|
||||
ArbitraryStorageModule::class,
|
||||
ViewModelModule::class,
|
||||
RepositoryModule::class,
|
||||
UtilsModule::class,
|
||||
ThemeModule::class,
|
||||
ManagerModule::class,
|
||||
DaosModule::class
|
||||
]
|
||||
)
|
||||
@Singleton
|
||||
@AutoInjector(F7cloudTalkApplication::class)
|
||||
open class F7cloudTalkApplication :
|
||||
MultiDexApplication(),
|
||||
LifecycleObserver {
|
||||
//region Fields (components)
|
||||
lateinit var componentApplication: F7cloudTalkApplicationComponent
|
||||
private set
|
||||
//endregion
|
||||
|
||||
//region Getters
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
@Inject
|
||||
lateinit var okHttpClient: OkHttpClient
|
||||
//endregion
|
||||
|
||||
//region private methods
|
||||
private fun initializeWebRtc() {
|
||||
try {
|
||||
PeerConnectionFactory.initialize(
|
||||
PeerConnectionFactory.InitializationOptions.builder(this)
|
||||
.createInitializationOptions()
|
||||
)
|
||||
} catch (e: UnsatisfiedLinkError) {
|
||||
Log.w(TAG, e)
|
||||
}
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
//region Overridden methods
|
||||
override fun onCreate() {
|
||||
Log.d(TAG, "onCreate")
|
||||
sharedApplication = this
|
||||
|
||||
val securityKeyManager = SecurityKeyManager.getInstance()
|
||||
val securityKeyConfigBuilder = SecurityKeyManagerConfig.Builder()
|
||||
.setEnableDebugLogging(BuildConfig.DEBUG)
|
||||
|
||||
try {
|
||||
val packageManager = packageManager
|
||||
val activities = packageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES).activities
|
||||
activities?.forEach { activityInfo ->
|
||||
try {
|
||||
val activityClass = Class.forName(activityInfo.name)
|
||||
if (activityClass != AccountVerificationActivity::class.java &&
|
||||
activityClass != BrowserLoginActivity::class.java
|
||||
) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
securityKeyConfigBuilder.addExcludedActivityClass(activityClass as Class<out Activity>)
|
||||
}
|
||||
} catch (exception: ClassNotFoundException) {
|
||||
Log.e(TAG, "Couldn't disable activity for security key listener", exception)
|
||||
}
|
||||
}
|
||||
} catch (exception: PackageManager.NameNotFoundException) {
|
||||
Log.e(TAG, "Couldn't disable activities for security key listener", exception)
|
||||
}
|
||||
|
||||
securityKeyManager.init(this, securityKeyConfigBuilder.build())
|
||||
initializeWebRtc()
|
||||
buildComponent()
|
||||
DavUtils.registerCustomFactories()
|
||||
|
||||
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
||||
|
||||
componentApplication.inject(this)
|
||||
|
||||
Coil.setImageLoader(buildDefaultImageLoader())
|
||||
setAppTheme(appPreferences.theme)
|
||||
super.onCreate()
|
||||
|
||||
ClosedInterfaceImpl().providerInstallerInstallIfNeededAsync()
|
||||
DeviceUtils.ignoreSpecialBatteryFeatures()
|
||||
|
||||
initWorkers()
|
||||
|
||||
val config = BundledEmojiCompatConfig(this)
|
||||
config.setReplaceAll(true)
|
||||
val emojiCompat = EmojiCompat.init(config)
|
||||
|
||||
EmojiManager.install(GoogleEmojiProvider())
|
||||
|
||||
NotificationUtils.registerNotificationChannels(applicationContext, appPreferences)
|
||||
}
|
||||
|
||||
private fun initWorkers() {
|
||||
val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build()
|
||||
val capabilitiesUpdateWork = OneTimeWorkRequest.Builder(CapabilitiesWorker::class.java).build()
|
||||
val signalingSettingsWork = OneTimeWorkRequest.Builder(SignalingSettingsWorker::class.java).build()
|
||||
val websocketConnectionsWorker = OneTimeWorkRequest.Builder(WebsocketConnectionsWorker::class.java).build()
|
||||
|
||||
WorkManager.getInstance(applicationContext)
|
||||
.beginWith(accountRemovalWork)
|
||||
.then(capabilitiesUpdateWork)
|
||||
.then(signalingSettingsWork)
|
||||
.then(websocketConnectionsWorker)
|
||||
.enqueue()
|
||||
|
||||
val periodicCapabilitiesUpdateWork = PeriodicWorkRequest.Builder(
|
||||
CapabilitiesWorker::class.java,
|
||||
HALF_DAY,
|
||||
TimeUnit.HOURS
|
||||
).build()
|
||||
WorkManager.getInstance(applicationContext).enqueueUniquePeriodicWork(
|
||||
"DailyCapabilitiesUpdateWork",
|
||||
ExistingPeriodicWorkPolicy.REPLACE,
|
||||
periodicCapabilitiesUpdateWork
|
||||
)
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
super.onTerminate()
|
||||
sharedApplication = null
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Protected methods
|
||||
protected fun buildComponent() {
|
||||
componentApplication = DaggerF7cloudTalkApplicationComponent.builder()
|
||||
.busModule(BusModule())
|
||||
.contextModule(ContextModule(applicationContext))
|
||||
.databaseModule(DatabaseModule())
|
||||
.restModule(RestModule(applicationContext))
|
||||
.arbitraryStorageModule(ArbitraryStorageModule())
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base)
|
||||
MultiDex.install(this)
|
||||
}
|
||||
|
||||
private fun buildDefaultImageLoader(): ImageLoader {
|
||||
val imageLoaderBuilder = ImageLoader.Builder(applicationContext)
|
||||
.memoryCache {
|
||||
// Use 50% of the application's available memory.
|
||||
MemoryCache.Builder(applicationContext).maxSizePercent(FIFTY_PERCENT).build()
|
||||
}
|
||||
.crossfade(true) // Show a short crossfade when loading images from network or disk into an ImageView.
|
||||
.components {
|
||||
if (SDK_INT >= P) {
|
||||
add(ImageDecoderDecoder.Factory())
|
||||
} else {
|
||||
add(GifDecoder.Factory())
|
||||
}
|
||||
add(SvgDecoder.Factory())
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
imageLoaderBuilder.logger(DebugLogger())
|
||||
}
|
||||
|
||||
imageLoaderBuilder.okHttpClient(okHttpClient)
|
||||
|
||||
return imageLoaderBuilder.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = F7cloudTalkApplication::class.java.simpleName
|
||||
const val FIFTY_PERCENT = 0.5
|
||||
const val HALF_DAY: Long = 12
|
||||
const val CIPHER_V4_MIGRATION: Int = 7
|
||||
//region Singleton
|
||||
//endregion
|
||||
|
||||
var sharedApplication: F7cloudTalkApplication? = null
|
||||
protected set
|
||||
//endregion
|
||||
|
||||
//region Setters
|
||||
fun setAppTheme(theme: String) {
|
||||
when (theme) {
|
||||
"night_no" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
"night_yes" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||
"battery_saver" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY)
|
||||
else ->
|
||||
// will be "follow_system" only for now
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
}
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
}
|
||||
Vendored
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.arbitrarystorage
|
||||
|
||||
import ru.f7cloud.talk.data.storage.ArbitraryStoragesRepository
|
||||
import ru.f7cloud.talk.data.storage.model.ArbitraryStorage
|
||||
import io.reactivex.Maybe
|
||||
|
||||
class ArbitraryStorageManager(private val arbitraryStoragesRepository: ArbitraryStoragesRepository) {
|
||||
fun storeStorageSetting(accountIdentifier: Long, key: String, value: String?, objectString: String?) {
|
||||
arbitraryStoragesRepository.saveArbitraryStorage(ArbitraryStorage(accountIdentifier, key, objectString, value))
|
||||
}
|
||||
|
||||
fun getStorageSetting(accountIdentifier: Long, key: String, objectString: String): Maybe<ArbitraryStorage> =
|
||||
arbitraryStoragesRepository.getStorageSetting(accountIdentifier, key, objectString)
|
||||
|
||||
fun deleteAllEntriesForAccountIdentifier(accountIdentifier: Long): Int =
|
||||
arbitraryStoragesRepository.deleteArbitraryStorage(accountIdentifier)
|
||||
}
|
||||
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* F7cloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2017-2019 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package ru.f7cloud.talk.bottomsheet.items
|
||||
|
||||
import android.widget.ImageView
|
||||
import androidx.annotation.DrawableRes
|
||||
|
||||
interface ListItemWithImage {
|
||||
val title: String
|
||||
fun populateIcon(imageView: ImageView)
|
||||
}
|
||||
|
||||
data class BasicListItemWithImage(@DrawableRes val iconRes: Int, override val title: String) : ListItemWithImage {
|
||||
|
||||
override fun populateIcon(imageView: ImageView) {
|
||||
imageView.setImageResource(iconRes)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user