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 2710b73b47..c2d11c60c1 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 @@ -1396,41 +1397,19 @@ class ChatActivity : } } - if (state.conversationModel.objectType == ConversationEnums.ObjectType.PHONE_TEMPORARY && - hasSpreedFeatureCapability( - conversationUser?.capabilities!!.spreedCapability!!, - SpreedFeatures.UNBIND_CONVERSATION - ) - ) { - val retentionPeriod = retentionOfSIPRoom(spreedCapabilities) - val systemMessage = currentConversation?.lastMessage?.systemMessageType - if (retentionPeriod != 0 && - ( - systemMessage == ChatMessage.SystemMessageType.CALL_ENDED || - systemMessage == ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE - ) - ) { - showConversationDeletionWarning(retentionPeriod) - } - } - - if (state.conversationModel.objectType == ConversationEnums.ObjectType.INSTANT_MEETING && - hasSpreedFeatureCapability( - conversationUser?.capabilities!!.spreedCapability!!, - SpreedFeatures.UNBIND_CONVERSATION - ) - ) { - val retentionPeriod = retentionOfInstantMeetingRoom(spreedCapabilities) - val systemMessage = state.conversationModel.lastMessage?.systemMessageType - if (retentionPeriod != 0 && - ( - systemMessage == ChatMessage.SystemMessageType.CALL_ENDED || - systemMessage == ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE - ) - ) { - showConversationDeletionWarning(retentionPeriod) - } - } + val initialSystemMessage = state.conversationModel.lastMessage?.systemMessageType + maybeShowCallEndedDeletionWarning( + ConversationEnums.ObjectType.PHONE_TEMPORARY, + initialSystemMessage + ) + maybeShowCallEndedDeletionWarning( + ConversationEnums.ObjectType.INSTANT_MEETING, + initialSystemMessage + ) + maybeShowCallEndedDeletionWarning( + ConversationEnums.ObjectType.CLASSIFIED, + initialSystemMessage + ) updateRoomTimerHandler(MILLIS_250) } @@ -1443,6 +1422,14 @@ class ChatActivity : } } + lifecycleScope.launch { + chatViewModel.callEndedSystemMessage.collect { systemMessage -> + maybeShowCallEndedDeletionWarning(ConversationEnums.ObjectType.PHONE_TEMPORARY, systemMessage) + maybeShowCallEndedDeletionWarning(ConversationEnums.ObjectType.INSTANT_MEETING, systemMessage) + maybeShowCallEndedDeletionWarning(ConversationEnums.ObjectType.CLASSIFIED, systemMessage) + } + } + chatViewModel.joinRoomViewState.observe(this) { state -> when (state) { is ChatViewModel.JoinRoomSuccessState -> { @@ -1734,6 +1721,40 @@ class ChatActivity : chatViewModel.setUnreadMessagesMarker(false) } + private fun retentionPeriodFor(objectType: ConversationEnums.ObjectType): Int = + when (objectType) { + ConversationEnums.ObjectType.PHONE_TEMPORARY -> retentionOfSIPRoom(spreedCapabilities) + ConversationEnums.ObjectType.INSTANT_MEETING -> retentionOfInstantMeetingRoom(spreedCapabilities) + ConversationEnums.ObjectType.CLASSIFIED -> retentionOfClassifiedRoom(spreedCapabilities) + else -> 0 + } + + private fun maybeShowCallEndedDeletionWarning( + objectType: ConversationEnums.ObjectType, + systemMessage: ChatMessage.SystemMessageType? + ) { + val conversation = currentConversation ?: return + if (conversation.objectType != objectType) { + return + } + if (!hasSpreedFeatureCapability( + conversationUser?.capabilities!!.spreedCapability!!, + SpreedFeatures.UNBIND_CONVERSATION + ) + ) { + return + } + if (systemMessage != ChatMessage.SystemMessageType.CALL_ENDED && + systemMessage != ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE + ) { + return + } + val retentionPeriod = retentionPeriodFor(objectType) + if (retentionPeriod != 0) { + showConversationDeletionWarning(retentionPeriod) + } + } + fun showConversationDeletionWarning(retentionPeriod: Int) { binding.conversationDeleteNoticeComposeView.apply { visibility = View.VISIBLE @@ -1891,6 +1912,9 @@ 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 +1930,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..c62f2ea10f 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 @@ -19,14 +19,17 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Shield import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api @@ -64,6 +67,7 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -311,7 +315,34 @@ private fun ConversationHeader(state: ChatToolbarState, onClick: (() -> Unit)?) overflow = TextOverflow.Ellipsis, color = MaterialTheme.colorScheme.onSurface ) - if (state.subtitle.isNotEmpty()) { + if (state.isClassified) { + Surface( + modifier = Modifier.padding(top = 2.dp), + color = MaterialTheme.colorScheme.error, + shape = RoundedCornerShape(4.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(horizontal = 6.dp, vertical = 0.dp) + ) { + Icon( + imageVector = Icons.Default.Shield, + contentDescription = null, + tint = MaterialTheme.colorScheme.onError, + modifier = Modifier.size(12.dp) + ) + Spacer(Modifier.width(4.dp)) + Text( + text = stringResource(R.string.nc_classified_conversation), + color = MaterialTheme.colorScheme.onError, + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + } 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 3a93ed1887..22b1c6781f 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 @@ -226,6 +226,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,11 +239,13 @@ internal fun buildMessageActionsState( hasUserId && hasUserActorId && conversation?.type != ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL && - isOnline, + isOnline && + !isClassifiedRoom, showOpenThread = message.isThread && conversationThreadId == null, showForward = ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == messageType && !(message.isDeletedCommentMessage || message.isDeleted) && - isOnline, + isOnline && + !isClassifiedRoom, showEdit = isMessageEditable, showCopy = !message.isDeleted, showCopyMessageLink = !message.isDeleted && @@ -263,7 +267,8 @@ internal fun buildMessageActionsState( isOnline, 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/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..ff5ae076f8 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 @@ -90,6 +90,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch @@ -1013,7 +1014,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( @@ -1034,6 +1037,10 @@ class ChatViewModel @AssistedInject constructor( ) } + private val _callEndedSystemMessage = MutableSharedFlow(extraBufferCapacity = 1) + val callEndedSystemMessage: SharedFlow + get() = _callEndedSystemMessage + private data class ProcessedMessages(val items: List, val missingParentIds: List) private fun observeMessages() { @@ -1044,13 +1051,31 @@ 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 { input -> + val ( + messages, + lastCommonRead, + parentMap, + conversationLastRead, + expandedParents, + conversation, + capabilities + ) = input val messageMap: Map = messages.associateBy { it.jsonMessageId.toLong() } val combinedMap: Map = messageMap + parentMap @@ -1059,6 +1084,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 +1094,8 @@ class ChatViewModel @AssistedInject constructor( user = user ?: currentUser, chatMessage = message, lastCommonReadMessageId = lastCommonRead, - parentMessage = parent + parentMessage = parent, + isClassified = isClassified ) } @@ -1376,16 +1403,28 @@ class ChatViewModel @AssistedInject constructor( return chatMessageMap.values.toList() } + private var hasSeenInitialCallSystemMessage = false + private var lastNotifiedCallEndedMessageId: Int? = null + private fun processCallSystemMessage(recent: ChatMessage) { + val isInitialSnapshot = !hasSeenInitialCallSystemMessage + hasSeenInitialCallSystemMessage = true + when (recent.systemMessageType) { ChatMessage.SystemMessageType.CALL_STARTED -> { _lastCallSystemMessage.tryEmit(recent) } ChatMessage.SystemMessageType.CALL_ENDED, - ChatMessage.SystemMessageType.CALL_MISSED, - ChatMessage.SystemMessageType.CALL_TRIED, ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE -> { _lastCallSystemMessage.tryEmit(null) + if (!isInitialSnapshot && lastNotifiedCallEndedMessageId != recent.jsonMessageId) { + _callEndedSystemMessage.tryEmit(recent.systemMessageType!!) + } + lastNotifiedCallEndedMessageId = recent.jsonMessageId + } + ChatMessage.SystemMessageType.CALL_MISSED, + ChatMessage.SystemMessageType.CALL_TRIED -> { + _lastCallSystemMessage.tryEmit(null) } else -> {} } 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..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 @@ -1,4 +1,4 @@ -/* +/* * Nextcloud Talk - Android Client * * SPDX-FileCopyrightText: 2023 Marcel Hibbe @@ -341,17 +341,19 @@ 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 +369,13 @@ 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 +384,8 @@ 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) @@ -412,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) @@ -438,6 +442,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..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 @@ -58,4 +59,15 @@ 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 CLASSIFIED_ATTRIBUTE_BIT) != 0 + + fun isClassified( + conversation: com.nextcloud.talk.models.json.conversations.Conversation, + spreedCapabilities: SpreedCapability? + ): Boolean = + CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.CLASSIFIED_CONVERSATIONS) && + ((conversation.attributes ?: 0) and CLASSIFIED_ATTRIBUTE_BIT) != 0 } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 21af10e0da..b9eb3224c7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -435,6 +435,7 @@ How to translate with transifex: Message preview will be disabled in conversation list and notifications Important conversation \"Do not disturb\" user status is ignored for important conversations + Classified conversation OK, all done! OK