diff --git a/app/src/main/java/com/nextcloud/talk/account/WebViewLoginActivity.kt b/app/src/main/java/com/nextcloud/talk/account/WebViewLoginActivity.kt index 0d52529394a..e2fcb5a6768 100644 --- a/app/src/main/java/com/nextcloud/talk/account/WebViewLoginActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/account/WebViewLoginActivity.kt @@ -1,7 +1,7 @@ /* * Nextcloud Talk - Android Client * - * SPDX-FileCopyrightText: 2025 Your Name + * SPDX-FileCopyrightText: 2025 Julius Linus * SPDX-License-Identifier: GPL-3.0-or-later */ package com.nextcloud.talk.account diff --git a/app/src/main/java/com/nextcloud/talk/account/viewmodels/BrowserLoginActivityViewModel.kt b/app/src/main/java/com/nextcloud/talk/account/viewmodels/BrowserLoginActivityViewModel.kt index b76b27c5e6e..e9560c57357 100644 --- a/app/src/main/java/com/nextcloud/talk/account/viewmodels/BrowserLoginActivityViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/account/viewmodels/BrowserLoginActivityViewModel.kt @@ -1,7 +1,7 @@ /* * Nextcloud Talk - Android Client * - * SPDX-FileCopyrightText: 2025 Your Name + * SPDX-FileCopyrightText: 2025 Julius Linus * SPDX-License-Identifier: GPL-3.0-or-later */ diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt index 4211aa2b3b7..a0cf017bb89 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt +++ b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt @@ -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 diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 647772720dd..c24bf1b7bf5 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -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 @@ -246,7 +248,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 +288,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 +325,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") @@ -514,6 +515,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( @@ -4431,7 +4434,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()) } } diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt b/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt index e8dadd33159..2b4399fae84 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt @@ -1,7 +1,7 @@ /* * Nextcloud Talk - Android Client * - * SPDX-FileCopyrightText: 2024 Your Name + * SPDX-FileCopyrightText: 2025 Marcel Hibbe * SPDX-License-Identifier: GPL-3.0-or-later */ diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt index 637cf5d7098..5dcdfe3b618 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt @@ -74,7 +74,8 @@ interface ChatNetworkDataSource { baseUrl: String, token: String, messageId: String, - limit: Int + limit: Int, + threadId: Int? ): List suspend fun getOpenGraph(credentials: String, baseUrl: String, extractedLinkToPreview: String): Reference? suspend fun unbindRoom(credentials: String, baseUrl: String, roomToken: String): GenericOverall diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt index 057472ff1ee..6bb6836cafe 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt @@ -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 { 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( diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt index 7b8f6dc7ad4..5d05189f57c 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt @@ -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> get() = _voiceMessagePlaybackSpeedPreferences - private val _getContextChatMessages: MutableLiveData> = MutableLiveData() - val getContextChatMessages: LiveData> - get() = _getContextChatMessages - private val _threadRetrieveState = MutableStateFlow(ThreadRetrieveUiState.None) val threadRetrieveState: StateFlow = _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) diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsApplication.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsApplication.kt index 445f0926468..cc702104f3d 100644 --- a/app/src/main/java/com/nextcloud/talk/contacts/ContactsApplication.kt +++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsApplication.kt @@ -1,7 +1,7 @@ /* * Nextcloud Talk - Android Client * - * SPDX-FileCopyrightText: 2024 Your Name + * SPDX-FileCopyrightText: 2024 Sowjanya Kota * SPDX-License-Identifier: GPL-3.0-or-later */ diff --git a/app/src/main/java/com/nextcloud/talk/contextchat/ContextChatView.kt b/app/src/main/java/com/nextcloud/talk/contextchat/ContextChatView.kt new file mode 100644 index 00000000000..5fb014e2ba5 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contextchat/ContextChatView.kt @@ -0,0 +1,240 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Julius Linus + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * 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 + ) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/contextchat/ContextChatViewModel.kt b/app/src/main/java/com/nextcloud/talk/contextchat/ContextChatViewModel.kt new file mode 100644 index 00000000000..462725db936 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contextchat/ContextChatViewModel.kt @@ -0,0 +1,109 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * 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.None) + val getContextChatMessagesState: StateFlow = _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, + val title: String?, + val subTitle: String? + ) : ContextChatRetrieveUiState() + data object Error : ContextChatRetrieveUiState() + } + + companion object { + private const val LIMIT = 50 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt index 06324a64e77..b7d0c3a3eef 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt @@ -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) diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt index 7d44652155f..5ee1068a24b 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt @@ -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 } diff --git a/app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchActivity.kt b/app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchActivity.kt index bf87be30715..f6c54ec1e44 100644 --- a/app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchActivity.kt @@ -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" } } diff --git a/app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchViewModel.kt b/app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchViewModel.kt index e84f0353b34..81b64a85995 100644 --- a/app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchViewModel.kt @@ -42,7 +42,7 @@ class MessageSearchViewModel @Inject constructor(private val unifiedSearchReposi object EmptyState : ViewState() object ErrorState : ViewState() class LoadedState(val results: List, 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 { diff --git a/app/src/main/java/com/nextcloud/talk/models/domain/SearchMessageEntry.kt b/app/src/main/java/com/nextcloud/talk/models/domain/SearchMessageEntry.kt index 7ef317b0fe5..e3d85b66fe4 100644 --- a/app/src/main/java/com/nextcloud/talk/models/domain/SearchMessageEntry.kt +++ b/app/src/main/java/com/nextcloud/talk/models/domain/SearchMessageEntry.kt @@ -13,5 +13,6 @@ data class SearchMessageEntry( val title: String, val messageExcerpt: String, val conversationToken: String, + val threadId: String?, val messageId: String? ) diff --git a/app/src/main/java/com/nextcloud/talk/models/json/conversations/ConversationEnums.kt b/app/src/main/java/com/nextcloud/talk/models/json/conversations/ConversationEnums.kt index 56a8d9660ce..c91c7eaf89f 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/conversations/ConversationEnums.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/conversations/ConversationEnums.kt @@ -1,7 +1,7 @@ /* * Nextcloud Talk - Android Client * - * SPDX-FileCopyrightText: 2024 Your Name + * SPDX-FileCopyrightText: 2024 Marcel Hibbe * SPDX-License-Identifier: GPL-3.0-or-later */ diff --git a/app/src/main/java/com/nextcloud/talk/repositories/unifiedsearch/UnifiedSearchRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/repositories/unifiedsearch/UnifiedSearchRepositoryImpl.kt index 88ce0175cd2..07db8198d31 100644 --- a/app/src/main/java/com/nextcloud/talk/repositories/unifiedsearch/UnifiedSearchRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/repositories/unifiedsearch/UnifiedSearchRepositoryImpl.kt @@ -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 ) } } diff --git a/app/src/main/java/com/nextcloud/talk/ui/BackgroundVoiceMessageCard.kt b/app/src/main/java/com/nextcloud/talk/ui/BackgroundVoiceMessageCard.kt index 88e75cf906b..e578ee367d2 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/BackgroundVoiceMessageCard.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/BackgroundVoiceMessageCard.kt @@ -1,7 +1,7 @@ /* * Nextcloud Talk - Android Client * - * SPDX-FileCopyrightText: 2024 Your Name + * SPDX-FileCopyrightText: 2024 Julius Linus * SPDX-License-Identifier: GPL-3.0-or-later */ diff --git a/app/src/main/java/com/nextcloud/talk/ui/ComposeChatAdapter.kt b/app/src/main/java/com/nextcloud/talk/ui/ComposeChatAdapter.kt index b391aca13ac..13a96535a00 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/ComposeChatAdapter.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/ComposeChatAdapter.kt @@ -127,6 +127,7 @@ import kotlin.random.Random class ComposeChatAdapter( private var messagesJson: List? = 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) { 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 } ) } diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/ContextChatCompose.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/ContextChatCompose.kt deleted file mode 100644 index 0da924ab7ce..00000000000 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/ContextChatCompose.kt +++ /dev/null @@ -1,262 +0,0 @@ -/* - * Nextcloud Talk - Android Client - * - * SPDX-FileCopyrightText: 2025 Julius Linus - * 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, - 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 - ) - } - } - } -} diff --git a/app/src/main/res/drawable/baseline_tag_faces_24.xml b/app/src/main/res/drawable/baseline_tag_faces_24.xml index 3a4f4587767..ecdf8416afe 100644 --- a/app/src/main/res/drawable/baseline_tag_faces_24.xml +++ b/app/src/main/res/drawable/baseline_tag_faces_24.xml @@ -1,10 +1,9 @@ - + ~ SPDX-FileCopyrightText: 2023-2024 Google LLC + ~ SPDX-License-Identifier: Apache-2.0 +--> and %1$s others are typing … %1$s in %2$s + Threads Go to thread Create a thread Thread title Cancel thread creation Recent threads - Followed threads Reply %d reply diff --git a/app/src/test/java/com/nextcloud/talk/messagesearch/MessageSearchHelperTest.kt b/app/src/test/java/com/nextcloud/talk/messagesearch/MessageSearchHelperTest.kt index 814c70bb34b..9e51c4ba8a3 100644 --- a/app/src/test/java/com/nextcloud/talk/messagesearch/MessageSearchHelperTest.kt +++ b/app/src/test/java/com/nextcloud/talk/messagesearch/MessageSearchHelperTest.kt @@ -27,8 +27,9 @@ class MessageSearchHelperTest { title: String = "foo", messageExcerpt: String = "foo", conversationToken: String = "foo", - messageId: String? = "foo" - ) = SearchMessageEntry(searchTerm, thumbnailURL, title, messageExcerpt, conversationToken, messageId) + messageId: String? = "foo", + threadId: String? = "foo" + ) = SearchMessageEntry(searchTerm, thumbnailURL, title, messageExcerpt, conversationToken, threadId, messageId) @Before fun setUp() {