Skip to content

Commit a0fe8a1

Browse files
Enhancement: Add classified conversations support (#6414)
Signed-off-by: thirumani-vihaan <arjunthirumani02@gmail.com>
1 parent 60c2140 commit a0fe8a1

14 files changed

Lines changed: 131 additions & 37 deletions

File tree

app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ import com.nextcloud.talk.utils.CapabilitiesUtil
171171
import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability
172172
import com.nextcloud.talk.utils.CapabilitiesUtil.retentionOfEventRooms
173173
import com.nextcloud.talk.utils.CapabilitiesUtil.retentionOfInstantMeetingRoom
174+
import com.nextcloud.talk.utils.CapabilitiesUtil.retentionOfClassifiedRoom
174175
import com.nextcloud.talk.utils.CapabilitiesUtil.retentionOfSIPRoom
175176
import com.nextcloud.talk.utils.ContactUtils
176177
import com.nextcloud.talk.utils.ConversationUtils
@@ -1432,6 +1433,24 @@ class ChatActivity :
14321433
}
14331434
}
14341435

1436+
if (state.conversationModel.objectType == ConversationEnums.ObjectType.CLASSIFIED &&
1437+
hasSpreedFeatureCapability(
1438+
conversationUser?.capabilities!!.spreedCapability!!,
1439+
SpreedFeatures.UNBIND_CONVERSATION
1440+
)
1441+
) {
1442+
val retentionPeriod = retentionOfClassifiedRoom(spreedCapabilities)
1443+
val systemMessage = state.conversationModel.lastMessage?.systemMessageType
1444+
if (retentionPeriod != 0 &&
1445+
(
1446+
systemMessage == ChatMessage.SystemMessageType.CALL_ENDED ||
1447+
systemMessage == ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE
1448+
)
1449+
) {
1450+
showConversationDeletionWarning(retentionPeriod)
1451+
}
1452+
}
1453+
14351454
updateRoomTimerHandler(MILLIS_250)
14361455
}
14371456

@@ -1891,6 +1910,7 @@ class ChatActivity :
18911910
val isOneToOne = isOneToOneConversation()
18921911
val capabilitiesReady = ::spreedCapabilities.isInitialized
18931912

1913+
val isClassified = conversation != null && capabilitiesReady && ConversationUtils.isClassified(conversation, spreedCapabilities)
18941914
chatToolbarState = chatToolbarState.copy(
18951915
title = buildToolbarTitle(conversation),
18961916
subtitle = buildToolbarSubtitle(conversation),
@@ -1906,7 +1926,8 @@ class ChatActivity :
19061926
showEventMenu = conversation?.objectType == ConversationEnums.ObjectType.EVENT,
19071927
supportsSilentCall = capabilitiesReady &&
19081928
hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.SILENT_CALL) &&
1909-
!isChatThread()
1929+
!isChatThread(),
1930+
isClassified = isClassified
19101931
)
19111932
}
19121933

app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbar.kt

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,16 @@ import androidx.compose.foundation.layout.WindowInsets
2121
import androidx.compose.foundation.layout.fillMaxWidth
2222
import androidx.compose.foundation.layout.size
2323
import androidx.compose.foundation.layout.width
24+
import androidx.compose.animation.core.LinearEasing
25+
import androidx.compose.animation.core.RepeatMode
26+
import androidx.compose.animation.core.animateFloat
27+
import androidx.compose.animation.core.infiniteRepeatable
28+
import androidx.compose.animation.core.rememberInfiniteTransition
29+
import androidx.compose.animation.core.tween
30+
import androidx.compose.foundation.layout.padding
2431
import androidx.compose.foundation.shape.CircleShape
32+
import androidx.compose.foundation.shape.RoundedCornerShape
33+
import androidx.compose.ui.graphics.Color
2534
import androidx.compose.foundation.text.BasicTextField
2635
import androidx.compose.foundation.text.KeyboardActions
2736
import androidx.compose.foundation.text.KeyboardOptions
@@ -311,7 +320,30 @@ private fun ConversationHeader(state: ChatToolbarState, onClick: (() -> Unit)?)
311320
overflow = TextOverflow.Ellipsis,
312321
color = MaterialTheme.colorScheme.onSurface
313322
)
314-
if (state.subtitle.isNotEmpty()) {
323+
if (state.isClassified) {
324+
val infiniteTransition = rememberInfiniteTransition()
325+
val alpha by infiniteTransition.animateFloat(
326+
initialValue = 1f,
327+
targetValue = 0.3f,
328+
animationSpec = infiniteRepeatable(
329+
animation = tween(1000, easing = LinearEasing),
330+
repeatMode = RepeatMode.Reverse
331+
),
332+
label = "alpha"
333+
)
334+
Surface(
335+
modifier = Modifier.padding(top = 2.dp),
336+
color = Color.Red.copy(alpha = alpha),
337+
shape = RoundedCornerShape(4.dp)
338+
) {
339+
Text(
340+
text = "🛡️ Classified conversation",
341+
color = Color.White,
342+
fontSize = 10.sp,
343+
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp)
344+
)
345+
}
346+
} else if (state.subtitle.isNotEmpty()) {
315347
Text(
316348
text = state.subtitle,
317349
fontSize = 12.sp,

app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbarState.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,6 @@ data class ChatToolbarState(
3232
/** Whether tapping the title area should open conversation info. */
3333
val titleClickable: Boolean = false,
3434
/** Whether the server capability SILENT_CALL is available (enables long-press on call buttons). */
35-
val supportsSilentCall: Boolean = false
35+
val supportsSilentCall: Boolean = false,
36+
val isClassified: Boolean = false
3637
)

app/src/main/java/com/nextcloud/talk/chat/ui/MessageActionsBottomSheet.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,11 +236,13 @@ internal fun buildMessageActionsState(
236236
hasUserId &&
237237
hasUserActorId &&
238238
conversation?.type != ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL &&
239-
isOnline,
239+
isOnline &&
240+
!(conversation != null && ConversationUtils.isClassified(conversation, spreedCapabilities)),
240241
showOpenThread = message.isThread && conversationThreadId == null,
241242
showForward = ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == messageType &&
242243
!(message.isDeletedCommentMessage || message.isDeleted) &&
243-
isOnline,
244+
isOnline &&
245+
!(conversation != null && ConversationUtils.isClassified(conversation, spreedCapabilities)),
244246
showEdit = isMessageEditable,
245247
showCopy = !message.isDeleted,
246248
showCopyMessageLink = !message.isDeleted &&

app/src/main/java/com/nextcloud/talk/chat/ui/model/ChatMessageUi.kt

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ sealed interface MessageTypeContent {
7474
val animateGif: Boolean = false,
7575
val blurhash: String? = null,
7676
val width: Int? = null,
77-
val height: Int? = null
77+
val height: Int? = null,
78+
val isClassified: Boolean = false
7879
) : MessageTypeContent
7980

8081
data class Geolocation(val id: String, val name: String, val lat: Double, val lon: Double) : MessageTypeContent
@@ -110,7 +111,8 @@ fun ChatMessage.toUiModel(
110111
user: User,
111112
chatMessage: ChatMessage,
112113
lastCommonReadMessageId: Int,
113-
parentMessage: ChatMessage?
114+
parentMessage: ChatMessage?,
115+
isClassified: Boolean = false
114116
): ChatMessageUi =
115117
ChatMessageUi(
116118
id = jsonMessageId,
@@ -132,7 +134,7 @@ fun ChatMessage.toUiModel(
132134
),
133135
timestamp = timestamp,
134136
date = dateKey(),
135-
content = getMessageTypeContent(user, chatMessage),
137+
content = getMessageTypeContent(user, chatMessage, isClassified),
136138
roomToken = token,
137139
activeUserId = user.userId,
138140
activeUserBaseUrl = user.baseUrl,
@@ -243,14 +245,14 @@ fun resolveStatusIcon(
243245
else -> MessageStatusIcon.SENT
244246
}
245247

246-
fun getMessageTypeContent(user: User, message: ChatMessage): MessageTypeContent? =
248+
fun getMessageTypeContent(user: User, message: ChatMessage, isClassified: Boolean = false): MessageTypeContent? =
247249

248250
if (message.isSystemMessage) {
249251
MessageTypeContent.SystemMessage
250252
} else if (message.isVoiceMessage) {
251253
getVoiceContent(message)
252254
} else if (message.hasFileAttachment) {
253-
getMediaContent(user, message)
255+
getMediaContent(user, message, isClassified)
254256
} else if (message.hasGeoLocation) {
255257
getGeolocationContent(message)
256258
} else if (message.hasPoll) {
@@ -263,7 +265,7 @@ fun getMessageTypeContent(user: User, message: ChatMessage): MessageTypeContent?
263265
?: MessageTypeContent.RegularText
264266
}
265267

266-
fun getMediaContent(user: User, message: ChatMessage): MessageTypeContent.Media {
268+
fun getMediaContent(user: User, message: ChatMessage, isClassified: Boolean = false): MessageTypeContent.Media {
267269
val mimetype = message.fileParameters.mimetype
268270
val drawableResourceId = DrawableUtils.getDrawableResourceIdForMimeType(mimetype)
269271

@@ -285,7 +287,8 @@ fun getMediaContent(user: User, message: ChatMessage): MessageTypeContent.Media
285287
animateGif = animateGif,
286288
blurhash = message.fileParameters.blurhash,
287289
width = message.fileParameters.width,
288-
height = message.fileParameters.height
290+
height = message.fileParameters.height,
291+
isClassified = isClassified
289292
)
290293
}
291294

app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,7 +1013,9 @@ class ChatViewModel @AssistedInject constructor(
10131013
val lastCommonRead: Int,
10141014
val parentMap: Map<Long, ChatMessage>,
10151015
val conversationLastRead: Int,
1016-
val expandedParents: Set<Int> = emptySet()
1016+
val expandedParents: Set<Int> = emptySet(),
1017+
val conversation: ConversationModel? = null,
1018+
val capabilities: SpreedCapability? = null
10171019
)
10181020

10191021
data class CallStartedIndicatorData(
@@ -1044,13 +1046,14 @@ class ChatViewModel @AssistedInject constructor(
10441046
messagesFlow,
10451047
getLastCommonReadFlow.onStart { emit(0) },
10461048
parentMessagesFlow,
1047-
conversationFlow.map { it.lastReadMessage },
1049+
combine(conversationFlow, _spreedCapabilities) { conv, caps -> conv to caps },
10481050
expandedSystemMessageParents
1049-
) { messages, lastCommonRead, parentMap, conversationLastRead, expandedParents ->
1050-
CombinedInput(messages, lastCommonRead, parentMap, conversationLastRead, expandedParents)
1051+
) { messages, lastCommonRead, parentMap, convAndCaps, expandedParents ->
1052+
val (conversation, capabilities) = convAndCaps
1053+
CombinedInput(messages, lastCommonRead, parentMap, conversation.lastReadMessage, expandedParents, conversation, capabilities)
10511054
}
10521055
.debounce(MESSAGES_REBUILD_DEBOUNCE_MS)
1053-
.map { (messages, lastCommonRead, parentMap, conversationLastRead, expandedParents) ->
1056+
.map { (messages, lastCommonRead, parentMap, conversationLastRead, expandedParents, conversation, capabilities) ->
10541057
val messageMap: Map<Long, ChatMessage> = messages.associateBy { it.jsonMessageId.toLong() }
10551058
val combinedMap: Map<Long, ChatMessage> = messageMap + parentMap
10561059

@@ -1059,6 +1062,7 @@ class ChatViewModel @AssistedInject constructor(
10591062
parentIds.filterNot { parentId -> combinedMap.containsKey(parentId) }
10601063
.distinct()
10611064

1065+
val isClassified = conversation != null && ConversationUtils.isClassified(conversation, capabilities)
10621066
val user = currentUserFlow.value
10631067
applyMessageGrouping(messages)
10641068
applySystemMessageGrouping(messages)
@@ -1068,7 +1072,8 @@ class ChatViewModel @AssistedInject constructor(
10681072
user = user ?: currentUser,
10691073
chatMessage = message,
10701074
lastCommonReadMessageId = lastCommonRead,
1071-
parentMessage = parent
1075+
parentMessage = parent,
1076+
isClassified = isClassified
10721077
)
10731078
}
10741079

app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoUiState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ data class ConversationInfoUiState(
4141
val showImportantConversation: Boolean = false,
4242
val sensitiveConversation: Boolean = false,
4343
val showSensitiveConversation: Boolean = false,
44+
val isClassified: Boolean = false,
4445

4546
val lobbyEnabled: Boolean = false,
4647
val showWebinarSettings: Boolean = false,

app/src/main/java/com/nextcloud/talk/conversationinfo/ui/ConversationInfoScreen.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -391,12 +391,13 @@ private fun SettingsRow(
391391
subtitle: String? = null,
392392
@DrawableRes iconRes: Int? = null,
393393
checked: Boolean? = null,
394+
enabled: Boolean = true,
394395
onClick: (() -> Unit)? = null
395396
) {
396397
Row(
397398
modifier = Modifier
398399
.fillMaxWidth()
399-
.then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier)
400+
.then(if (onClick != null && enabled) Modifier.clickable(onClick = onClick) else Modifier)
400401
.padding(horizontal = 16.dp, vertical = 12.dp),
401402
verticalAlignment = Alignment.CenterVertically
402403
) {
@@ -420,7 +421,7 @@ private fun SettingsRow(
420421
}
421422
}
422423
if (checked != null) {
423-
Switch(checked = checked, onCheckedChange = null)
424+
Switch(checked = checked, onCheckedChange = null, enabled = enabled)
424425
}
425426
}
426427
}
@@ -482,7 +483,8 @@ private fun NotificationSettingsSection(state: ConversationInfoUiState, callback
482483
SettingsRow(
483484
title = stringResource(R.string.nc_sensitive_conversation),
484485
subtitle = stringResource(R.string.nc_sensitive_conversation_hint),
485-
checked = state.sensitiveConversation,
486+
checked = state.isClassified || state.sensitiveConversation,
487+
enabled = !state.isClassified,
486488
onClick = callbacks.onSensitiveConversationClick
487489
)
488490
}

app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*
1+
/*
22
* Nextcloud Talk - Android Client
33
*
44
* SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
@@ -341,17 +341,18 @@ class ConversationInfoViewModel @Inject constructor(
341341
""
342342
}
343343

344+
val isClassified = ConversationUtils.isClassified(conversationModel, spreedCapabilities)
344345
val recordingConsentType = CapabilitiesUtil.getRecordingConsentType(spreedCapabilities)
345346
val showRecordingConsent = isModerator &&
346347
!isNoteToSelf &&
347-
recordingConsentType != CapabilitiesUtil.RECORDING_CONSENT_NOT_REQUIRED
348+
recordingConsentType != CapabilitiesUtil.RECORDING_CONSENT_NOT_REQUIRED && !isClassified
348349
val showRecordingConsentSwitch =
349350
showRecordingConsent && recordingConsentType == CapabilitiesUtil.RECORDING_CONSENT_DEPEND_ON_CONVERSATION
350351
val showRecordingConsentAll =
351352
showRecordingConsent && recordingConsentType == CapabilitiesUtil.RECORDING_CONSENT_REQUIRED
352353
val recordingConsentForConversation =
353354
conversationModel.recordingConsentRequired == RECORDING_CONSENT_REQUIRED_FOR_CONVERSATION
354-
val recordingConsentEnabled = !conversationModel.hasCall
355+
val recordingConsentEnabled = !conversationModel.hasCall && !isClassified
355356

356357
val showCallNotifications = !isSystem &&
357358
conversationModel.notificationCalls != null &&
@@ -367,12 +368,12 @@ class ConversationInfoViewModel @Inject constructor(
367368
val canLeave = conversationModel.canLeaveConversation
368369
val canDelete = conversationModel.canDeleteConversation
369370

370-
val showGuestAccess = canModerate
371-
val guestsAllowed = isPublic
371+
val showGuestAccess = canModerate && !isClassified
372+
val guestsAllowed = isPublic && !isClassified
372373
val hasPassword = guestsAllowed && conversationModel.hasPassword
373374
val showPasswordProtection = guestsAllowed
374375
val showResendInvitations = guestsAllowed &&
375-
user.capabilities?.spreedCapability?.features?.contains("sip-support") == true
376+
user.capabilities?.spreedCapability?.features?.contains("sip-support") == true && !isClassified
376377

377378
val showSharedItems = hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.RICH_OBJECT_LIST_MEDIA) &&
378379
conversationModel.remoteServer.isNullOrEmpty()
@@ -381,7 +382,7 @@ class ConversationInfoViewModel @Inject constructor(
381382
val isWebinarRoom = isGroup || isPublic
382383
val showWebinarSettings = hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.WEBINARY_LOBBY) &&
383384
isWebinarRoom &&
384-
canModerate
385+
canModerate && !isClassified
385386

386387
val showImportantConversation =
387388
hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.IMPORTANT_CONVERSATIONS)
@@ -438,6 +439,7 @@ class ConversationInfoViewModel @Inject constructor(
438439
showImportantConversation = showImportantConversation,
439440
sensitiveConversation = sensitiveConversation,
440441
showSensitiveConversation = showSensitiveConversation,
442+
isClassified = isClassified,
441443
lobbyEnabled = isLobbyEnabled,
442444
showWebinarSettings = showWebinarSettings,
443445
lobbyTimerLabel = lobbyTimerLabel,

app/src/main/java/com/nextcloud/talk/models/json/conversations/ConversationEnums.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ class ConversationEnums {
4747
EVENT,
4848
PHONE_TEMPORARY,
4949
PHONE_PERSIST,
50-
INSTANT_MEETING
50+
INSTANT_MEETING,
51+
CLASSIFIED
5152
}
5253

5354
enum class Preset {

0 commit comments

Comments
 (0)