From a0fe8a1b151a4bde77a3f7a88190ae9f6198628e Mon Sep 17 00:00:00 2001 From: thirumani-vihaan Date: Mon, 27 Jul 2026 22:47:41 +0530 Subject: [PATCH 1/2] Enhancement: Add classified conversations support (#6414) Signed-off-by: thirumani-vihaan --- .../com/nextcloud/talk/chat/ChatActivity.kt | 23 ++++++++++++- .../com/nextcloud/talk/chat/ui/ChatToolbar.kt | 34 ++++++++++++++++++- .../talk/chat/ui/ChatToolbarState.kt | 3 +- .../talk/chat/ui/MessageActionsBottomSheet.kt | 6 ++-- .../talk/chat/ui/model/ChatMessageUi.kt | 17 ++++++---- .../talk/chat/viewmodels/ChatViewModel.kt | 17 ++++++---- .../ConversationInfoUiState.kt | 1 + .../ui/ConversationInfoScreen.kt | 8 +++-- .../viewmodel/ConversationInfoViewModel.kt | 16 +++++---- .../json/conversations/ConversationEnums.kt | 3 +- .../ConversationObjectTypeConverter.kt | 2 ++ .../nextcloud/talk/ui/chat/MediaMessage.kt | 18 ++++++---- .../nextcloud/talk/utils/CapabilitiesUtil.kt | 13 ++++++- .../nextcloud/talk/utils/ConversationUtils.kt | 7 ++++ 14 files changed, 131 insertions(+), 37 deletions(-) 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 fa07b43ab8..9f276c1eae 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -171,6 +171,7 @@ import com.nextcloud.talk.utils.CapabilitiesUtil import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability import com.nextcloud.talk.utils.CapabilitiesUtil.retentionOfEventRooms import com.nextcloud.talk.utils.CapabilitiesUtil.retentionOfInstantMeetingRoom +import com.nextcloud.talk.utils.CapabilitiesUtil.retentionOfClassifiedRoom import com.nextcloud.talk.utils.CapabilitiesUtil.retentionOfSIPRoom import com.nextcloud.talk.utils.ContactUtils import com.nextcloud.talk.utils.ConversationUtils @@ -1432,6 +1433,24 @@ class ChatActivity : } } + if (state.conversationModel.objectType == ConversationEnums.ObjectType.CLASSIFIED && + hasSpreedFeatureCapability( + conversationUser?.capabilities!!.spreedCapability!!, + SpreedFeatures.UNBIND_CONVERSATION + ) + ) { + val retentionPeriod = retentionOfClassifiedRoom(spreedCapabilities) + val systemMessage = state.conversationModel.lastMessage?.systemMessageType + if (retentionPeriod != 0 && + ( + systemMessage == ChatMessage.SystemMessageType.CALL_ENDED || + systemMessage == ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE + ) + ) { + showConversationDeletionWarning(retentionPeriod) + } + } + updateRoomTimerHandler(MILLIS_250) } @@ -1891,6 +1910,7 @@ class ChatActivity : val isOneToOne = isOneToOneConversation() val capabilitiesReady = ::spreedCapabilities.isInitialized + val isClassified = conversation != null && capabilitiesReady && ConversationUtils.isClassified(conversation, spreedCapabilities) chatToolbarState = chatToolbarState.copy( title = buildToolbarTitle(conversation), subtitle = buildToolbarSubtitle(conversation), @@ -1906,7 +1926,8 @@ class ChatActivity : showEventMenu = conversation?.objectType == ConversationEnums.ObjectType.EVENT, supportsSilentCall = capabilitiesReady && hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.SILENT_CALL) && - !isChatThread() + !isChatThread(), + isClassified = isClassified ) } diff --git a/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbar.kt b/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbar.kt index 9abcd73532..774add3239 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbar.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbar.kt @@ -21,7 +21,16 @@ import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.graphics.Color import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -311,7 +320,30 @@ private fun ConversationHeader(state: ChatToolbarState, onClick: (() -> Unit)?) overflow = TextOverflow.Ellipsis, color = MaterialTheme.colorScheme.onSurface ) - if (state.subtitle.isNotEmpty()) { + if (state.isClassified) { + val infiniteTransition = rememberInfiniteTransition() + val alpha by infiniteTransition.animateFloat( + initialValue = 1f, + targetValue = 0.3f, + animationSpec = infiniteRepeatable( + animation = tween(1000, easing = LinearEasing), + repeatMode = RepeatMode.Reverse + ), + label = "alpha" + ) + Surface( + modifier = Modifier.padding(top = 2.dp), + color = Color.Red.copy(alpha = alpha), + shape = RoundedCornerShape(4.dp) + ) { + Text( + text = "🛡️ Classified conversation", + color = Color.White, + fontSize = 10.sp, + modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp) + ) + } + } else if (state.subtitle.isNotEmpty()) { Text( text = state.subtitle, fontSize = 12.sp, diff --git a/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbarState.kt b/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbarState.kt index 2b7d2dfa71..45d7f00339 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbarState.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbarState.kt @@ -32,5 +32,6 @@ data class ChatToolbarState( /** Whether tapping the title area should open conversation info. */ val titleClickable: Boolean = false, /** Whether the server capability SILENT_CALL is available (enables long-press on call buttons). */ - val supportsSilentCall: Boolean = false + val supportsSilentCall: Boolean = false, + val isClassified: Boolean = false ) diff --git a/app/src/main/java/com/nextcloud/talk/chat/ui/MessageActionsBottomSheet.kt b/app/src/main/java/com/nextcloud/talk/chat/ui/MessageActionsBottomSheet.kt index e2d1e34871..4098fe1399 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ui/MessageActionsBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ui/MessageActionsBottomSheet.kt @@ -236,11 +236,13 @@ internal fun buildMessageActionsState( hasUserId && hasUserActorId && conversation?.type != ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL && - isOnline, + isOnline && + !(conversation != null && ConversationUtils.isClassified(conversation, spreedCapabilities)), showOpenThread = message.isThread && conversationThreadId == null, showForward = ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == messageType && !(message.isDeletedCommentMessage || message.isDeleted) && - isOnline, + isOnline && + !(conversation != null && ConversationUtils.isClassified(conversation, spreedCapabilities)), showEdit = isMessageEditable, showCopy = !message.isDeleted, showCopyMessageLink = !message.isDeleted && diff --git a/app/src/main/java/com/nextcloud/talk/chat/ui/model/ChatMessageUi.kt b/app/src/main/java/com/nextcloud/talk/chat/ui/model/ChatMessageUi.kt index ea5f4ea6c8..6b9826bb3d 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ui/model/ChatMessageUi.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ui/model/ChatMessageUi.kt @@ -74,7 +74,8 @@ sealed interface MessageTypeContent { val animateGif: Boolean = false, val blurhash: String? = null, val width: Int? = null, - val height: Int? = null + val height: Int? = null, + val isClassified: Boolean = false ) : MessageTypeContent data class Geolocation(val id: String, val name: String, val lat: Double, val lon: Double) : MessageTypeContent @@ -110,7 +111,8 @@ fun ChatMessage.toUiModel( user: User, chatMessage: ChatMessage, lastCommonReadMessageId: Int, - parentMessage: ChatMessage? + parentMessage: ChatMessage?, + isClassified: Boolean = false ): ChatMessageUi = ChatMessageUi( id = jsonMessageId, @@ -132,7 +134,7 @@ fun ChatMessage.toUiModel( ), timestamp = timestamp, date = dateKey(), - content = getMessageTypeContent(user, chatMessage), + content = getMessageTypeContent(user, chatMessage, isClassified), roomToken = token, activeUserId = user.userId, activeUserBaseUrl = user.baseUrl, @@ -243,14 +245,14 @@ fun resolveStatusIcon( else -> MessageStatusIcon.SENT } -fun getMessageTypeContent(user: User, message: ChatMessage): MessageTypeContent? = +fun getMessageTypeContent(user: User, message: ChatMessage, isClassified: Boolean = false): MessageTypeContent? = if (message.isSystemMessage) { MessageTypeContent.SystemMessage } else if (message.isVoiceMessage) { getVoiceContent(message) } else if (message.hasFileAttachment) { - getMediaContent(user, message) + getMediaContent(user, message, isClassified) } else if (message.hasGeoLocation) { getGeolocationContent(message) } else if (message.hasPoll) { @@ -263,7 +265,7 @@ fun getMessageTypeContent(user: User, message: ChatMessage): MessageTypeContent? ?: MessageTypeContent.RegularText } -fun getMediaContent(user: User, message: ChatMessage): MessageTypeContent.Media { +fun getMediaContent(user: User, message: ChatMessage, isClassified: Boolean = false): MessageTypeContent.Media { val mimetype = message.fileParameters.mimetype val drawableResourceId = DrawableUtils.getDrawableResourceIdForMimeType(mimetype) @@ -285,7 +287,8 @@ fun getMediaContent(user: User, message: ChatMessage): MessageTypeContent.Media animateGif = animateGif, blurhash = message.fileParameters.blurhash, width = message.fileParameters.width, - height = message.fileParameters.height + height = message.fileParameters.height, + isClassified = isClassified ) } 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 0eb664ba8f..921ec3a486 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 @@ -1013,7 +1013,9 @@ class ChatViewModel @AssistedInject constructor( val lastCommonRead: Int, val parentMap: Map, val conversationLastRead: Int, - val expandedParents: Set = emptySet() + val expandedParents: Set = emptySet(), + val conversation: ConversationModel? = null, + val capabilities: SpreedCapability? = null ) data class CallStartedIndicatorData( @@ -1044,13 +1046,14 @@ class ChatViewModel @AssistedInject constructor( messagesFlow, getLastCommonReadFlow.onStart { emit(0) }, parentMessagesFlow, - conversationFlow.map { it.lastReadMessage }, + combine(conversationFlow, _spreedCapabilities) { conv, caps -> conv to caps }, expandedSystemMessageParents - ) { messages, lastCommonRead, parentMap, conversationLastRead, expandedParents -> - CombinedInput(messages, lastCommonRead, parentMap, conversationLastRead, expandedParents) + ) { messages, lastCommonRead, parentMap, convAndCaps, expandedParents -> + val (conversation, capabilities) = convAndCaps + CombinedInput(messages, lastCommonRead, parentMap, conversation.lastReadMessage, expandedParents, conversation, capabilities) } .debounce(MESSAGES_REBUILD_DEBOUNCE_MS) - .map { (messages, lastCommonRead, parentMap, conversationLastRead, expandedParents) -> + .map { (messages, lastCommonRead, parentMap, conversationLastRead, expandedParents, conversation, capabilities) -> val messageMap: Map = messages.associateBy { it.jsonMessageId.toLong() } val combinedMap: Map = messageMap + parentMap @@ -1059,6 +1062,7 @@ class ChatViewModel @AssistedInject constructor( parentIds.filterNot { parentId -> combinedMap.containsKey(parentId) } .distinct() + val isClassified = conversation != null && ConversationUtils.isClassified(conversation, capabilities) val user = currentUserFlow.value applyMessageGrouping(messages) applySystemMessageGrouping(messages) @@ -1068,7 +1072,8 @@ class ChatViewModel @AssistedInject constructor( user = user ?: currentUser, chatMessage = message, lastCommonReadMessageId = lastCommonRead, - parentMessage = parent + parentMessage = parent, + isClassified = isClassified ) } diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoUiState.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoUiState.kt index 37f21a3da1..60a63582d8 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoUiState.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoUiState.kt @@ -41,6 +41,7 @@ data class ConversationInfoUiState( val showImportantConversation: Boolean = false, val sensitiveConversation: Boolean = false, val showSensitiveConversation: Boolean = false, + val isClassified: Boolean = false, val lobbyEnabled: Boolean = false, val showWebinarSettings: Boolean = false, diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/ui/ConversationInfoScreen.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/ui/ConversationInfoScreen.kt index 3fd87bd91b..008d2e5ca3 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfo/ui/ConversationInfoScreen.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/ui/ConversationInfoScreen.kt @@ -391,12 +391,13 @@ private fun SettingsRow( subtitle: String? = null, @DrawableRes iconRes: Int? = null, checked: Boolean? = null, + enabled: Boolean = true, onClick: (() -> Unit)? = null ) { Row( modifier = Modifier .fillMaxWidth() - .then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier) + .then(if (onClick != null && enabled) Modifier.clickable(onClick = onClick) else Modifier) .padding(horizontal = 16.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically ) { @@ -420,7 +421,7 @@ private fun SettingsRow( } } if (checked != null) { - Switch(checked = checked, onCheckedChange = null) + Switch(checked = checked, onCheckedChange = null, enabled = enabled) } } } @@ -482,7 +483,8 @@ private fun NotificationSettingsSection(state: ConversationInfoUiState, callback SettingsRow( title = stringResource(R.string.nc_sensitive_conversation), subtitle = stringResource(R.string.nc_sensitive_conversation_hint), - checked = state.sensitiveConversation, + checked = state.isClassified || state.sensitiveConversation, + enabled = !state.isClassified, onClick = callbacks.onSensitiveConversationClick ) } diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt index 927c9a943a..93bd94cecf 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt @@ -1,4 +1,4 @@ -/* +/* * Nextcloud Talk - Android Client * * SPDX-FileCopyrightText: 2023 Marcel Hibbe @@ -341,17 +341,18 @@ class ConversationInfoViewModel @Inject constructor( "" } + val isClassified = ConversationUtils.isClassified(conversationModel, spreedCapabilities) val recordingConsentType = CapabilitiesUtil.getRecordingConsentType(spreedCapabilities) val showRecordingConsent = isModerator && !isNoteToSelf && - recordingConsentType != CapabilitiesUtil.RECORDING_CONSENT_NOT_REQUIRED + recordingConsentType != CapabilitiesUtil.RECORDING_CONSENT_NOT_REQUIRED && !isClassified val showRecordingConsentSwitch = showRecordingConsent && recordingConsentType == CapabilitiesUtil.RECORDING_CONSENT_DEPEND_ON_CONVERSATION val showRecordingConsentAll = showRecordingConsent && recordingConsentType == CapabilitiesUtil.RECORDING_CONSENT_REQUIRED val recordingConsentForConversation = conversationModel.recordingConsentRequired == RECORDING_CONSENT_REQUIRED_FOR_CONVERSATION - val recordingConsentEnabled = !conversationModel.hasCall + val recordingConsentEnabled = !conversationModel.hasCall && !isClassified val showCallNotifications = !isSystem && conversationModel.notificationCalls != null && @@ -367,12 +368,12 @@ class ConversationInfoViewModel @Inject constructor( val canLeave = conversationModel.canLeaveConversation val canDelete = conversationModel.canDeleteConversation - val showGuestAccess = canModerate - val guestsAllowed = isPublic + val showGuestAccess = canModerate && !isClassified + val guestsAllowed = isPublic && !isClassified val hasPassword = guestsAllowed && conversationModel.hasPassword val showPasswordProtection = guestsAllowed val showResendInvitations = guestsAllowed && - user.capabilities?.spreedCapability?.features?.contains("sip-support") == true + user.capabilities?.spreedCapability?.features?.contains("sip-support") == true && !isClassified val showSharedItems = hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.RICH_OBJECT_LIST_MEDIA) && conversationModel.remoteServer.isNullOrEmpty() @@ -381,7 +382,7 @@ class ConversationInfoViewModel @Inject constructor( val isWebinarRoom = isGroup || isPublic val showWebinarSettings = hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.WEBINARY_LOBBY) && isWebinarRoom && - canModerate + canModerate && !isClassified val showImportantConversation = hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.IMPORTANT_CONVERSATIONS) @@ -438,6 +439,7 @@ class ConversationInfoViewModel @Inject constructor( showImportantConversation = showImportantConversation, sensitiveConversation = sensitiveConversation, showSensitiveConversation = showSensitiveConversation, + isClassified = isClassified, lobbyEnabled = isLobbyEnabled, showWebinarSettings = showWebinarSettings, lobbyTimerLabel = lobbyTimerLabel, 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 ce0347ae84..1066c423c9 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 @@ -47,7 +47,8 @@ class ConversationEnums { EVENT, PHONE_TEMPORARY, PHONE_PERSIST, - INSTANT_MEETING + INSTANT_MEETING, + CLASSIFIED } enum class Preset { diff --git a/app/src/main/java/com/nextcloud/talk/models/json/converters/ConversationObjectTypeConverter.kt b/app/src/main/java/com/nextcloud/talk/models/json/converters/ConversationObjectTypeConverter.kt index b7e429fe9a..2bdb42efe9 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/converters/ConversationObjectTypeConverter.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/converters/ConversationObjectTypeConverter.kt @@ -19,6 +19,7 @@ class ConversationObjectTypeConverter : StringBasedTypeConverter ConversationEnums.ObjectType.PHONE_PERSIST "phone_temporary" -> ConversationEnums.ObjectType.PHONE_TEMPORARY "instant_meeting" -> ConversationEnums.ObjectType.INSTANT_MEETING + "classified" -> ConversationEnums.ObjectType.CLASSIFIED else -> ConversationEnums.ObjectType.DEFAULT } @@ -35,6 +36,7 @@ class ConversationObjectTypeConverter : StringBasedTypeConverter "phone_persist" ConversationEnums.ObjectType.PHONE_TEMPORARY -> "phone_temporary" ConversationEnums.ObjectType.INSTANT_MEETING -> "instant_meeting" + ConversationEnums.ObjectType.CLASSIFIED -> "classified" else -> "" } } diff --git a/app/src/main/java/com/nextcloud/talk/ui/chat/MediaMessage.kt b/app/src/main/java/com/nextcloud/talk/ui/chat/MediaMessage.kt index 6fa5e9d5f7..d07efe2ca7 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/chat/MediaMessage.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/chat/MediaMessage.kt @@ -139,13 +139,17 @@ fun MediaMessage( val h = typeContent.height if (w != null && h != null && w > 0 && h > 0) w.toFloat() / h else null } - val loadedImage = remember(retryAwarePreviewUrl) { - load( - imageUri = retryAwarePreviewUrl, - context = context, - errorPlaceholderImage = typeContent.drawableResourceId, - animated = typeContent.animateGif - ) + val loadedImage = remember(retryAwarePreviewUrl, typeContent.isClassified) { + if (typeContent.isClassified) { + null + } else { + load( + imageUri = retryAwarePreviewUrl, + context = context, + errorPlaceholderImage = typeContent.drawableResourceId, + animated = typeContent.animateGif + ) + } } val fallbackPainter = painterResource(typeContent.drawableResourceId) diff --git a/app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt b/app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt index e172544680..613b693ba5 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt @@ -65,7 +65,8 @@ enum class SpreedFeatures(val value: String) { PINNED_MESSAGES("pinned-messages"), SCHEDULED_MESSAGES("scheduled-messages"), REACT_PERMISSION("react-permission"), - CONVERSATION_PRESETS("conversation-presets") + CONVERSATION_PRESETS("conversation-presets"), + CLASSIFIED_CONVERSATIONS("classified-conversations") } @Suppress("TooManyFunctions") @@ -209,6 +210,16 @@ object CapabilitiesUtil { return 0 } + fun retentionOfClassifiedRoom(spreedCapabilities: SpreedCapability): Int { + if (spreedCapabilities.config?.containsKey("conversations") == true) { + val map = spreedCapabilities.config!!["conversations"] + if (map?.containsKey("retention-classified") == true) { + return map["retention-classified"].toString().toInt() + } + } + return 0 + } + @JvmStatic fun isCallRecordingAvailable(spreedCapabilities: SpreedCapability): Boolean { if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.RECORDING_V1) && diff --git a/app/src/main/java/com/nextcloud/talk/utils/ConversationUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/ConversationUtils.kt index 85e89b0d3e..b61fbad2ba 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ConversationUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/ConversationUtils.kt @@ -58,4 +58,11 @@ object ConversationUtils { fun isNoteToSelfConversation(currentConversation: ConversationModel?): Boolean = currentConversation != null && currentConversation.type == ConversationEnums.ConversationType.NOTE_TO_SELF + + fun isClassified( + conversation: ConversationModel, + spreedCapabilities: SpreedCapability? + ): Boolean = + CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.CLASSIFIED_CONVERSATIONS) && + ((conversation.attributes ?: 0) and 4) != 0 } From 775bfa02325a55acacc0f8615303cec12edde579 Mon Sep 17 00:00:00 2001 From: thirumani-vihaan Date: Mon, 27 Jul 2026 23:20:28 +0530 Subject: [PATCH 2/2] Fix edge cases: Share Link, Translation, and Call Recording button Signed-off-by: thirumani-vihaan --- AUTHORS.md | 1 + .../nextcloud/talk/activities/CallActivity.kt | 13 ++++++++---- .../com/nextcloud/talk/chat/ChatActivity.kt | 4 +++- .../talk/chat/MessageInputFragment.kt | 1 - .../com/nextcloud/talk/chat/ui/ChatToolbar.kt | 9 ++++---- .../talk/chat/ui/MessageActionsBottomSheet.kt | 12 +++++++---- .../talk/chat/viewmodels/ChatViewModel.kt | 21 +++++++++++++++++-- .../viewmodel/ConversationInfoViewModel.kt | 11 ++++++---- .../nextcloud/talk/utils/ConversationUtils.kt | 9 ++++++-- 9 files changed, 59 insertions(+), 22 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index c2c510aea6..e1937eebf8 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -52,6 +52,7 @@ * Stefan Niedermann * Stephan Ritscher * Tarek Loubani +* thirumani-vihaan * Tilo Spannagel * Tim Krüger * Tobias Kaminsky diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt index f1550a17d3..49bc0cfa70 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt @@ -3154,10 +3154,15 @@ class CallActivity : CallBaseActivity() { } val isAllowedToStartOrStopRecording: Boolean - get() = ( - isCallRecordingAvailable(conversationUser!!.capabilities!!.spreedCapability!!) && - isModerator - ) + get() { + val spreedCap = conversationUser.capabilities?.spreedCapability + val isClassified = currentConversation?.let { + com.nextcloud.talk.utils.ConversationUtils.isClassified(it, spreedCap) + } ?: false + return isCallRecordingAvailable(conversationUser.capabilities!!.spreedCapability!!) && + isModerator && + !isClassified + } val isAllowedToRaiseHand: Boolean get() = hasSpreedFeatureCapability( conversationUser.capabilities!!.spreedCapability!!, 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 9f276c1eae..1d068368d7 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -1910,7 +1910,9 @@ class ChatActivity : val isOneToOne = isOneToOneConversation() val capabilitiesReady = ::spreedCapabilities.isInitialized - val isClassified = conversation != null && capabilitiesReady && ConversationUtils.isClassified(conversation, spreedCapabilities) + val isClassified = conversation != null && + capabilitiesReady && + ConversationUtils.isClassified(conversation, spreedCapabilities) chatToolbarState = chatToolbarState.copy( title = buildToolbarTitle(conversation), subtitle = buildToolbarSubtitle(conversation), diff --git a/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt index b5a16950c0..bb1baa2766 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt @@ -331,7 +331,6 @@ class MessageInputFragment : Fragment() { override fun onSuccess(result: Drawable) { binding.fragmentCallStarted.callAuthorChip.chipIcon = result - } }) .build() diff --git a/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbar.kt b/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbar.kt index 774add3239..72c32ec882 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbar.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbar.kt @@ -30,7 +30,6 @@ import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.ui.graphics.Color import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -83,6 +82,8 @@ import coil.request.ImageRequest import com.nextcloud.talk.R import com.nextcloud.talk.chat.MenuItemData +private const val BANNER_ANIMATION_DURATION_MS = 1000 + @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable fun ChatToolbar(state: ChatToolbarState, callbacks: ChatToolbarCallbacks, modifier: Modifier = Modifier) { @@ -326,19 +327,19 @@ private fun ConversationHeader(state: ChatToolbarState, onClick: (() -> Unit)?) initialValue = 1f, targetValue = 0.3f, animationSpec = infiniteRepeatable( - animation = tween(1000, easing = LinearEasing), + animation = tween(BANNER_ANIMATION_DURATION_MS, easing = LinearEasing), repeatMode = RepeatMode.Reverse ), label = "alpha" ) Surface( modifier = Modifier.padding(top = 2.dp), - color = Color.Red.copy(alpha = alpha), + color = MaterialTheme.colorScheme.error.copy(alpha = alpha), shape = RoundedCornerShape(4.dp) ) { Text( text = "🛡️ Classified conversation", - color = Color.White, + color = MaterialTheme.colorScheme.onError, fontSize = 10.sp, modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp) ) diff --git a/app/src/main/java/com/nextcloud/talk/chat/ui/MessageActionsBottomSheet.kt b/app/src/main/java/com/nextcloud/talk/chat/ui/MessageActionsBottomSheet.kt index 4098fe1399..4b315fc953 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ui/MessageActionsBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ui/MessageActionsBottomSheet.kt @@ -225,6 +225,8 @@ internal fun buildMessageActionsState( val hasUserId = user?.userId?.isNotEmpty() == true && user.userId != "?" val hasUserActorId = message.actorType == "users" && message.actorId != conversation?.actorId + val isClassifiedRoom = conversation != null && ConversationUtils.isClassified(conversation, spreedCapabilities) + return MessageActionsState( showEmojiBar = showEmojiBar, selfReactions = message.reactionsSelf?.toSet() ?: emptySet(), @@ -237,12 +239,12 @@ internal fun buildMessageActionsState( hasUserActorId && conversation?.type != ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL && isOnline && - !(conversation != null && ConversationUtils.isClassified(conversation, spreedCapabilities)), + !isClassifiedRoom, showOpenThread = message.isThread && conversationThreadId == null, showForward = ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == messageType && !(message.isDeletedCommentMessage || message.isDeleted) && isOnline && - !(conversation != null && ConversationUtils.isClassified(conversation, spreedCapabilities)), + !isClassifiedRoom, showEdit = isMessageEditable, showCopy = !message.isDeleted, showCopyMessageLink = !message.isDeleted && @@ -261,10 +263,12 @@ internal fun buildMessageActionsState( showTranslate = !message.isDeleted && ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == messageType && CapabilitiesUtil.isTranslationsSupported(spreedCapabilities) && - isOnline, + isOnline && + !isClassifiedRoom, showShareToNote = !message.isDeleted && !ConversationUtils.isNoteToSelfConversation(conversation) && - isOnline, + isOnline && + !isClassifiedRoom, showShare = messageHasFileAttachment || (messageHasRegularText && isOnline), showSave = messageHasFileAttachment, showOpenInFiles = messageHasFileAttachment && isOnline, 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 921ec3a486..2e5af8fbf1 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 @@ -1050,10 +1050,27 @@ class ChatViewModel @AssistedInject constructor( expandedSystemMessageParents ) { messages, lastCommonRead, parentMap, convAndCaps, expandedParents -> val (conversation, capabilities) = convAndCaps - CombinedInput(messages, lastCommonRead, parentMap, conversation.lastReadMessage, expandedParents, conversation, capabilities) + CombinedInput( + messages, + lastCommonRead, + parentMap, + conversation.lastReadMessage, + expandedParents, + conversation, + capabilities + ) } .debounce(MESSAGES_REBUILD_DEBOUNCE_MS) - .map { (messages, lastCommonRead, parentMap, conversationLastRead, expandedParents, conversation, capabilities) -> + .map { input -> + val ( + messages, + lastCommonRead, + parentMap, + conversationLastRead, + expandedParents, + conversation, + capabilities + ) = input val messageMap: Map = messages.associateBy { it.jsonMessageId.toLong() } val combinedMap: Map = messageMap + parentMap diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt index 93bd94cecf..a1e1bde1e9 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt @@ -345,7 +345,8 @@ class ConversationInfoViewModel @Inject constructor( val recordingConsentType = CapabilitiesUtil.getRecordingConsentType(spreedCapabilities) val showRecordingConsent = isModerator && !isNoteToSelf && - recordingConsentType != CapabilitiesUtil.RECORDING_CONSENT_NOT_REQUIRED && !isClassified + recordingConsentType != CapabilitiesUtil.RECORDING_CONSENT_NOT_REQUIRED && + !isClassified val showRecordingConsentSwitch = showRecordingConsent && recordingConsentType == CapabilitiesUtil.RECORDING_CONSENT_DEPEND_ON_CONVERSATION val showRecordingConsentAll = @@ -373,7 +374,8 @@ class ConversationInfoViewModel @Inject constructor( val hasPassword = guestsAllowed && conversationModel.hasPassword val showPasswordProtection = guestsAllowed val showResendInvitations = guestsAllowed && - user.capabilities?.spreedCapability?.features?.contains("sip-support") == true && !isClassified + user.capabilities?.spreedCapability?.features?.contains("sip-support") == true && + !isClassified val showSharedItems = hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.RICH_OBJECT_LIST_MEDIA) && conversationModel.remoteServer.isNullOrEmpty() @@ -382,7 +384,8 @@ class ConversationInfoViewModel @Inject constructor( val isWebinarRoom = isGroup || isPublic val showWebinarSettings = hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.WEBINARY_LOBBY) && isWebinarRoom && - canModerate && !isClassified + canModerate && + !isClassified val showImportantConversation = hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.IMPORTANT_CONVERSATIONS) @@ -413,7 +416,7 @@ class ConversationInfoViewModel @Inject constructor( } val showEditButton = canModerate && hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.AVATAR) - val showShareConversationButton = !isNoteToSelf + val showShareConversationButton = !isNoteToSelf && !isClassified val showListBans = canModerate && !isOne2One val showMessageExpiration = isModerator && hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MESSAGE_EXPIRATION) diff --git a/app/src/main/java/com/nextcloud/talk/utils/ConversationUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/ConversationUtils.kt index b61fbad2ba..6af15ccc1e 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ConversationUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/ConversationUtils.kt @@ -13,6 +13,7 @@ import com.nextcloud.talk.models.json.participants.Participant object ConversationUtils { private val TAG = ConversationUtils::class.java.simpleName + private const val CLASSIFIED_ATTRIBUTE_BIT = 4 fun isPublic(conversation: ConversationModel): Boolean = ConversationEnums.ConversationType.ROOM_PUBLIC_CALL == conversation.type @@ -59,10 +60,14 @@ object ConversationUtils { currentConversation != null && currentConversation.type == ConversationEnums.ConversationType.NOTE_TO_SELF + fun isClassified(conversation: ConversationModel, spreedCapabilities: SpreedCapability?): Boolean = + CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.CLASSIFIED_CONVERSATIONS) && + ((conversation.attributes ?: 0) and CLASSIFIED_ATTRIBUTE_BIT) != 0 + fun isClassified( - conversation: ConversationModel, + conversation: com.nextcloud.talk.models.json.conversations.Conversation, spreedCapabilities: SpreedCapability? ): Boolean = CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.CLASSIFIED_CONVERSATIONS) && - ((conversation.attributes ?: 0) and 4) != 0 + ((conversation.attributes ?: 0) and CLASSIFIED_ATTRIBUTE_BIT) != 0 }