updated to 22.0.0
This commit is contained in:
parent
93184d21d1
commit
356462d6ab
60 changed files with 1198 additions and 469 deletions
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Your Name <your@email.com>
|
||||
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.account
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Your Name <your@email.com>
|
||||
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
|
|
|
|||
|
|
@ -277,7 +277,8 @@ interface NcApiCoroutines {
|
|||
suspend fun getContextOfChatMessage(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Url url: String,
|
||||
@Query("limit") limit: Int
|
||||
@Query("limit") limit: Int,
|
||||
@Query("threadId") threadId: Int?
|
||||
): ChatOverall
|
||||
|
||||
@GET
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.content.PermissionChecker
|
||||
import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
|
||||
|
|
@ -134,6 +135,8 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
|
|||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
import com.nextcloud.talk.chat.viewmodels.ChatViewModel
|
||||
import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel
|
||||
import com.nextcloud.talk.contextchat.ContextChatView
|
||||
import com.nextcloud.talk.contextchat.ContextChatViewModel
|
||||
import com.nextcloud.talk.conversationinfo.ConversationInfoActivity
|
||||
import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel
|
||||
import com.nextcloud.talk.conversationlist.ConversationsListActivity
|
||||
|
|
@ -168,7 +171,6 @@ import com.nextcloud.talk.ui.PlaybackSpeed
|
|||
import com.nextcloud.talk.ui.PlaybackSpeedControl
|
||||
import com.nextcloud.talk.ui.StatusDrawable
|
||||
import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
|
||||
import com.nextcloud.talk.ui.dialog.ContextChatCompose
|
||||
import com.nextcloud.talk.ui.dialog.DateTimeCompose
|
||||
import com.nextcloud.talk.ui.dialog.FileAttachmentPreviewFragment
|
||||
import com.nextcloud.talk.ui.dialog.MessageActionsDialog
|
||||
|
|
@ -203,6 +205,7 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FILE_PATHS
|
|||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_BREAKOUT_ROOM
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_MODERATOR
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_OPENED_VIA_NOTIFICATION
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_RECORDING_STATE
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_START_CALL_AFTER_ROOM_SWITCH
|
||||
|
|
@ -246,7 +249,6 @@ import java.util.Locale
|
|||
import java.util.concurrent.ExecutionException
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.roundToInt
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
|
|
@ -287,6 +289,7 @@ class ChatActivity :
|
|||
lateinit var chatViewModel: ChatViewModel
|
||||
|
||||
lateinit var conversationInfoViewModel: ConversationInfoViewModel
|
||||
lateinit var contextChatViewModel: ContextChatViewModel
|
||||
lateinit var messageInputViewModel: MessageInputViewModel
|
||||
|
||||
private var chatMenu: Menu? = null
|
||||
|
|
@ -323,28 +326,27 @@ class ChatActivity :
|
|||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
executeIfResultOk(it) { intent ->
|
||||
runBlocking {
|
||||
val id = intent?.getStringExtra(MessageSearchActivity.RESULT_KEY_MESSAGE_ID)
|
||||
id?.let {
|
||||
startContextChatWindowForMessage(id)
|
||||
val messageId = intent?.getStringExtra(MessageSearchActivity.RESULT_KEY_MESSAGE_ID)
|
||||
val threadId = intent?.getStringExtra(MessageSearchActivity.RESULT_KEY_THREAD_ID)
|
||||
messageId?.let {
|
||||
startContextChatWindowForMessage(messageId, threadId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startContextChatWindowForMessage(id: String?) {
|
||||
private fun startContextChatWindowForMessage(messageId: String?, threadId: String?) {
|
||||
binding.genericComposeView.apply {
|
||||
val shouldDismiss = mutableStateOf(false)
|
||||
setContent {
|
||||
val bundle = bundleOf()
|
||||
bundle.putString(BundleKeys.KEY_CREDENTIALS, credentials!!)
|
||||
bundle.putString(BundleKeys.KEY_BASE_URL, conversationUser!!.baseUrl)
|
||||
bundle.putString(KEY_ROOM_TOKEN, roomToken)
|
||||
bundle.putString(BundleKeys.KEY_MESSAGE_ID, id)
|
||||
bundle.putString(
|
||||
KEY_CONVERSATION_NAME,
|
||||
currentConversation!!.displayName
|
||||
contextChatViewModel.getContextForChatMessages(
|
||||
credentials = credentials!!,
|
||||
baseUrl = conversationUser!!.baseUrl!!,
|
||||
token = roomToken,
|
||||
threadId = threadId,
|
||||
messageId = messageId!!,
|
||||
title = currentConversation!!.displayName
|
||||
)
|
||||
ContextChatCompose(bundle).GetDialogView(shouldDismiss, context)
|
||||
ContextChatView(context, contextChatViewModel)
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "Should open something else")
|
||||
|
|
@ -366,6 +368,7 @@ class ChatActivity :
|
|||
var sessionIdAfterRoomJoined: String? = null
|
||||
lateinit var roomToken: String
|
||||
var conversationThreadId: Long? = null
|
||||
var openedViaNotification: Boolean = false
|
||||
var conversationThreadInfo: ThreadInfo? = null
|
||||
var conversationUser: User? = null
|
||||
lateinit var spreedCapabilities: SpreedCapability
|
||||
|
|
@ -408,12 +411,11 @@ class ChatActivity :
|
|||
|
||||
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (isChatThread()) {
|
||||
if (!openedViaNotification && isChatThread()) {
|
||||
isEnabled = false
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
} else {
|
||||
val intent = Intent(this@ChatActivity, ConversationsListActivity::class.java)
|
||||
intent.putExtras(Bundle())
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
|
@ -514,6 +516,8 @@ class ChatActivity :
|
|||
|
||||
conversationInfoViewModel = ViewModelProvider(this, viewModelFactory)[ConversationInfoViewModel::class.java]
|
||||
|
||||
contextChatViewModel = ViewModelProvider(this, viewModelFactory)[ContextChatViewModel::class.java]
|
||||
|
||||
val urlForChatting = ApiUtils.getUrlForChat(chatApiVersion, conversationUser?.baseUrl, roomToken)
|
||||
val credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser!!.token)
|
||||
chatViewModel.initData(
|
||||
|
|
@ -592,6 +596,8 @@ class ChatActivity :
|
|||
null
|
||||
}
|
||||
|
||||
openedViaNotification = extras?.getBoolean(KEY_OPENED_VIA_NOTIFICATION) ?: false
|
||||
|
||||
sharedText = extras?.getString(BundleKeys.KEY_SHARED_TEXT).orEmpty()
|
||||
|
||||
Log.d(TAG, " roomToken = $roomToken")
|
||||
|
|
@ -4431,7 +4437,7 @@ class ChatActivity :
|
|||
}
|
||||
if (!foundMessage) {
|
||||
Log.d(TAG, "quoted message with id " + parentMessage.id + " was not found in adapter")
|
||||
startContextChatWindowForMessage(parentMessage.id)
|
||||
startContextChatWindowForMessage(parentMessage.id, conversationThreadId.toString())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Your Name <your@email.com>
|
||||
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
|
|
|
|||
|
|
@ -74,7 +74,8 @@ interface ChatNetworkDataSource {
|
|||
baseUrl: String,
|
||||
token: String,
|
||||
messageId: String,
|
||||
limit: Int
|
||||
limit: Int,
|
||||
threadId: Int?
|
||||
): List<ChatMessageJson>
|
||||
suspend fun getOpenGraph(credentials: String, baseUrl: String, extractedLinkToPreview: String): Reference?
|
||||
suspend fun unbindRoom(credentials: String, baseUrl: String, roomToken: String): GenericOverall
|
||||
|
|
|
|||
|
|
@ -198,10 +198,11 @@ class RetrofitChatNetwork(private val ncApi: NcApi, private val ncApiCoroutines:
|
|||
baseUrl: String,
|
||||
token: String,
|
||||
messageId: String,
|
||||
limit: Int
|
||||
limit: Int,
|
||||
threadId: Int?
|
||||
): List<ChatMessageJson> {
|
||||
val url = ApiUtils.getUrlForChatMessageContext(baseUrl, token, messageId)
|
||||
return ncApiCoroutines.getContextOfChatMessage(credentials, url, limit).ocs?.data ?: listOf()
|
||||
return ncApiCoroutines.getContextOfChatMessage(credentials, url, limit, threadId).ocs?.data ?: listOf()
|
||||
}
|
||||
|
||||
override suspend fun getOpenGraph(
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ import com.nextcloud.talk.models.domain.ConversationModel
|
|||
import com.nextcloud.talk.models.domain.ReactionAddedModel
|
||||
import com.nextcloud.talk.models.domain.ReactionDeletedModel
|
||||
import com.nextcloud.talk.models.json.capabilities.SpreedCapability
|
||||
import com.nextcloud.talk.models.json.chat.ChatMessageJson
|
||||
import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage
|
||||
import com.nextcloud.talk.models.json.conversations.RoomOverall
|
||||
import com.nextcloud.talk.models.json.generic.GenericOverall
|
||||
|
|
@ -171,10 +170,6 @@ class ChatViewModel @Inject constructor(
|
|||
val voiceMessagePlaybackSpeedPreferences: LiveData<Map<String, PlaybackSpeed>>
|
||||
get() = _voiceMessagePlaybackSpeedPreferences
|
||||
|
||||
private val _getContextChatMessages: MutableLiveData<List<ChatMessageJson>> = MutableLiveData()
|
||||
val getContextChatMessages: LiveData<List<ChatMessageJson>>
|
||||
get() = _getContextChatMessages
|
||||
|
||||
private val _threadRetrieveState = MutableStateFlow<ThreadRetrieveUiState>(ThreadRetrieveUiState.None)
|
||||
val threadRetrieveState: StateFlow<ThreadRetrieveUiState> = _threadRetrieveState
|
||||
|
||||
|
|
@ -944,20 +939,6 @@ class ChatViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun getContextForChatMessages(credentials: String, baseUrl: String, token: String, messageId: String, limit: Int) {
|
||||
viewModelScope.launch {
|
||||
val messages = chatNetworkDataSource.getContextForChatMessage(
|
||||
credentials,
|
||||
baseUrl,
|
||||
token,
|
||||
messageId,
|
||||
limit
|
||||
)
|
||||
|
||||
_getContextChatMessages.value = messages
|
||||
}
|
||||
}
|
||||
|
||||
fun getOpenGraph(credentials: String, baseUrl: String, urlToPreview: String) {
|
||||
viewModelScope.launch {
|
||||
_getOpenGraph.value = chatNetworkDataSource.getOpenGraph(credentials, baseUrl, urlToPreview)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Your Name <your@email.com>
|
||||
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,240 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
|
||||
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.contextchat
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.data.database.mappers.asModel
|
||||
import com.nextcloud.talk.models.json.chat.ChatMessageJson
|
||||
import com.nextcloud.talk.ui.ComposeChatAdapter
|
||||
import com.nextcloud.talk.utils.preview.ComposePreviewUtils
|
||||
|
||||
@Composable
|
||||
fun ContextChatView(context: Context, contextViewModel: ContextChatViewModel) {
|
||||
val contextChatMessagesState = contextViewModel.getContextChatMessagesState.collectAsState().value
|
||||
|
||||
when (contextChatMessagesState) {
|
||||
ContextChatViewModel.ContextChatRetrieveUiState.None -> {}
|
||||
is ContextChatViewModel.ContextChatRetrieveUiState.Success -> {
|
||||
ContextChatSuccessView(
|
||||
visible = true,
|
||||
context = context,
|
||||
contextChatRetrieveUiStateSuccess = contextChatMessagesState,
|
||||
onDismiss = {
|
||||
contextViewModel.clearContextChatState()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
is ContextChatViewModel.ContextChatRetrieveUiState.Error -> {
|
||||
ContextChatErrorView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContextChatErrorView() {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Info,
|
||||
contentDescription = "Info Icon"
|
||||
)
|
||||
|
||||
Text(
|
||||
stringResource(R.string.nc_capabilities_failed)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContextChatSuccessView(
|
||||
visible: Boolean,
|
||||
context: Context,
|
||||
contextChatRetrieveUiStateSuccess: ContextChatViewModel.ContextChatRetrieveUiState.Success,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val previewUtils = ComposePreviewUtils.getInstance(LocalContext.current)
|
||||
val colorScheme = previewUtils.viewThemeUtils.getColorScheme(context)
|
||||
|
||||
if (visible) {
|
||||
MaterialTheme(colorScheme) {
|
||||
Dialog(
|
||||
onDismissRequest = onDismiss,
|
||||
properties = DialogProperties(
|
||||
dismissOnBackPress = true,
|
||||
dismissOnClickOutside = true,
|
||||
usePlatformDefaultWidth = false
|
||||
)
|
||||
) {
|
||||
Surface {
|
||||
Column(
|
||||
modifier = Modifier.Companion
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
.padding(top = 16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.Companion.align(Alignment.Companion.Start),
|
||||
verticalAlignment = Alignment.Companion.CenterVertically
|
||||
) {
|
||||
IconButton(onClick = onDismiss) {
|
||||
Icon(
|
||||
Icons.Filled.Close,
|
||||
stringResource(R.string.close),
|
||||
modifier = Modifier.Companion
|
||||
.size(24.dp)
|
||||
)
|
||||
}
|
||||
Column(verticalArrangement = Arrangement.Center) {
|
||||
Text(contextChatRetrieveUiStateSuccess.title ?: "", fontSize = 18.sp)
|
||||
|
||||
if (!contextChatRetrieveUiStateSuccess.subTitle.isNullOrEmpty()) {
|
||||
Text(contextChatRetrieveUiStateSuccess.subTitle, fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
|
||||
// This code was written back then but not needed yet, but it's not deleted yet
|
||||
// because it may be used soon when further migrating to Compose...
|
||||
|
||||
// Spacer(modifier = Modifier.weight(1f))
|
||||
// val cInt = context.resources.getColor(R.color.high_emphasis_text, null)
|
||||
// Icon(
|
||||
// painterResource(R.drawable.ic_call_black_24dp),
|
||||
// "",
|
||||
// tint = Color(cInt),
|
||||
// modifier = Modifier
|
||||
// .padding()
|
||||
// .padding(end = 16.dp)
|
||||
// .alpha(HALF_ALPHA)
|
||||
// )
|
||||
//
|
||||
// Icon(
|
||||
// painterResource(R.drawable.ic_baseline_videocam_24),
|
||||
// "",
|
||||
// tint = Color(cInt),
|
||||
// modifier = Modifier
|
||||
// .padding()
|
||||
// .alpha(HALF_ALPHA)
|
||||
// )
|
||||
//
|
||||
// ComposeChatMenu(colorScheme.background, false)
|
||||
}
|
||||
|
||||
val messages = contextChatRetrieveUiStateSuccess.messages.map(ChatMessageJson::asModel)
|
||||
val messageId = contextChatRetrieveUiStateSuccess.messageId
|
||||
val threadId = contextChatRetrieveUiStateSuccess.threadId
|
||||
val adapter = ComposeChatAdapter(
|
||||
messagesJson = contextChatRetrieveUiStateSuccess.messages,
|
||||
messageId = messageId,
|
||||
threadId = threadId
|
||||
)
|
||||
SideEffect {
|
||||
adapter.addMessages(messages.toMutableList(), true)
|
||||
}
|
||||
adapter.GetView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This code was written back then but not needed yet, but it's not deleted yet
|
||||
// because it may be used soon when further migrating to Compose...
|
||||
@Composable
|
||||
private fun ComposeChatMenu(backgroundColor: Color, enabled: Boolean = true) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
Box(
|
||||
modifier = Modifier.Companion.wrapContentSize(Alignment.Companion.TopStart)
|
||||
) {
|
||||
IconButton(onClick = { expanded = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = "More options"
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
modifier = Modifier.Companion.background(backgroundColor)
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.nc_search)) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
},
|
||||
enabled = enabled
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.nc_conversation_menu_conversation_info)) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
},
|
||||
enabled = enabled
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.nc_shared_items)) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
},
|
||||
enabled = enabled
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.contextchat
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import autodagger.AutoInjector
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource
|
||||
import com.nextcloud.talk.chat.viewmodels.ChatViewModel
|
||||
import com.nextcloud.talk.models.json.chat.ChatMessageJson
|
||||
import com.nextcloud.talk.users.UserManager
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class ContextChatViewModel @Inject constructor(private val chatNetworkDataSource: ChatNetworkDataSource) :
|
||||
ViewModel() {
|
||||
|
||||
@Inject
|
||||
lateinit var chatViewModel: ChatViewModel
|
||||
|
||||
@Inject
|
||||
lateinit var userManager: UserManager
|
||||
|
||||
var threadId: String? = null
|
||||
|
||||
private val _getContextChatMessagesState =
|
||||
MutableStateFlow<ContextChatRetrieveUiState>(ContextChatRetrieveUiState.None)
|
||||
val getContextChatMessagesState: StateFlow<ContextChatRetrieveUiState> = _getContextChatMessagesState
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
fun getContextForChatMessages(
|
||||
credentials: String,
|
||||
baseUrl: String,
|
||||
token: String,
|
||||
threadId: String?,
|
||||
messageId: String,
|
||||
title: String
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val user = userManager.currentUser.blockingGet()
|
||||
|
||||
if (!user.hasSpreedFeatureCapability("chat-get-context") ||
|
||||
!user.hasSpreedFeatureCapability("federation-v1")
|
||||
) {
|
||||
_getContextChatMessagesState.value = ContextChatRetrieveUiState.Error
|
||||
}
|
||||
|
||||
var messages = chatNetworkDataSource.getContextForChatMessage(
|
||||
credentials = credentials,
|
||||
baseUrl = baseUrl,
|
||||
token = token,
|
||||
messageId = messageId,
|
||||
limit = LIMIT,
|
||||
threadId = threadId?.toInt()
|
||||
)
|
||||
|
||||
if (threadId.isNullOrEmpty()) {
|
||||
messages = messages.filter { !isThreadChildMessage(it) }
|
||||
}
|
||||
|
||||
val subTitle = if (threadId?.isNotEmpty() == true) {
|
||||
messages.firstOrNull()?.threadTitle
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
_getContextChatMessagesState.value = ContextChatRetrieveUiState.Success(
|
||||
messageId = messageId,
|
||||
threadId = threadId,
|
||||
messages = messages,
|
||||
title = title,
|
||||
subTitle = subTitle
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun isThreadChildMessage(currentMessage: ChatMessageJson): Boolean =
|
||||
currentMessage.hasThread &&
|
||||
currentMessage.threadId != currentMessage.id
|
||||
|
||||
fun clearContextChatState() {
|
||||
_getContextChatMessagesState.value = ContextChatRetrieveUiState.None
|
||||
}
|
||||
|
||||
sealed class ContextChatRetrieveUiState {
|
||||
data object None : ContextChatRetrieveUiState()
|
||||
data class Success(
|
||||
val messageId: String,
|
||||
val threadId: String?,
|
||||
val messages: List<ChatMessageJson>,
|
||||
val title: String?,
|
||||
val subTitle: String?
|
||||
) : ContextChatRetrieveUiState()
|
||||
data object Error : ContextChatRetrieveUiState()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val LIMIT = 50
|
||||
}
|
||||
}
|
||||
|
|
@ -38,14 +38,12 @@ import androidx.activity.OnBackPressedCallback
|
|||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.core.content.pm.ShortcutInfoCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.MenuItemCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.DialogFragment
|
||||
|
|
@ -115,7 +113,8 @@ import com.nextcloud.talk.threadsoverview.ThreadsOverviewActivity
|
|||
import com.nextcloud.talk.ui.BackgroundVoiceMessageCard
|
||||
import com.nextcloud.talk.ui.dialog.ChooseAccountDialogFragment
|
||||
import com.nextcloud.talk.ui.dialog.ChooseAccountShareToDialogFragment
|
||||
import com.nextcloud.talk.ui.dialog.ContextChatCompose
|
||||
import com.nextcloud.talk.contextchat.ContextChatView
|
||||
import com.nextcloud.talk.contextchat.ContextChatViewModel
|
||||
import com.nextcloud.talk.ui.dialog.ConversationsListBottomDialog
|
||||
import com.nextcloud.talk.ui.dialog.FilterConversationFragment
|
||||
import com.nextcloud.talk.ui.dialog.FilterConversationFragment.Companion.ARCHIVE
|
||||
|
|
@ -204,6 +203,7 @@ class ConversationsListActivity :
|
|||
lateinit var contactsViewModel: ContactsViewModel
|
||||
|
||||
lateinit var conversationsListViewModel: ConversationsListViewModel
|
||||
lateinit var contextChatViewModel: ContextChatViewModel
|
||||
|
||||
override val appBarLayoutType: AppBarLayoutType
|
||||
get() = AppBarLayoutType.SEARCH_BAR
|
||||
|
|
@ -263,6 +263,7 @@ class ConversationsListActivity :
|
|||
currentUser = currentUserProvider.currentUser.blockingGet()
|
||||
|
||||
conversationsListViewModel = ViewModelProvider(this, viewModelFactory)[ConversationsListViewModel::class.java]
|
||||
contextChatViewModel = ViewModelProvider(this, viewModelFactory)[ContextChatViewModel::class.java]
|
||||
|
||||
binding = ActivityConversationsBinding.inflate(layoutInflater)
|
||||
setupActionBar()
|
||||
|
|
@ -1533,15 +1534,16 @@ class ConversationsListActivity :
|
|||
).model.displayName
|
||||
|
||||
binding.genericComposeView.apply {
|
||||
val shouldDismiss = mutableStateOf(false)
|
||||
setContent {
|
||||
val bundle = bundleOf()
|
||||
bundle.putString(BundleKeys.KEY_CREDENTIALS, credentials!!)
|
||||
bundle.putString(BundleKeys.KEY_BASE_URL, currentUser!!.baseUrl)
|
||||
bundle.putString(KEY_ROOM_TOKEN, token)
|
||||
bundle.putString(BundleKeys.KEY_MESSAGE_ID, item.messageEntry.messageId)
|
||||
bundle.putString(BundleKeys.KEY_CONVERSATION_NAME, conversationName)
|
||||
ContextChatCompose(bundle).GetDialogView(shouldDismiss, context)
|
||||
contextChatViewModel.getContextForChatMessages(
|
||||
credentials = credentials!!,
|
||||
baseUrl = currentUser!!.baseUrl!!,
|
||||
token = token,
|
||||
threadId = item.messageEntry.threadId,
|
||||
messageId = item.messageEntry.messageId!!,
|
||||
title = item.messageEntry.title
|
||||
)
|
||||
ContextChatView(context, contextChatViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2244,7 +2246,7 @@ class ConversationsListActivity :
|
|||
)
|
||||
|
||||
val bundle = Bundle()
|
||||
bundle.putString(ThreadsOverviewActivity.KEY_APPBAR_TITLE, getString(R.string.followed_threads))
|
||||
bundle.putString(ThreadsOverviewActivity.KEY_APPBAR_TITLE, getString(R.string.threads))
|
||||
bundle.putString(ThreadsOverviewActivity.KEY_THREADS_SOURCE_URL, threadsUrl)
|
||||
val threadsOverviewIntent = Intent(context, ThreadsOverviewActivity::class.java)
|
||||
threadsOverviewIntent.putExtras(bundle)
|
||||
|
|
|
|||
|
|
@ -219,16 +219,6 @@ public class RestModule {
|
|||
|
||||
httpClient.addInterceptor(new HeadersInterceptor());
|
||||
|
||||
List<ConnectionSpec> specs = new ArrayList<>();
|
||||
if (BuildConfig.DEBUG) {
|
||||
specs.add(ConnectionSpec.COMPATIBLE_TLS);
|
||||
specs.add(ConnectionSpec.CLEARTEXT);
|
||||
httpClient.connectionSpecs(specs);
|
||||
} else {
|
||||
specs.add(ConnectionSpec.COMPATIBLE_TLS);
|
||||
httpClient.connectionSpecs(specs);
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG && !context.getResources().getBoolean(R.bool.nc_is_debug)) {
|
||||
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
|
||||
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import com.nextcloud.talk.account.viewmodels.BrowserLoginActivityViewModel
|
|||
import com.nextcloud.talk.chat.viewmodels.ChatViewModel
|
||||
import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel
|
||||
import com.nextcloud.talk.contacts.ContactsViewModel
|
||||
import com.nextcloud.talk.contextchat.ContextChatViewModel
|
||||
import com.nextcloud.talk.conversationcreation.ConversationCreationViewModel
|
||||
import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel
|
||||
import com.nextcloud.talk.conversationinfoedit.viewmodel.ConversationInfoEditViewModel
|
||||
|
|
@ -166,4 +167,9 @@ abstract class ViewModelModule {
|
|||
@IntoMap
|
||||
@ViewModelKey(BrowserLoginActivityViewModel::class)
|
||||
abstract fun browserLoginActivityViewModel(viewModel: BrowserLoginActivityViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(ContextChatViewModel::class)
|
||||
abstract fun contextChatViewModel(viewModel: ContextChatViewModel): ViewModel
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,8 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ONE_TO_ONE
|
|||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SHARE_RECORDING_TO_CHAT_URL
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SYSTEM_NOTIFICATION_ID
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_THREAD_ID
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_OPENED_VIA_NOTIFICATION
|
||||
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Observer
|
||||
|
|
@ -397,6 +399,10 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
|
|||
val ncNotification = notificationOverall.ocs!!.notification
|
||||
if (ncNotification != null) {
|
||||
enrichPushMessageByNcNotificationData(ncNotification)
|
||||
|
||||
val threadId = parseThreadId(ncNotification.objectId)
|
||||
threadId?.let { intent.putExtra(KEY_THREAD_ID, it) }
|
||||
|
||||
showNotification(intent, ncNotification)
|
||||
}
|
||||
}
|
||||
|
|
@ -827,6 +833,8 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
|
|||
}
|
||||
}
|
||||
|
||||
private fun parseThreadId(objectId: String?): Long? = objectId?.split("/")?.getOrNull(2)?.toLongOrNull()
|
||||
|
||||
private fun sendNotification(notificationId: Int, notification: Notification) {
|
||||
Log.d(TAG, "show notification with id $notificationId")
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
|
|
@ -982,6 +990,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
|
|||
val bundle = Bundle()
|
||||
bundle.putString(KEY_ROOM_TOKEN, pushMessage.id)
|
||||
bundle.putLong(KEY_INTERNAL_USER_ID, signatureVerification.user!!.id!!)
|
||||
bundle.putBoolean(KEY_OPENED_VIA_NOTIFICATION, true)
|
||||
intent.putExtras(bundle)
|
||||
return intent
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@ class MessageSearchActivity : BaseActivity() {
|
|||
if (state is MessageSearchViewModel.FinishedState) {
|
||||
val resultIntent = Intent().apply {
|
||||
putExtra(RESULT_KEY_MESSAGE_ID, state.selectedMessageId)
|
||||
putExtra(RESULT_KEY_THREAD_ID, state.selectedThreadId)
|
||||
}
|
||||
setResult(Activity.RESULT_OK, resultIntent)
|
||||
finish()
|
||||
|
|
@ -244,5 +245,6 @@ class MessageSearchActivity : BaseActivity() {
|
|||
|
||||
companion object {
|
||||
const val RESULT_KEY_MESSAGE_ID = "MessageSearchActivity.result.message"
|
||||
const val RESULT_KEY_THREAD_ID = "MessageSearchActivity.result.thread"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class MessageSearchViewModel @Inject constructor(private val unifiedSearchReposi
|
|||
object EmptyState : ViewState()
|
||||
object ErrorState : ViewState()
|
||||
class LoadedState(val results: List<SearchMessageEntry>, val hasMore: Boolean) : ViewState()
|
||||
class FinishedState(val selectedMessageId: String) : ViewState()
|
||||
class FinishedState(val selectedMessageId: String, val selectedThreadId: String?) : ViewState()
|
||||
|
||||
private lateinit var messageSearchHelper: MessageSearchHelper
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ class MessageSearchViewModel @Inject constructor(private val unifiedSearchReposi
|
|||
}
|
||||
|
||||
fun selectMessage(messageEntry: SearchMessageEntry) {
|
||||
_state.value = FinishedState(messageEntry.messageId!!)
|
||||
_state.value = FinishedState(messageEntry.messageId!!, messageEntry.threadId)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -13,5 +13,6 @@ data class SearchMessageEntry(
|
|||
val title: String,
|
||||
val messageExcerpt: String,
|
||||
val conversationToken: String,
|
||||
val threadId: String?,
|
||||
val messageId: String?
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Your Name <your@email.com>
|
||||
* SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ class UnifiedSearchRepositoryImpl(private val api: NcApi, private val userProvid
|
|||
|
||||
private const val ATTRIBUTE_CONVERSATION = "conversation"
|
||||
private const val ATTRIBUTE_MESSAGE_ID = "messageId"
|
||||
private const val ATTRIBUTE_THREAD_ID = "threadId"
|
||||
|
||||
private fun mapToMessageResults(
|
||||
data: UnifiedSearchResponseData,
|
||||
|
|
@ -81,13 +82,15 @@ class UnifiedSearchRepositoryImpl(private val api: NcApi, private val userProvid
|
|||
private fun mapToMessage(unifiedSearchEntry: UnifiedSearchEntry, searchTerm: String): SearchMessageEntry {
|
||||
val conversation = unifiedSearchEntry.attributes?.get(ATTRIBUTE_CONVERSATION)!!
|
||||
val messageId = unifiedSearchEntry.attributes?.get(ATTRIBUTE_MESSAGE_ID)
|
||||
val threadId = unifiedSearchEntry.attributes?.get(ATTRIBUTE_THREAD_ID)
|
||||
return SearchMessageEntry(
|
||||
searchTerm,
|
||||
unifiedSearchEntry.thumbnailUrl,
|
||||
unifiedSearchEntry.title!!,
|
||||
unifiedSearchEntry.subline!!,
|
||||
conversation,
|
||||
messageId
|
||||
searchTerm = searchTerm,
|
||||
thumbnailURL = unifiedSearchEntry.thumbnailUrl,
|
||||
title = unifiedSearchEntry.title!!,
|
||||
messageExcerpt = unifiedSearchEntry.subline!!,
|
||||
conversationToken = conversation,
|
||||
threadId = threadId,
|
||||
messageId = messageId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Your Name <your@email.com>
|
||||
* SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ import kotlin.random.Random
|
|||
class ComposeChatAdapter(
|
||||
private var messagesJson: List<ChatMessageJson>? = null,
|
||||
private var messageId: String? = null,
|
||||
private var threadId: String? = null,
|
||||
private val utils: ComposePreviewUtils? = null
|
||||
) {
|
||||
|
||||
|
|
@ -195,6 +196,7 @@ class ComposeChatAdapter(
|
|||
private const val ANIMATED_BLINK = 500
|
||||
private const val FLOAT_06 = 0.6f
|
||||
private const val HALF_OPACITY = 127
|
||||
private const val MESSAGE_LENGTH_THRESHOLD = 25
|
||||
}
|
||||
|
||||
private var incomingShape: RoundedCornerShape = RoundedCornerShape(2.dp, 20.dp, 20.dp, 20.dp)
|
||||
|
|
@ -354,7 +356,8 @@ class ComposeChatAdapter(
|
|||
this.isReaction() ||
|
||||
this.isPollVotedMessage() ||
|
||||
this.isEditMessage() ||
|
||||
this.isInfoMessageAboutDeletion()
|
||||
this.isInfoMessageAboutDeletion() ||
|
||||
this.isThreadCreatedMessage()
|
||||
|
||||
private fun ChatMessage.isInfoMessageAboutDeletion(): Boolean =
|
||||
this.parentMessageId != null &&
|
||||
|
|
@ -366,6 +369,9 @@ class ComposeChatAdapter(
|
|||
private fun ChatMessage.isEditMessage(): Boolean =
|
||||
this.systemMessageType == ChatMessage.SystemMessageType.MESSAGE_EDITED
|
||||
|
||||
private fun ChatMessage.isThreadCreatedMessage(): Boolean =
|
||||
this.systemMessageType == ChatMessage.SystemMessageType.THREAD_CREATED
|
||||
|
||||
private fun ChatMessage.isReaction(): Boolean =
|
||||
systemMessageType == ChatMessage.SystemMessageType.REACTION ||
|
||||
systemMessageType == ChatMessage.SystemMessageType.REACTION_DELETED ||
|
||||
|
|
@ -429,16 +435,30 @@ class ComposeChatAdapter(
|
|||
message: ChatMessage,
|
||||
includePadding: Boolean = true,
|
||||
playAnimation: Boolean = false,
|
||||
content:
|
||||
@Composable
|
||||
(RowScope.() -> Unit)
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
fun shouldShowTimeNextToContent(message: ChatMessage): Boolean {
|
||||
val containsLinebreak = message.message?.contains("\n") ?: false ||
|
||||
message.message?.contains("\r") ?: false
|
||||
|
||||
return ((message.message?.length ?: 0) < MESSAGE_LENGTH_THRESHOLD) &&
|
||||
!isFirstMessageOfThreadInNormalChat(message) &&
|
||||
message.messageParameters.isNullOrEmpty() &&
|
||||
!containsLinebreak
|
||||
}
|
||||
|
||||
val incoming = message.actorId != currentUser.userId
|
||||
val color = if (incoming) {
|
||||
if (message.isDeleted) {
|
||||
LocalContext.current.resources.getColor(R.color.bg_message_list_incoming_bubble_deleted, null)
|
||||
LocalContext.current.resources.getColor(
|
||||
R.color.bg_message_list_incoming_bubble_deleted,
|
||||
null
|
||||
)
|
||||
} else {
|
||||
LocalContext.current.resources.getColor(R.color.bg_message_list_incoming_bubble, null)
|
||||
LocalContext.current.resources.getColor(
|
||||
R.color.bg_message_list_incoming_bubble,
|
||||
null
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (message.isDeleted) {
|
||||
|
|
@ -449,11 +469,15 @@ class ComposeChatAdapter(
|
|||
}
|
||||
val shape = if (incoming) incomingShape else outgoingShape
|
||||
|
||||
val rowModifier = if (message.id == messageId && playAnimation) {
|
||||
Modifier.withCustomAnimation(incoming)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = (
|
||||
if (message.id == messageId && playAnimation) Modifier.withCustomAnimation(incoming) else Modifier
|
||||
)
|
||||
.fillMaxWidth(1f)
|
||||
modifier = rowModifier.fillMaxWidth(),
|
||||
horizontalArrangement = if (incoming) Arrangement.Start else Arrangement.End
|
||||
) {
|
||||
if (incoming) {
|
||||
val imageUri = message.actorId?.let { viewModel.contactsViewModel.getImageUri(it, true) }
|
||||
|
|
@ -465,11 +489,10 @@ class ComposeChatAdapter(
|
|||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding()
|
||||
.padding(end = 8.dp)
|
||||
)
|
||||
} else {
|
||||
Spacer(Modifier.weight(1f))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
}
|
||||
|
||||
Surface(
|
||||
|
|
@ -480,38 +503,51 @@ class ComposeChatAdapter(
|
|||
color = Color(color),
|
||||
shape = shape
|
||||
) {
|
||||
val timeString = DateUtils(LocalContext.current).getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
val modifier = if (includePadding) Modifier.padding(8.dp, 4.dp, 8.dp, 4.dp) else Modifier
|
||||
val modifier = if (includePadding) {
|
||||
Modifier.padding(16.dp, 4.dp, 16.dp, 4.dp)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
Column(modifier = modifier) {
|
||||
if (message.parentMessageId != null && !message.isDeleted && messagesJson != null) {
|
||||
if (messagesJson != null &&
|
||||
message.parentMessageId != null &&
|
||||
!message.isDeleted &&
|
||||
message.parentMessageId.toString() != threadId
|
||||
) {
|
||||
messagesJson!!
|
||||
.find { it.parentMessage?.id == message.parentMessageId }
|
||||
?.parentMessage!!.asModel().let { CommonMessageQuote(LocalContext.current, it) }
|
||||
?.parentMessage!!.asModel()
|
||||
.let { CommonMessageQuote(LocalContext.current, it) }
|
||||
}
|
||||
|
||||
if (incoming) {
|
||||
Text(message.actorDisplayName.toString(), fontSize = AUTHOR_TEXT_SIZE)
|
||||
}
|
||||
|
||||
Row {
|
||||
ThreadTitle(message)
|
||||
|
||||
if (shouldShowTimeNextToContent(message)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
content()
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(top = 6.dp, start = 8.dp)
|
||||
) {
|
||||
TimeDisplay(message)
|
||||
ReadStatus(message)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
content()
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Text(
|
||||
timeString,
|
||||
fontSize = TIME_TEXT_SIZE,
|
||||
textAlign = TextAlign.End,
|
||||
modifier = Modifier.align(Alignment.CenterVertically)
|
||||
)
|
||||
if (message.readStatus == ReadStatus.NONE) {
|
||||
val read = painterResource(R.drawable.ic_check_all)
|
||||
Icon(
|
||||
read,
|
||||
"",
|
||||
modifier = Modifier
|
||||
.padding(start = 2.dp)
|
||||
.size(12.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.align(Alignment.End),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
TimeDisplay(message)
|
||||
ReadStatus(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -519,6 +555,55 @@ class ComposeChatAdapter(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TimeDisplay(message: ChatMessage) {
|
||||
val timeString = DateUtils(LocalContext.current)
|
||||
.getLocalTimeStringFromTimestamp(message.timestamp)
|
||||
Text(
|
||||
timeString,
|
||||
fontSize = TIME_TEXT_SIZE,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReadStatus(message: ChatMessage) {
|
||||
if (message.readStatus == ReadStatus.NONE) {
|
||||
val read = painterResource(R.drawable.ic_check_all)
|
||||
Icon(
|
||||
read,
|
||||
"",
|
||||
modifier = Modifier
|
||||
.padding(start = 4.dp)
|
||||
.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ThreadTitle(message: ChatMessage) {
|
||||
if (isFirstMessageOfThreadInNormalChat(message)) {
|
||||
Row {
|
||||
val read = painterResource(R.drawable.outline_forum_24)
|
||||
Icon(
|
||||
read,
|
||||
"",
|
||||
modifier = Modifier
|
||||
.padding(end = 6.dp)
|
||||
.size(18.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
)
|
||||
Text(
|
||||
text = message.threadTitle ?: "",
|
||||
fontSize = REGULAR_TEXT_SIZE,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isFirstMessageOfThreadInNormalChat(message: ChatMessage): Boolean = threadId == null && message.isThread
|
||||
|
||||
@Composable
|
||||
private fun Modifier.withCustomAnimation(incoming: Boolean): Modifier {
|
||||
val infiniteTransition = rememberInfiniteTransition()
|
||||
|
|
@ -750,8 +835,8 @@ class ComposeChatAdapter(
|
|||
read,
|
||||
"",
|
||||
modifier = Modifier
|
||||
.padding(start = 2.dp)
|
||||
.size(12.dp)
|
||||
.padding(start = 4.dp)
|
||||
.size(16.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
)
|
||||
}
|
||||
|
|
@ -762,29 +847,30 @@ class ComposeChatAdapter(
|
|||
@Composable
|
||||
private fun VoiceMessage(message: ChatMessage, state: MutableState<Boolean>) {
|
||||
CommonMessageBody(message, playAnimation = state.value) {
|
||||
Icon(
|
||||
Icons.Filled.PlayArrow,
|
||||
"play",
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.PlayArrow,
|
||||
contentDescription = "play",
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
|
||||
AndroidView(
|
||||
factory = { ctx ->
|
||||
WaveformSeekBar(ctx).apply {
|
||||
setWaveData(FloatArray(DEFAULT_WAVE_SIZE) { Random.nextFloat() }) // READ ONLY for now
|
||||
setColors(
|
||||
colorScheme.inversePrimary.toArgb(),
|
||||
colorScheme.onPrimaryContainer.toArgb()
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.width(180.dp)
|
||||
.height(80.dp)
|
||||
)
|
||||
AndroidView(
|
||||
factory = { ctx ->
|
||||
WaveformSeekBar(ctx).apply {
|
||||
setWaveData(FloatArray(DEFAULT_WAVE_SIZE) { Random.nextFloat() }) // READ ONLY for now
|
||||
setColors(
|
||||
colorScheme.inversePrimary.toArgb(),
|
||||
colorScheme.onPrimaryContainer.toArgb()
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.width(180.dp)
|
||||
.height(80.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -856,6 +942,7 @@ class ComposeChatAdapter(
|
|||
message.extractedUrlToPreview!!
|
||||
)
|
||||
CommonMessageBody(message, playAnimation = state.value) {
|
||||
EnrichedText(message)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.drawWithCache {
|
||||
|
|
@ -960,9 +1047,17 @@ class ComposeChatAdapter(
|
|||
|
||||
@Preview(showBackground = true, widthDp = 380, heightDp = 800)
|
||||
@Composable
|
||||
@Suppress("MagicNumber", "LongMethod")
|
||||
fun AllMessageTypesPreview() {
|
||||
val previewUtils = ComposePreviewUtils.getInstance(LocalContext.current)
|
||||
val adapter = remember { ComposeChatAdapter(messagesJson = null, messageId = null, previewUtils) }
|
||||
val adapter = remember {
|
||||
ComposeChatAdapter(
|
||||
messagesJson = null,
|
||||
messageId = null,
|
||||
threadId = null,
|
||||
previewUtils
|
||||
)
|
||||
}
|
||||
|
||||
val sampleMessages = remember {
|
||||
listOf(
|
||||
|
|
@ -982,6 +1077,42 @@ fun AllMessageTypesPreview() {
|
|||
timestamp = System.currentTimeMillis()
|
||||
actorDisplayName = "User2"
|
||||
messageType = ChatMessage.MessageType.REGULAR_TEXT_MESSAGE.name
|
||||
},
|
||||
ChatMessage().apply {
|
||||
jsonMessageId = 3
|
||||
actorId = "user1_id"
|
||||
message = "This is a really really really really really really really really really long message"
|
||||
timestamp = System.currentTimeMillis()
|
||||
actorDisplayName = "User2"
|
||||
messageType = ChatMessage.MessageType.REGULAR_TEXT_MESSAGE.name
|
||||
},
|
||||
ChatMessage().apply {
|
||||
jsonMessageId = 4
|
||||
actorId = "user1_id"
|
||||
message = "some \n linebreak"
|
||||
timestamp = System.currentTimeMillis()
|
||||
actorDisplayName = "User2"
|
||||
messageType = ChatMessage.MessageType.REGULAR_TEXT_MESSAGE.name
|
||||
},
|
||||
ChatMessage().apply {
|
||||
jsonMessageId = 5
|
||||
actorId = "user1_id"
|
||||
threadTitle = "Thread title"
|
||||
isThread = true
|
||||
message = "Content of a first thread message"
|
||||
timestamp = System.currentTimeMillis()
|
||||
actorDisplayName = "User2"
|
||||
messageType = ChatMessage.MessageType.REGULAR_TEXT_MESSAGE.name
|
||||
},
|
||||
ChatMessage().apply {
|
||||
jsonMessageId = 6
|
||||
actorId = "user1_id"
|
||||
threadTitle = "looooooooooooong Thread title"
|
||||
isThread = true
|
||||
message = "Content"
|
||||
timestamp = System.currentTimeMillis()
|
||||
actorDisplayName = "User2"
|
||||
messageType = ChatMessage.MessageType.REGULAR_TEXT_MESSAGE.name
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,262 +0,0 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.ui.dialog
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.os.Bundle
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asFlow
|
||||
import autodagger.AutoInjector
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.chat.viewmodels.ChatViewModel
|
||||
import com.nextcloud.talk.data.database.mappers.asModel
|
||||
import com.nextcloud.talk.models.json.chat.ChatMessageJson
|
||||
import com.nextcloud.talk.ui.ComposeChatAdapter
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
import com.nextcloud.talk.users.UserManager
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("FunctionNaming", "LongMethod", "StaticFieldLeak")
|
||||
class ContextChatCompose(val bundle: Bundle) {
|
||||
|
||||
companion object {
|
||||
const val LIMIT = 50
|
||||
const val HALF_ALPHA = 0.5f
|
||||
}
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
inner class ContextChatComposeViewModel : ViewModel() {
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var chatViewModel: ChatViewModel
|
||||
|
||||
@Inject
|
||||
lateinit var userManager: UserManager
|
||||
|
||||
init {
|
||||
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
|
||||
val credentials = bundle.getString(BundleKeys.KEY_CREDENTIALS)!!
|
||||
val baseUrl = bundle.getString(BundleKeys.KEY_BASE_URL)!!
|
||||
val token = bundle.getString(BundleKeys.KEY_ROOM_TOKEN)!!
|
||||
val messageId = bundle.getString(BundleKeys.KEY_MESSAGE_ID)!!
|
||||
|
||||
chatViewModel.getContextForChatMessages(credentials, baseUrl, token, messageId, LIMIT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.requireActivity(): Activity {
|
||||
var context = this
|
||||
while (context is ContextWrapper) {
|
||||
if (context is Activity) return context
|
||||
context = context.baseContext
|
||||
}
|
||||
throw IllegalStateException("No activity was present but it is required.")
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GetDialogView(
|
||||
shouldDismiss: MutableState<Boolean>,
|
||||
context: Context,
|
||||
contextViewModel: ContextChatComposeViewModel = ContextChatComposeViewModel()
|
||||
) {
|
||||
if (shouldDismiss.value) {
|
||||
context.requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
return
|
||||
}
|
||||
|
||||
context.requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
|
||||
val colorScheme = contextViewModel.viewThemeUtils.getColorScheme(context)
|
||||
MaterialTheme(colorScheme) {
|
||||
Dialog(
|
||||
onDismissRequest = {
|
||||
shouldDismiss.value = true
|
||||
},
|
||||
properties = DialogProperties(
|
||||
dismissOnBackPress = true,
|
||||
dismissOnClickOutside = true,
|
||||
usePlatformDefaultWidth = false
|
||||
)
|
||||
) {
|
||||
Surface {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
.padding(top = 16.dp)
|
||||
) {
|
||||
val user = contextViewModel.userManager.currentUser.blockingGet()
|
||||
val shouldShow = !user.hasSpreedFeatureCapability("chat-get-context") ||
|
||||
!user.hasSpreedFeatureCapability("federation-v1")
|
||||
Row(
|
||||
modifier = Modifier.align(Alignment.Start),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(onClick = {
|
||||
shouldDismiss.value = true
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Filled.Close,
|
||||
stringResource(R.string.close),
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
)
|
||||
}
|
||||
Column(verticalArrangement = Arrangement.Center) {
|
||||
val name = bundle.getString(BundleKeys.KEY_CONVERSATION_NAME)!!
|
||||
Text(name, fontSize = 24.sp)
|
||||
}
|
||||
// Spacer(modifier = Modifier.weight(1f))
|
||||
// val cInt = context.resources.getColor(R.color.high_emphasis_text, null)
|
||||
// Icon(
|
||||
// painterResource(R.drawable.ic_call_black_24dp),
|
||||
// "",
|
||||
// tint = Color(cInt),
|
||||
// modifier = Modifier
|
||||
// .padding()
|
||||
// .padding(end = 16.dp)
|
||||
// .alpha(HALF_ALPHA)
|
||||
// )
|
||||
//
|
||||
// Icon(
|
||||
// painterResource(R.drawable.ic_baseline_videocam_24),
|
||||
// "",
|
||||
// tint = Color(cInt),
|
||||
// modifier = Modifier
|
||||
// .padding()
|
||||
// .alpha(HALF_ALPHA)
|
||||
// )
|
||||
//
|
||||
// ComposeChatMenu(colorScheme.background, false)
|
||||
}
|
||||
if (shouldShow) {
|
||||
Icon(
|
||||
Icons.Filled.Info,
|
||||
"Info Icon",
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
|
||||
Text(
|
||||
stringResource(R.string.nc_capabilities_failed),
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
} else {
|
||||
val contextState = contextViewModel
|
||||
.chatViewModel
|
||||
.getContextChatMessages
|
||||
.asFlow()
|
||||
.collectAsState(listOf())
|
||||
val messagesJson = contextState.value
|
||||
val messages = messagesJson.map(ChatMessageJson::asModel)
|
||||
val messageId = bundle.getString(BundleKeys.KEY_MESSAGE_ID)!!
|
||||
val adapter = ComposeChatAdapter(messagesJson, messageId)
|
||||
SideEffect {
|
||||
adapter.addMessages(messages.toMutableList(), true)
|
||||
}
|
||||
adapter.GetView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ComposeChatMenu(backgroundColor: Color, enabled: Boolean = true) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
Box(
|
||||
modifier = Modifier.wrapContentSize(Alignment.TopStart)
|
||||
) {
|
||||
IconButton(onClick = { expanded = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = "More options"
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
modifier = Modifier.background(backgroundColor)
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.nc_search)) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
},
|
||||
enabled = enabled
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.nc_conversation_menu_conversation_info)) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
},
|
||||
enabled = enabled
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.nc_shared_items)) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
},
|
||||
enabled = enabled
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -84,4 +84,5 @@ object BundleKeys {
|
|||
const val KEY_FOCUS_INPUT: String = "KEY_FOCUS_INPUT"
|
||||
const val KEY_THREAD_ID = "KEY_THREAD_ID"
|
||||
const val KEY_FROM_QR: String = "KEY_FROM_QR"
|
||||
const val KEY_OPENED_VIA_NOTIFICATION: String = "KEY_OPENED_VIA_NOTIFICATION"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue