diff --git a/app/src/main/kotlin/com/wire/android/mapper/MessageMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/MessageMapper.kt index 8bed0aea1f..49008e225f 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/MessageMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/MessageMapper.kt @@ -69,6 +69,7 @@ class MessageMapper @Inject constructor( is Message.System -> { when (val content = message.content) { is MessageContent.MemberChange -> content.members + is MessageContent.LegalHold.ForMembers -> content.members else -> listOf() } } diff --git a/app/src/main/kotlin/com/wire/android/mapper/SystemMessageContentMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/SystemMessageContentMapper.kt index 1ce361e681..3e06819edf 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/SystemMessageContentMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/SystemMessageContentMapper.kt @@ -71,6 +71,7 @@ class SystemMessageContentMapper @Inject constructor( is MessageContent.FederationStopped -> mapFederationMessage(content) is MessageContent.ConversationProtocolChanged -> mapConversationProtocolChanged(content) is MessageContent.ConversationStartedUnverifiedWarning -> mapConversationCreatedUnverifiedWarning() + is MessageContent.LegalHold -> mapLegalHoldMessage(content, message.senderUserId, members) } private fun mapConversationCreated(senderUserId: UserId, date: String, userList: List): UIMessageContent.SystemMessage { @@ -276,6 +277,38 @@ class SystemMessageContentMapper @Inject constructor( else -> UIText.StringResource(messageResourceProvider.memberNameDeleted) } + private fun mapLegalHoldMessage( + content: MessageContent.LegalHold, + senderUserId: UserId, + userList: List + ): UIMessageContent.SystemMessage { + + fun handleLegalHoldForMembers( + members: List, + self: () -> UIMessageContent.SystemMessage.LegalHold, + others: (List) -> UIMessageContent.SystemMessage.LegalHold + ): UIMessageContent.SystemMessage.LegalHold = + if (members.size == 1 && senderUserId == members.first()) self() + else others(members.map { mapMemberName(user = userList.findUser(userId = it), type = SelfNameType.ResourceLowercase) }) + + return when (content) { + MessageContent.LegalHold.ForConversation.Disabled -> UIMessageContent.SystemMessage.LegalHold.Disabled.Conversation + MessageContent.LegalHold.ForConversation.Enabled -> UIMessageContent.SystemMessage.LegalHold.Enabled.Conversation + + is MessageContent.LegalHold.ForMembers.Disabled -> handleLegalHoldForMembers( + members = content.members, + self = { UIMessageContent.SystemMessage.LegalHold.Disabled.Self }, + others = { UIMessageContent.SystemMessage.LegalHold.Disabled.Others(it) } + ) + + is MessageContent.LegalHold.ForMembers.Enabled -> handleLegalHoldForMembers( + members = content.members, + self = { UIMessageContent.SystemMessage.LegalHold.Enabled.Self }, + others = { UIMessageContent.SystemMessage.LegalHold.Enabled.Others(it) } + ) + } + } + enum class SelfNameType { ResourceLowercase, ResourceTitleCase, NameOrDeleted } diff --git a/app/src/main/kotlin/com/wire/android/ui/common/LegalHoldIndicator.kt b/app/src/main/kotlin/com/wire/android/ui/common/LegalHoldIndicator.kt index 899f250d54..1569e62b9a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/LegalHoldIndicator.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/LegalHoldIndicator.kt @@ -20,41 +20,30 @@ package com.wire.android.ui.common -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp +import androidx.compose.ui.res.painterResource +import com.wire.android.R +import com.wire.android.ui.theme.WireTheme +import com.wire.android.ui.theme.wireColorScheme +import com.wire.android.util.ui.PreviewMultipleThemes @Composable fun LegalHoldIndicator(modifier: Modifier = Modifier) { - Box( + Icon( + painter = painterResource(id = R.drawable.ic_legal_hold), + contentDescription = null, + tint = MaterialTheme.wireColorScheme.error, modifier = modifier, - contentAlignment = Alignment.Center - ) { - Box( - modifier = Modifier - .size(12.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.errorContainer) - ) - Box( - modifier = Modifier - .size(6.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.error) - ) - } + ) } -@Preview +@PreviewMultipleThemes @Composable fun PreviewLegalHoldIndicator() { - LegalHoldIndicator() + WireTheme { + LegalHoldIndicator() + } } diff --git a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModel.kt index c55fa1d118..419518bde1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModel.kt @@ -34,7 +34,7 @@ import com.wire.kalium.logic.data.call.Call import com.wire.kalium.logic.data.user.LegalHoldStatus import com.wire.kalium.logic.data.sync.SyncState import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldRequestUseCaseResult +import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldRequestUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -82,7 +82,7 @@ class CommonTopAppBarViewModel @Inject constructor( observeLegalHoldRequest() // TODO combine with legal hold status .map { legalHoldRequestResult -> when (legalHoldRequestResult) { - is ObserveLegalHoldRequestUseCaseResult.ObserveLegalHoldRequestAvailable -> LegalHoldStatus.PENDING + is ObserveLegalHoldRequestUseCase.Result.LegalHoldRequestAvailable -> LegalHoldStatus.PENDING else -> LegalHoldStatus.DISABLED } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/SystemMessageItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/SystemMessageItem.kt index 037f501893..3f4bcfe5c7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/SystemMessageItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/SystemMessageItem.kt @@ -67,6 +67,7 @@ import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.spacers.VerticalSpace import com.wire.android.ui.home.conversations.mock.mockMessageWithKnock +import com.wire.android.ui.home.conversations.mock.mockUsersUITexts import com.wire.android.ui.home.conversations.model.MessageFlowStatus import com.wire.android.ui.home.conversations.model.UIMessage import com.wire.android.ui.home.conversations.model.UIMessageContent.SystemMessage @@ -76,7 +77,8 @@ import com.wire.android.ui.theme.wireTypography import com.wire.android.util.CustomTabsHelper import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.android.util.ui.UIText -import com.wire.android.util.ui.annotatedText +import com.wire.android.util.ui.markdownBold +import com.wire.android.util.ui.markdownText import com.wire.android.util.ui.toUIText import kotlin.math.roundToInt @@ -84,6 +86,7 @@ import kotlin.math.roundToInt @Composable fun SystemMessageItem( message: UIMessage.System, + initiallyExpanded: Boolean = false, onFailedMessageRetryClicked: (String) -> Unit = {}, onFailedMessageCancelClicked: (String) -> Unit = {}, onSelfDeletingMessageRead: (UIMessage) -> Unit = {} @@ -149,25 +152,50 @@ fun SystemMessageItem( .alignBy { centerOfFirstLine.roundToInt() } ) { val context = LocalContext.current - var expanded: Boolean by remember { mutableStateOf(false) } - Text( + var expanded: Boolean by remember { mutableStateOf(initiallyExpanded) } + val annotatedString = message.messageContent.annotatedString( + res = context.resources, + expanded = expanded, + normalStyle = MaterialTheme.wireTypography.body01, + boldStyle = MaterialTheme.wireTypography.body02, + normalColor = MaterialTheme.wireColorScheme.secondaryText, + boldColor = MaterialTheme.wireColorScheme.onBackground, + errorColor = MaterialTheme.wireColorScheme.error, + isErrorString = message.addingFailed, + ) + val learnMoreAnnotatedString = message.messageContent.learnMoreResId?.let { + val learnMoreLink = stringResource(id = message.messageContent.learnMoreResId) + val learnMoreText = stringResource(id = R.string.label_learn_more) + buildAnnotatedString { + append(learnMoreText) + addStyle( + style = SpanStyle( + color = MaterialTheme.colorScheme.primary, + textDecoration = TextDecoration.Underline + ), + start = 0, + end = learnMoreText.length + ) + addStringAnnotation(tag = TAG_LEARN_MORE, annotation = learnMoreLink, start = 0, end = learnMoreText.length) + } + } + val fullAnnotatedString = + if (learnMoreAnnotatedString != null) annotatedString + AnnotatedString(" ") + learnMoreAnnotatedString + else annotatedString + + ClickableText( modifier = Modifier.defaultMinSize(minHeight = dimensions().spacing20x), - style = MaterialTheme.wireTypography.body01, - lineHeight = MaterialTheme.wireTypography.body02.lineHeight, - text = message.messageContent.annotatedString( - res = context.resources, - expanded = expanded, - normalStyle = MaterialTheme.wireTypography.body01, - boldStyle = MaterialTheme.wireTypography.body02, - normalColor = MaterialTheme.wireColorScheme.secondaryText, - boldColor = MaterialTheme.wireColorScheme.onBackground, - errorColor = MaterialTheme.wireColorScheme.error, - isErrorString = message.addingFailed, - ), + text = fullAnnotatedString, + onClick = { offset -> + fullAnnotatedString.getStringAnnotations(TAG_LEARN_MORE, offset, offset,) + .firstOrNull()?.let { result -> CustomTabsHelper.launchUrl(context, result.item) } + }, + style = MaterialTheme.wireTypography.body02, onTextLayout = { centerOfFirstLine = if (it.lineCount == 0) 0f else ((it.getLineTop(0) + it.getLineBottom(0)) / 2) } ) + if ((message.addingFailed && expanded) || message.singleUserAddFailed) { OfflineBackendsLearnMoreLink() } @@ -190,31 +218,6 @@ fun SystemMessageItem( onCancelClick = remember { { onFailedMessageCancelClicked(message.header.messageId) } } ) } - if (message.messageContent.learnMoreResId != null) { - val learnMoreLink = stringResource(id = message.messageContent.learnMoreResId) - val learnMoreText = stringResource(id = R.string.label_learn_more) - val annotatedString = buildAnnotatedString { - append(learnMoreText) - addStyle( - style = SpanStyle( - color = MaterialTheme.colorScheme.primary, - textDecoration = TextDecoration.Underline - ), - start = 0, - end = learnMoreText.length - ) - } - ClickableText( - text = annotatedString, - onClick = { - CustomTabsHelper.launchUrl( - context, - learnMoreLink - ) - }, - style = MaterialTheme.wireTypography.body01, - ) - } } } if (message.messageContent is SystemMessage.ConversationMessageCreated) { @@ -244,7 +247,9 @@ private fun getColorFilter(message: SystemMessage): ColorFilter? { is SystemMessage.ConversationDegraded -> null is SystemMessage.ConversationVerified -> null is SystemMessage.Knock -> ColorFilter.tint(colorsScheme().primary) + is SystemMessage.LegalHold, is SystemMessage.MemberFailedToAdd -> ColorFilter.tint(colorsScheme().error) + is SystemMessage.MemberAdded, is SystemMessage.MemberJoined, is SystemMessage.MemberLeft, @@ -398,6 +403,24 @@ fun PreviewSystemMessageFailedToAddMultiple() { } } +@PreviewMultipleThemes +@Composable +fun PreviewSystemMessageFailedToAddMultipleExpanded() { + WireTheme { + SystemMessageItem( + message = mockMessageWithKnock.copy( + messageContent = SystemMessage.MemberFailedToAdd( + listOf( + UIText.DynamicString("Barbara Cotolina"), + UIText.DynamicString("Albert Lewis") + ) + ) + ), + initiallyExpanded = true, + ) + } +} + @PreviewMultipleThemes @Composable fun PreviewSystemMessageFederationMemberRemoved() { @@ -468,6 +491,54 @@ fun PreviewSystemMessageFederationStoppedSelf() { } } +@PreviewMultipleThemes +@Composable +fun PreviewSystemMessageLegalHoldEnabledSelf() { + WireTheme { + SystemMessageItem(message = mockMessageWithKnock.copy(messageContent = SystemMessage.LegalHold.Enabled.Self)) + } +} + +@PreviewMultipleThemes +@Composable +fun PreviewSystemMessageLegalHoldDisabledSelf() { + WireTheme { + SystemMessageItem(message = mockMessageWithKnock.copy(messageContent = SystemMessage.LegalHold.Disabled.Self)) + } +} + +@PreviewMultipleThemes +@Composable +fun PreviewSystemMessageLegalHoldEnabledOthers() { + WireTheme { + SystemMessageItem(message = mockMessageWithKnock.copy(messageContent = SystemMessage.LegalHold.Enabled.Others(mockUsersUITexts))) + } +} + +@PreviewMultipleThemes +@Composable +fun PreviewSystemMessageLegalHoldDisabledOthers() { + WireTheme { + SystemMessageItem(message = mockMessageWithKnock.copy(messageContent = SystemMessage.LegalHold.Disabled.Others(mockUsersUITexts))) + } +} + +@PreviewMultipleThemes +@Composable +fun PreviewSystemMessageLegalHoldDisabledConversation() { + WireTheme { + SystemMessageItem(message = mockMessageWithKnock.copy(messageContent = SystemMessage.LegalHold.Disabled.Conversation)) + } +} + +@PreviewMultipleThemes +@Composable +fun PreviewSystemMessageLegalHoldEnabledConversation() { + WireTheme { + SystemMessageItem(message = mockMessageWithKnock.copy(messageContent = SystemMessage.LegalHold.Enabled.Conversation)) + } +} + private val SystemMessage.expandable get() = when (this) { is SystemMessage.MemberAdded -> this.memberNames.size > EXPANDABLE_THRESHOLD @@ -495,24 +566,30 @@ private val SystemMessage.expandable is SystemMessage.ConversationVerified -> false is SystemMessage.FederationStopped -> false is SystemMessage.ConversationMessageCreatedUnverifiedWarning -> false + is SystemMessage.LegalHold -> false } -private fun List.toUserNamesListString(res: Resources): String = when { +private fun List.toUserNamesListMarkdownString(res: Resources): String = when { this.isEmpty() -> "" - this.size == 1 -> this[0] - else -> res.getString(R.string.label_system_message_and, this.dropLast(1).joinToString(", "), this.last()) + this.size == 1 -> this[0].markdownBold() + else -> res.getString( + R.string.label_system_message_and, + this.dropLast(1).joinToString(", ") { it.markdownBold() }, + this.last().markdownBold() + ) } private fun List.limitUserNamesList( res: Resources, - threshold: Int, + expanded: Boolean, + collapsedSize: Int = EXPANDABLE_THRESHOLD, @PluralsRes quantityString: Int = R.plurals.label_system_message_x_more ): List = - if (this.size <= threshold) { + if (expanded || this.size <= collapsedSize) { this.map { it.asString(res) } } else { - val moreCount = this.size - (threshold - 1) // the last visible place is taken by "and X more" - this.take(threshold - 1) + val moreCount = this.size - (collapsedSize - 1) // the last visible place is taken by "and X more" + this.take(collapsedSize - 1) .map { it.asString(res) } .plus(res.getQuantityString(quantityString, moreCount, moreCount)) } @@ -528,33 +605,37 @@ fun SystemMessage.annotatedString( errorColor: Color, isErrorString: Boolean = false ): AnnotatedString { - val args = when (this) { + val markdownArgs = when (this) { is SystemMessage.MemberAdded -> arrayOf( - author.asString(res), - memberNames.limitUserNamesList(res, if (expanded) memberNames.size else EXPANDABLE_THRESHOLD).toUserNamesListString(res) + author.asString(res).markdownBold(), + memberNames.limitUserNamesList(res, expanded).toUserNamesListMarkdownString(res) ) is SystemMessage.MemberRemoved -> arrayOf( - author.asString(res), - memberNames.limitUserNamesList(res, if (expanded) memberNames.size else EXPANDABLE_THRESHOLD).toUserNamesListString(res) + author.asString(res).markdownBold(), + memberNames.limitUserNamesList(res, expanded).toUserNamesListMarkdownString(res) ) is SystemMessage.FederationMemberRemoved -> arrayOf( - memberNames.limitUserNamesList(res, if (expanded) memberNames.size else EXPANDABLE_THRESHOLD).toUserNamesListString(res) + memberNames.limitUserNamesList(res, expanded).toUserNamesListMarkdownString(res) ) - is SystemMessage.MemberJoined -> arrayOf(author.asString(res)) - is SystemMessage.MemberLeft -> arrayOf(author.asString(res)) - is SystemMessage.MissedCall -> arrayOf(author.asString(res)) - is SystemMessage.RenamedConversation -> arrayOf(author.asString(res), additionalContent) - is SystemMessage.TeamMemberRemoved -> arrayOf(content.userName) - is SystemMessage.CryptoSessionReset -> arrayOf(author.asString(res)) - is SystemMessage.NewConversationReceiptMode -> arrayOf(receiptMode.asString(res)) - is SystemMessage.ConversationReceiptModeChanged -> arrayOf(author.asString(res), receiptMode.asString(res)) - is SystemMessage.Knock -> arrayOf(author.asString(res)) + is SystemMessage.MemberJoined -> arrayOf(author.asString(res).markdownBold()) + is SystemMessage.MemberLeft -> arrayOf(author.asString(res).markdownBold()) + is SystemMessage.MissedCall -> arrayOf(author.asString(res).markdownBold()) + is SystemMessage.RenamedConversation -> arrayOf(author.asString(res).markdownBold(), content.conversationName.markdownBold()) + is SystemMessage.TeamMemberRemoved -> arrayOf(content.userName.markdownBold()) + is SystemMessage.CryptoSessionReset -> arrayOf(author.asString(res).markdownBold()) + is SystemMessage.NewConversationReceiptMode -> arrayOf(receiptMode.asString(res).markdownBold()) + is SystemMessage.ConversationReceiptModeChanged -> arrayOf( + author.asString(res).markdownBold(), + receiptMode.asString(res).markdownBold() + ) + + is SystemMessage.Knock -> arrayOf(author.asString(res).markdownBold()) is SystemMessage.HistoryLost -> arrayOf() is SystemMessage.MLSWrongEpochWarning -> arrayOf() is SystemMessage.ConversationDegraded -> arrayOf() @@ -562,33 +643,33 @@ fun SystemMessage.annotatedString( is SystemMessage.HistoryLostProtocolChanged -> arrayOf() is SystemMessage.ConversationProtocolChanged -> arrayOf() is SystemMessage.ConversationMessageTimerActivated -> arrayOf( - author.asString(res), - selfDeletionDuration.longLabel.asString(res) + author.asString(res).markdownBold(), + selfDeletionDuration.longLabel.asString(res).markdownBold() ) - is SystemMessage.ConversationMessageTimerDeactivated -> arrayOf(author.asString(res)) - is SystemMessage.ConversationMessageCreated -> arrayOf(author.asString(res)) + is SystemMessage.ConversationMessageTimerDeactivated -> arrayOf(author.asString(res).markdownBold()) + is SystemMessage.ConversationMessageCreated -> arrayOf(author.asString(res).markdownBold()) is SystemMessage.ConversationStartedWithMembers -> - arrayOf( - memberNames.limitUserNamesList(res, if (expanded) memberNames.size else EXPANDABLE_THRESHOLD) - .toUserNamesListString(res) - ) + arrayOf(memberNames.limitUserNamesList(res, expanded).toUserNamesListMarkdownString(res)) is SystemMessage.MemberFailedToAdd -> - return this.toFailedToAddAnnotatedText( + return this.toFailedToAddMarkdownText( res, normalStyle, boldStyle, normalColor, boldColor, errorColor, isErrorString, if (usersCount > SINGLE_EXPANDABLE_THRESHOLD) expanded else true ) is SystemMessage.FederationStopped -> domainList.toTypedArray() is SystemMessage.ConversationMessageCreatedUnverifiedWarning -> arrayOf() + is SystemMessage.LegalHold -> memberNames?.let { memberNames -> + arrayOf(memberNames.limitUserNamesList(res, true).toUserNamesListMarkdownString(res)) + } ?: arrayOf() } - - return res.annotatedText(stringResId, normalStyle, boldStyle, normalColor, boldColor, errorColor, isErrorString, *args) + val markdownString = res.getString(stringResId, *markdownArgs) + return markdownText(markdownString, normalStyle, boldStyle, normalColor, boldColor, errorColor, isErrorString) } @Suppress("LongParameterList", "SpreadOperator", "ComplexMethod") -private fun SystemMessage.MemberFailedToAdd.toFailedToAddAnnotatedText( +private fun SystemMessage.MemberFailedToAdd.toFailedToAddMarkdownText( res: Resources, normalStyle: TextStyle, boldStyle: TextStyle, @@ -602,31 +683,29 @@ private fun SystemMessage.MemberFailedToAdd.toFailedToAddAnnotatedText( val isMultipleUsersFailure = usersCount > SINGLE_EXPANDABLE_THRESHOLD if (isMultipleUsersFailure) { failedToAddAnnotatedText.append( - res.annotatedText( - R.string.label_system_message_conversation_failed_add_members_summary, + markdownText( + res.getString(R.string.label_system_message_conversation_failed_add_members_summary, usersCount.toString().markdownBold()), normalStyle, boldStyle, normalColor, boldColor, errorColor, isErrorString, - this.usersCount.toString() ) ) } if (expanded) { - if (isMultipleUsersFailure) failedToAddAnnotatedText.append("\n") + if (isMultipleUsersFailure) failedToAddAnnotatedText.append("\n\n") failedToAddAnnotatedText.append( - res.annotatedText( - stringResId, + markdownText( + res.getString(stringResId, memberNames.limitUserNamesList(res, true).toUserNamesListMarkdownString(res)), normalStyle, boldStyle, normalColor, boldColor, errorColor, isErrorString, - memberNames.limitUserNamesList(res, EXPANDABLE_THRESHOLD).toUserNamesListString(res) ) ) } @@ -635,3 +714,4 @@ private fun SystemMessage.MemberFailedToAdd.toFailedToAddAnnotatedText( private const val EXPANDABLE_THRESHOLD = 4 private const val SINGLE_EXPANDABLE_THRESHOLD = 1 +private const val TAG_LEARN_MORE = "tag_learn_more" diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/mock/Mock.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/mock/Mock.kt index d511094021..cfa866999a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/mock/Mock.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/mock/Mock.kt @@ -45,6 +45,7 @@ import com.wire.android.ui.home.conversations.model.UIMessageContent import com.wire.android.ui.home.conversationslist.model.Membership import com.wire.android.util.ui.UIText import com.wire.android.util.ui.WireSessionImageLoader +import com.wire.android.util.ui.toUIText import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.message.Message import com.wire.kalium.logic.data.user.ConnectionState @@ -242,6 +243,16 @@ val mockMessageWithKnock = UIMessage.System( source = MessageSource.Self, ) +val mockUsersUITexts = listOf( + "Albert Lewis".toUIText(), + "Bert Strunk".toUIText(), + "Claudia Schiffer".toUIText(), + "Dorothee Friedrich".toUIText(), + "Erich Weinert".toUIText(), + "Frieda Kahlo".toUIText(), + "Gudrun Gut".toUIText() +) + val mockImageLoader = WireSessionImageLoader(object : ImageLoader { override val components: ComponentRegistry get() = TODO("Not yet implemented") override val defaults: DefaultRequestOptions get() = TODO("Not yet implemented") @@ -255,7 +266,7 @@ val mockImageLoader = WireSessionImageLoader(object : ImageLoader { object : NetworkStateObserver { override fun observeNetworkState(): StateFlow = MutableStateFlow(NetworkState.ConnectedWithInternet) } - ) +) fun mockAssetMessage(uploadStatus: Message.UploadStatus = Message.UploadStatus.UPLOADED) = UIMessage.Regular( userAvatarData = UserAvatarData( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt index 8d0d6a94af..38f5ef8fdc 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt @@ -280,9 +280,8 @@ sealed class UIMessageContent { sealed class SystemMessage( @DrawableRes val iconResId: Int?, @StringRes open val stringResId: Int, + @StringRes val learnMoreResId: Int? = null, val isSmallIcon: Boolean = true, - val additionalContent: String = "", - @StringRes val learnMoreResId: Int? = null ) : UIMessageContent() { data class Knock(val author: UIText, val isSelfTriggered: Boolean) : SystemMessage( @@ -358,21 +357,17 @@ sealed class UIMessageContent { sealed class MissedCall( open val author: UIText, @StringRes override val stringResId: Int - ) : SystemMessage(R.drawable.ic_call_end, stringResId, false) { + ) : SystemMessage(R.drawable.ic_call_end, stringResId) { data class YouCalled(override val author: UIText) : MissedCall(author, R.string.label_system_message_you_called) data class OtherCalled(override val author: UIText) : MissedCall(author, R.string.label_system_message_other_called) } data class RenamedConversation(val author: UIText, val content: MessageContent.ConversationRenamed) : - SystemMessage( - R.drawable.ic_edit, R.string.label_system_message_renamed_the_conversation, - false, - content.conversationName - ) + SystemMessage(R.drawable.ic_edit, R.string.label_system_message_renamed_the_conversation) data class TeamMemberRemoved(val content: MessageContent.TeamMemberRemoved) : - SystemMessage(R.drawable.ic_minus, R.string.label_system_message_team_member_left, true, content.userName) + SystemMessage(R.drawable.ic_minus, R.string.label_system_message_team_member_left) data class CryptoSessionReset(val author: UIText) : SystemMessage(R.drawable.ic_info, R.string.label_system_message_session_reset) @@ -496,6 +491,25 @@ sealed class UIMessageContent { R.drawable.ic_info, R.string.label_system_message_conversation_started_sensitive_information ) + + sealed class LegalHold( + @StringRes stringResId: Int, + @StringRes learnMoreResId: Int? = null, + open val memberNames: List? = null, + ) : SystemMessage(R.drawable.ic_legal_hold, stringResId, learnMoreResId) { + + sealed class Enabled(@StringRes override val stringResId: Int) : LegalHold(stringResId, R.string.url_legal_hold_learn_more) { + data object Self : Enabled(R.string.legal_hold_system_message_enabled_self) + data class Others(override val memberNames: List) : Enabled(R.string.legal_hold_system_message_enabled_others) + data object Conversation : Enabled(R.string.legal_hold_system_message_enabled_conversation) + } + + sealed class Disabled(@StringRes override val stringResId: Int) : LegalHold(stringResId, null) { + data object Self : Disabled(R.string.legal_hold_system_message_disabled_self) + data class Others(override val memberNames: List) : Disabled(R.string.legal_hold_system_message_disabled_others) + data object Conversation : Disabled(R.string.legal_hold_system_message_disabled_conversation) + } + } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationMessagesFromSearchUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationMessagesFromSearchUseCase.kt index 3dd771258c..c54ec06fae 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationMessagesFromSearchUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationMessagesFromSearchUseCase.kt @@ -64,7 +64,7 @@ class GetConversationMessagesFromSearchUseCase @Inject constructor( searchQuery = searchTerm, conversationId = conversationId, pagingConfig = pagingConfig, - startingOffset = max(0, lastReadIndex - PREFETCH_DISTANCE) + startingOffset = max(0, lastReadIndex - PREFETCH_DISTANCE).toLong() ).map { pagingData -> pagingData.flatMap { messageItem -> observeMemberDetailsByIds(messageMapper.memberIdList(listOf(messageItem))) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetMessagesForConversationUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetMessagesForConversationUseCase.kt index 9d76db008d..0061493000 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetMessagesForConversationUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetMessagesForConversationUseCase.kt @@ -55,7 +55,7 @@ class GetMessagesForConversationUseCase @Inject constructor( return getMessages( conversationId, pagingConfig = pagingConfig, - startingOffset = max(0, lastReadIndex - PREFETCH_DISTANCE) + startingOffset = max(0, lastReadIndex - PREFETCH_DISTANCE).toLong() ).map { pagingData -> pagingData.flatMap { messageItem -> observeMemberDetailsByIds(messageMapper.memberIdList(listOf(messageItem))) diff --git a/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModel.kt index d396b6350f..7a5ae1f84d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModel.kt @@ -30,7 +30,7 @@ import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.UserSessionScope import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase import com.wire.kalium.logic.feature.legalhold.ApproveLegalHoldRequestUseCase -import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldRequestUseCaseResult +import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldRequestUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.user.IsPasswordRequiredUseCase import dagger.hilt.android.lifecycle.HiltViewModel @@ -57,13 +57,13 @@ class LegalHoldRequestedViewModel @Inject constructor( observeLegalHoldRequest() .mapLatest { legalHoldRequestResult -> when (legalHoldRequestResult) { - is ObserveLegalHoldRequestUseCaseResult.Failure -> { + is ObserveLegalHoldRequestUseCase.Result.Failure -> { appLogger.e("$TAG: Failed to get legal hold request data: ${legalHoldRequestResult.failure}") LegalHoldRequestData.None } - ObserveLegalHoldRequestUseCaseResult.NoObserveLegalHoldRequest -> LegalHoldRequestData.None - is ObserveLegalHoldRequestUseCaseResult.ObserveLegalHoldRequestAvailable -> + ObserveLegalHoldRequestUseCase.Result.NoLegalHoldRequest -> LegalHoldRequestData.None + is ObserveLegalHoldRequestUseCase.Result.LegalHoldRequestAvailable -> users.isPasswordRequired() .let { LegalHoldRequestData.Pending( diff --git a/app/src/main/kotlin/com/wire/android/util/ui/StyledStringUtil.kt b/app/src/main/kotlin/com/wire/android/util/ui/StyledStringUtil.kt index 97fd53f706..98d69a84f7 100644 --- a/app/src/main/kotlin/com/wire/android/util/ui/StyledStringUtil.kt +++ b/app/src/main/kotlin/com/wire/android/util/ui/StyledStringUtil.kt @@ -51,36 +51,32 @@ fun Resources.stringWithStyledArgs( ): AnnotatedString { val normalSpanStyle = toSpanStyle(normalStyle, normalColor) val boldSpanStyle = toSpanStyle(argsStyle, argsColor) - val string = this.getString(stringResId, *formatArgs.map { it.bold() }.toTypedArray()) + val string = this.getString(stringResId, *formatArgs.map { it.markdownBold() }.toTypedArray()) return buildAnnotatedString { - string.split(STYLE_SEPARATOR).forEachIndexed { index, text -> + string.split(BOLD_SEPARATOR).forEachIndexed { index, text -> withStyle(if (index % 2 == 0) normalSpanStyle else boldSpanStyle) { append(text) } } } } @Suppress("LongParameterList", "SpreadOperator") -fun Resources.annotatedText( - @StringRes stringResId: Int, +fun markdownText( + markdownInput: String, normalStyle: TextStyle, boldStyle: TextStyle, normalColor: Color, boldColor: Color, errorColor: Color, isErrorString: Boolean, - vararg formatArgs: String ): AnnotatedString { - - // Mark all arguments as bold, by adding ** - val input = this.getString(stringResId, *formatArgs.map { it.markdownBold() }.toTypedArray()) // The text gets split into pieces based on ** - val splitText = input.split(BOLD_SEPARATOR).filter { it.isNotEmpty() } + val splitText = markdownInput.split(BOLD_SEPARATOR).filter { it.isNotEmpty() } // Prepare the annotated string return buildAnnotatedString { splitText.forEach { piece -> when { - input.contains(BOLD_SEPARATOR + piece.trim() + BOLD_SEPARATOR) -> { // If the piece was between ** characters + markdownInput.contains(BOLD_SEPARATOR + piece.trim() + BOLD_SEPARATOR) -> { // If the piece was between ** characters pushStyle(style = toSpanStyle(boldStyle, useErrorColorIfApplies(isErrorString, errorColor, boldColor))) append(piece) pop() @@ -105,9 +101,9 @@ fun Resources.stringWithBoldArgs( @StringRes stringResId: Int, vararg formatArgs: String ): SpannedString { - val string = this.getString(stringResId, *formatArgs.map { it.bold() }.toTypedArray()) + val string = this.getString(stringResId, *formatArgs.map { it.markdownBold() }.toTypedArray()) return buildSpannedString { - string.split(STYLE_SEPARATOR).forEachIndexed { index, text -> + string.split(BOLD_SEPARATOR).forEachIndexed { index, text -> if (index % 2 == 0) append(text) else bold { append(text) } } @@ -123,10 +119,8 @@ private fun toSpanStyle(textStyle: TextStyle, color: Color) = SpanStyle( textDecoration = textStyle.textDecoration, ) -private fun String.bold() = STYLE_SEPARATOR + this + STYLE_SEPARATOR -private fun String.markdownBold() = BOLD_SEPARATOR + this + BOLD_SEPARATOR +fun String.markdownBold() = BOLD_SEPARATOR + this + BOLD_SEPARATOR -private const val STYLE_SEPARATOR: String = "\u0000" private const val BOLD_SEPARATOR: String = "**" data class LinkTextData( diff --git a/app/src/main/res/drawable/ic_legal_hold.xml b/app/src/main/res/drawable/ic_legal_hold.xml new file mode 100644 index 0000000000..232dd5eac7 --- /dev/null +++ b/app/src/main/res/drawable/ic_legal_hold.xml @@ -0,0 +1,30 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2926358a57..146ed56685 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1314,6 +1314,12 @@ legal hold Legal hold is active Legal hold is pending + You are now subject to legal hold. + Legal hold activated for %1$s. + You are no longer subject to legal hold. + Legal hold deactivated for %1$s. + Legal hold is no longer active for this conversation. + Legal hold is now active for this conversation. Conversation no longer verified diff --git a/app/src/test/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModelTest.kt index 43b87c8c61..27296ab163 100644 --- a/app/src/test/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModelTest.kt @@ -32,7 +32,7 @@ import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.sync.SyncState import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase -import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldRequestUseCaseResult +import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldRequestUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.sync.ObserveSyncStateUseCase import io.mockk.MockKAnnotations @@ -192,7 +192,7 @@ class CommonTopAppBarViewModelTest { private fun testLegalHoldRequestInfo( currentScreen: CurrentScreen, - result: ObserveLegalHoldRequestUseCaseResult, + result: ObserveLegalHoldRequestUseCase.Result, expectedState: LegalHoldUIState, ) = runTest { val (_, commonTopAppBarViewModel) = Arrangement() @@ -210,28 +210,28 @@ class CommonTopAppBarViewModelTest { @Test fun givenNoLegalHoldRequest_whenGettingState_thenShouldNotHaveLegalHoldRequestInfo() = testLegalHoldRequestInfo( currentScreen = CurrentScreen.Home, - result = ObserveLegalHoldRequestUseCaseResult.NoObserveLegalHoldRequest, + result = ObserveLegalHoldRequestUseCase.Result.NoLegalHoldRequest, expectedState = LegalHoldUIState.None ) @Test fun givenLegalHoldRequestAndHomeScreen_whenGettingState_thenShouldHaveLegalHoldRequestInfo() = testLegalHoldRequestInfo( currentScreen = CurrentScreen.Home, - result = ObserveLegalHoldRequestUseCaseResult.ObserveLegalHoldRequestAvailable(byteArrayOf()), + result = ObserveLegalHoldRequestUseCase.Result.LegalHoldRequestAvailable(byteArrayOf()), expectedState = LegalHoldUIState.Pending ) @Test fun givenLegalHoldRequestAndCallScreen_whenGettingState_thenShouldNotHaveLegalHoldRequestInfo() = testLegalHoldRequestInfo( currentScreen = CurrentScreen.OngoingCallScreen(mockk()), - result = ObserveLegalHoldRequestUseCaseResult.ObserveLegalHoldRequestAvailable(byteArrayOf()), + result = ObserveLegalHoldRequestUseCase.Result.LegalHoldRequestAvailable(byteArrayOf()), expectedState = LegalHoldUIState.None ) @Test fun givenLegalHoldRequestAndAuthRelatedScreen_whenGettingState_thenShouldNotHaveLegalHoldRequestInfo() = testLegalHoldRequestInfo( currentScreen = CurrentScreen.AuthRelated, - result = ObserveLegalHoldRequestUseCaseResult.ObserveLegalHoldRequestAvailable(byteArrayOf()), + result = ObserveLegalHoldRequestUseCase.Result.LegalHoldRequestAvailable(byteArrayOf()), expectedState = LegalHoldUIState.None ) @@ -282,7 +282,7 @@ class CommonTopAppBarViewModelTest { withSyncState(SyncState.Live) withoutActiveCall() - withLegalHoldRequestResult(ObserveLegalHoldRequestUseCaseResult.NoObserveLegalHoldRequest) + withLegalHoldRequestResult(ObserveLegalHoldRequestUseCase.Result.NoLegalHoldRequest) } private val commonTopAppBarViewModel by lazy { @@ -327,7 +327,7 @@ class CommonTopAppBarViewModelTest { coEvery { currentScreenManager.observeCurrentScreen(any()) } returns MutableStateFlow(currentScreen) } - fun withLegalHoldRequestResult(result: ObserveLegalHoldRequestUseCaseResult) = apply { + fun withLegalHoldRequestResult(result: ObserveLegalHoldRequestUseCase.Result) = apply { every { coreLogic.getSessionScope(any()).observeLegalHoldRequest() } returns flowOf(result) } diff --git a/app/src/test/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModelTest.kt index c0ccb32c89..9d5bb122b4 100644 --- a/app/src/test/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModelTest.kt @@ -27,7 +27,7 @@ import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.auth.ValidatePasswordResult import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase import com.wire.kalium.logic.feature.legalhold.ApproveLegalHoldRequestUseCase -import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldRequestUseCaseResult +import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldRequestUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.user.IsPasswordRequiredUseCase import io.mockk.MockKAnnotations @@ -70,7 +70,7 @@ class LegalHoldRequestedViewModelTest { fun givenLegalHoldRequestReturnsFailure_whenGettingState_thenStateShouldBeHidden() = runTest { val (_, viewModel) = Arrangement() .withCurrentSessionExists() - .withLegalHoldRequestResult(ObserveLegalHoldRequestUseCaseResult.Failure(UNKNOWN_ERROR)) + .withLegalHoldRequestResult(ObserveLegalHoldRequestUseCase.Result.Failure(UNKNOWN_ERROR)) .arrange() advanceUntilIdle() viewModel.state shouldBeInstanceOf LegalHoldRequestedState.Hidden::class @@ -80,7 +80,7 @@ class LegalHoldRequestedViewModelTest { fun givenNoPendingLegalHoldRequest_whenGettingState_thenStateShouldBeHidden() = runTest { val (_, viewModel) = Arrangement() .withCurrentSessionExists() - .withLegalHoldRequestResult(ObserveLegalHoldRequestUseCaseResult.NoObserveLegalHoldRequest) + .withLegalHoldRequestResult(ObserveLegalHoldRequestUseCase.Result.NoLegalHoldRequest) .arrange() advanceUntilIdle() viewModel.state shouldBeInstanceOf LegalHoldRequestedState.Hidden::class @@ -91,7 +91,7 @@ class LegalHoldRequestedViewModelTest { val fingerprint = "fingerprint".toByteArray() val (_, viewModel) = Arrangement() .withCurrentSessionExists() - .withLegalHoldRequestResult(ObserveLegalHoldRequestUseCaseResult.ObserveLegalHoldRequestAvailable(fingerprint)) + .withLegalHoldRequestResult(ObserveLegalHoldRequestUseCase.Result.LegalHoldRequestAvailable(fingerprint)) .withIsPasswordRequiredResult(IsPasswordRequiredUseCase.Result.Success(true)) .arrange() advanceUntilIdle() @@ -106,7 +106,7 @@ class LegalHoldRequestedViewModelTest { val fingerprint = "fingerprint".toByteArray() val (_, viewModel) = Arrangement() .withCurrentSessionExists() - .withLegalHoldRequestResult(ObserveLegalHoldRequestUseCaseResult.ObserveLegalHoldRequestAvailable(fingerprint)) + .withLegalHoldRequestResult(ObserveLegalHoldRequestUseCase.Result.LegalHoldRequestAvailable(fingerprint)) .withIsPasswordRequiredResult(IsPasswordRequiredUseCase.Result.Success(true)) .arrange() advanceUntilIdle() @@ -121,7 +121,7 @@ class LegalHoldRequestedViewModelTest { val fingerprint = "fingerprint".toByteArray() val (_, viewModel) = Arrangement() .withCurrentSessionExists() - .withLegalHoldRequestResult(ObserveLegalHoldRequestUseCaseResult.ObserveLegalHoldRequestAvailable(fingerprint)) + .withLegalHoldRequestResult(ObserveLegalHoldRequestUseCase.Result.LegalHoldRequestAvailable(fingerprint)) .withIsPasswordRequiredResult(IsPasswordRequiredUseCase.Result.Success(false)) .arrange() advanceUntilIdle() @@ -133,7 +133,7 @@ class LegalHoldRequestedViewModelTest { private fun arrangeWithLegalHoldRequest(isPasswordRequired: Boolean = true) = Arrangement() .withCurrentSessionExists() - .withLegalHoldRequestResult(ObserveLegalHoldRequestUseCaseResult.ObserveLegalHoldRequestAvailable("fingerprint".toByteArray())) + .withLegalHoldRequestResult(ObserveLegalHoldRequestUseCase.Result.LegalHoldRequestAvailable("fingerprint".toByteArray())) .withIsPasswordRequiredResult(IsPasswordRequiredUseCase.Result.Success(isPasswordRequired)) private fun LegalHoldRequestedState.assertStateVisible(assert: (LegalHoldRequestedState.Visible) -> Unit) { @@ -259,7 +259,7 @@ class LegalHoldRequestedViewModelTest { every { coreLogic.globalScope { session.currentSessionFlow() } } returns flowOf(CurrentSessionResult.Success(AccountInfo.Valid(UserId("userId", "domain")))) } - fun withLegalHoldRequestResult(result: ObserveLegalHoldRequestUseCaseResult) = apply { + fun withLegalHoldRequestResult(result: ObserveLegalHoldRequestUseCase.Result) = apply { every { coreLogic.getSessionScope(any()).observeLegalHoldRequest() } returns flowOf(result) } fun withIsPasswordRequiredResult(result: IsPasswordRequiredUseCase.Result) = apply { diff --git a/kalium b/kalium index ab83fea4e2..faaa741b92 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit ab83fea4e235cc3c6936d5c7788dd90035014593 +Subproject commit faaa741b924617cd47600037c91de267c32bb66d