From 740c99f9dd98df86cb6d1f7481b77a743fd74330 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Tue, 19 Nov 2024 15:22:12 +0100 Subject: [PATCH 01/15] chore: bump next 4.9.1 --- build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt b/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt index a93b618ee19..51888ade39a 100644 --- a/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt +++ b/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt @@ -26,7 +26,7 @@ object AndroidSdk { object AndroidApp { const val id = "com.wire.android" - const val versionName = "4.9.0" + const val versionName = "4.9.1" val versionCode by lazy { Versionizer(_rootDir).versionCode } From 9a791bfd9fd009b831a7fa875fee44a85058923a Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Fri, 22 Nov 2024 16:00:06 +0100 Subject: [PATCH 02/15] chore: enabling pagination for beta --- default.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/default.json b/default.json index a2ba34da13d..5787cb41974 100644 --- a/default.json +++ b/default.json @@ -68,7 +68,8 @@ "encrypt_proteus_storage": true, "analytics_enabled": true, "analytics_app_key": "8ffae535f1836ed5f58fd5c8a11c00eca07c5438", - "analytics_server_url": "https://countly.wire.com/" + "analytics_server_url": "https://countly.wire.com/", + "paginated_conversation_list_enabled": true }, "internal": { "application_id": "com.wire.internal", From b5ec0276fe8a1869580cc2ca44d8003923b57793 Mon Sep 17 00:00:00 2001 From: boris Date: Tue, 26 Nov 2024 12:35:37 +0200 Subject: [PATCH 03/15] fix: Accessibility strings founded issues #WPB-9784 (#3657) --- .../com/wire/android/model/UserAvatarData.kt | 8 +++++ .../overview/CreateAccountOverviewParams.kt | 3 +- .../CreatePersonalAccountOverviewScreen.kt | 13 +++++++-- .../ui/authentication/devices/DeviceItem.kt | 29 ++++++++++++++++++- .../android/ui/common/UserProfileAvatar.kt | 15 ++++++++-- .../wire/android/ui/common/WireDropDown.kt | 21 ++++++++++---- .../ConversationParticipantItem.kt | 14 +++++++-- .../search/HighLightSubtTitle.kt | 6 +++- .../search/SearchUsersAndServicesScreen.kt | 7 +++-- .../common/ConversationItemFactory.kt | 25 +++++++++++----- .../ui/home/messagecomposer/MessageActions.kt | 2 +- .../recordaudio/RecordAudioButtons.kt | 2 +- .../self/dialog/LogoutOptionsDialog.kt | 3 +- app/src/main/res/values/strings.xml | 5 ++++ .../com/wire/android/ui/common/WireDialog.kt | 12 ++++---- .../bottomsheet/ModalSheetHeaderItem.kt | 4 +-- .../android/ui/common/button/WireButton.kt | 16 +++++++--- .../android/ui/common/button/WireItemLabel.kt | 6 ++-- .../ui/common/button/WirePrimaryButton.kt | 6 ++-- .../ui/common/button/WireSecondaryButton.kt | 6 ++-- .../ui/common/button/WireTertiaryButton.kt | 6 ++-- .../common/topappbar/NavigationIconButton.kt | 4 +-- .../ui-common/src/main/res/values/strings.xml | 2 ++ 23 files changed, 166 insertions(+), 49 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/model/UserAvatarData.kt b/app/src/main/kotlin/com/wire/android/model/UserAvatarData.kt index 5d49d78d5d3..372e2ee474f 100644 --- a/app/src/main/kotlin/com/wire/android/model/UserAvatarData.kt +++ b/app/src/main/kotlin/com/wire/android/model/UserAvatarData.kt @@ -19,6 +19,7 @@ package com.wire.android.model import androidx.compose.runtime.Stable +import com.wire.android.R import com.wire.android.ui.home.conversationslist.model.Membership import com.wire.android.util.EMPTY import com.wire.kalium.logic.data.user.ConnectionState @@ -37,6 +38,13 @@ data class UserAvatarData( return asset == null && nameBasedAvatar != null && nameBasedAvatar.initials.isEmpty().not() && membership != Membership.Service } + + fun getAvailabilityStatusDescriptionId(): Int? = when (availabilityStatus) { + UserAvailabilityStatus.NONE -> null + UserAvailabilityStatus.AVAILABLE -> R.string.user_profile_status_available + UserAvailabilityStatus.AWAY -> R.string.user_profile_status_away + UserAvailabilityStatus.BUSY -> R.string.user_profile_status_busy + } } /** diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/create/overview/CreateAccountOverviewParams.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/overview/CreateAccountOverviewParams.kt index 51d3d2336e0..d59243c3c77 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/create/overview/CreateAccountOverviewParams.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/overview/CreateAccountOverviewParams.kt @@ -26,5 +26,6 @@ data class CreateAccountOverviewParams( val contentText: String = "", @DrawableRes val contentIconResId: Int = 0, val learnMoreText: String = "", - val learnMoreUrl: String = "" + val learnMoreUrl: String = "", + val isContentTextSemanticAccessible: Boolean = false ) diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/create/overview/CreatePersonalAccountOverviewScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/overview/CreatePersonalAccountOverviewScreen.kt index ba76da0c892..7e2237c1ed3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/create/overview/CreatePersonalAccountOverviewScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/overview/CreatePersonalAccountOverviewScreen.kt @@ -109,7 +109,8 @@ fun CreateTeamAccountOverviewScreen( contentText = stringResource(id = overviewResources.overviewContentTextResId), contentIconResId = overviewResources.overviewContentIconResId, learnMoreText = stringResource(id = overviewResources.overviewLearnMoreTextResId), - learnMoreUrl = viewModel.learnMoreUrl() + learnMoreUrl = viewModel.learnMoreUrl(), + isContentTextSemanticAccessible = true ) ) } @@ -193,7 +194,15 @@ private fun OverviewTexts( text = overviewParams.contentText, style = MaterialTheme.wireTypography.body02, textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth().clearAndSetSemantics {} + modifier = Modifier + .fillMaxWidth() + .run { + if (overviewParams.isContentTextSemanticAccessible) { + this + } else { + clearAndSetSemantics {} + } + } ) Text( text = overviewParams.learnMoreText, diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/DeviceItem.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/DeviceItem.kt index a3a940da9e5..b0a7d95a2b4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/DeviceItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/DeviceItem.kt @@ -42,6 +42,8 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.wire.android.BuildConfig @@ -189,13 +191,22 @@ private fun ColumnScope.DeviceItemTexts( .fillMaxWidth() .shimmerPlaceholder(visible = placeholder) ) { + val deviceName = device.name.asString() + val shouldAddNotVerifiedLabel = shouldShowVerifyLabel && !shouldShowE2EIInfo && !(device.isVerifiedProteus && !isCurrentClient) + val semantic = if (shouldAddNotVerifiedLabel) { + val notVerifiedLabel = stringResource(R.string.label_client_unverified) + Modifier.clearAndSetSemantics { contentDescription = "$deviceName, $notVerifiedLabel" } + } else { + Modifier + } Text( style = MaterialTheme.wireTypography.body02, color = MaterialTheme.wireColorScheme.onBackground, - text = device.name.asString(), + text = deviceName, modifier = Modifier .wrapContentWidth() .shimmerPlaceholder(visible = placeholder) + .then(semantic) ) if (shouldShowVerifyLabel) { if (shouldShowE2EIInfo) { @@ -223,6 +234,16 @@ private fun ColumnScope.DeviceItemTexts( Spacer(modifier = Modifier.height(MaterialTheme.wireDimensions.removeDeviceItemTitleVerticalPadding)) + MLSDetails(device, placeholder) + + ProteusDetails(device, placeholder) +} + +@Composable +private fun MLSDetails( + device: Device, + placeholder: Boolean +) { device.mlsClientIdentity?.let { identity -> Text( style = MaterialTheme.wireTypography.subline01, @@ -238,7 +259,13 @@ private fun ColumnScope.DeviceItemTexts( .shimmerPlaceholder(visible = placeholder) ) } +} +@Composable +private fun ProteusDetails( + device: Device, + placeholder: Boolean +) { val proteusDetails: String = if (!device.registrationTime.isNullOrBlank()) { if (device.lastActiveInWholeWeeks != null) { stringResource( diff --git a/app/src/main/kotlin/com/wire/android/ui/common/UserProfileAvatar.kt b/app/src/main/kotlin/com/wire/android/ui/common/UserProfileAvatar.kt index abe895cabba..ccaf7a8eb44 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/UserProfileAvatar.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/UserProfileAvatar.kt @@ -57,7 +57,10 @@ import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -248,12 +251,19 @@ private fun DefaultInitialsAvatar( type: UserProfileAvatarType, size: Dp, modifier: Modifier = Modifier, + contentDescription: String? = null ) { - val contentDescription = stringResource(R.string.content_description_user_avatar) + val semantics = if (contentDescription != null) { + Modifier.semantics { + this.contentDescription = contentDescription + this.role = Role.Image + } + } else { + Modifier.clearAndSetSemantics { } + } Box( contentAlignment = Alignment.Center, modifier = modifier - .semantics { this.contentDescription = contentDescription } .size(size) .clip(CircleShape) .background( @@ -266,6 +276,7 @@ private fun DefaultInitialsAvatar( ) } ) + .then(semantics) ) { Text( text = nameBasedAvatar.initials, diff --git a/app/src/main/kotlin/com/wire/android/ui/common/WireDropDown.kt b/app/src/main/kotlin/com/wire/android/ui/common/WireDropDown.kt index 42d3a910900..d05b7c7e741 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/WireDropDown.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/WireDropDown.kt @@ -56,6 +56,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.paneTitle import androidx.compose.ui.semantics.selected import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview @@ -164,6 +165,8 @@ private fun MenuPopUp( hidePopUp: () -> Unit, onChange: (selectedIndex: Int) -> Unit ) { + val dropdownDescription = stringResource(R.string.content_description_drop_down) + MaterialTheme(shapes = MaterialTheme.shapes.copy(extraSmall = shape)) { // we want PopUp to cover the selection field, so we set this offset. // "- 8.dp" is because DropdownMenu has inner top padding, which can't be changed, @@ -178,14 +181,15 @@ private fun MenuPopUp( .width(with(LocalDensity.current) { textFieldWidth.width.toDp() }) .background(color = MaterialTheme.wireColorScheme.secondaryButtonEnabled) .border(width = 1.dp, color = borderColor, shape) + .semantics { paneTitle = dropdownDescription } ) { SelectionField( - Modifier.clickable(onClickLabel = stringResource(R.string.content_description_close_dropdown)) { hidePopUp() }, leadingCompose, selectedIndex, selectionText, - arrowRotation + arrowRotation, + Modifier.clickable(onClickLabel = stringResource(R.string.content_description_close_dropdown)) { hidePopUp() } ) List(items.size) { index -> @@ -210,11 +214,11 @@ private fun MenuPopUp( @Composable private fun SelectionField( - modifier: Modifier = Modifier, leadingCompose: @Composable ((index: Int) -> Unit)?, selectedIndex: Int, text: String, - arrowRotation: Float + arrowRotation: Float, + modifier: Modifier = Modifier ) { Row( modifier @@ -262,6 +266,7 @@ private fun DropdownItem( onClick: () -> Unit ) { val selectLabel = stringResource(R.string.content_description_select_label) + val closeDropdownLabel = stringResource(R.string.content_description_close_dropdown) return DropdownMenuItem( text = { Text( @@ -281,8 +286,12 @@ private fun DropdownItem( onClick = onClick, modifier = Modifier .semantics { - onClick(selectLabel) { false } - if (isSelected) selected = true + if (isSelected) { + selected = true + onClick(closeDropdownLabel) { false } + } else { + onClick(selectLabel) { false } + } } .background( color = if (isSelected) MaterialTheme.wireColorScheme.secondaryButtonSelected diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/ConversationParticipantItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/ConversationParticipantItem.kt index 846eedf887a..6fb983b7ae7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/ConversationParticipantItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/ConversationParticipantItem.kt @@ -28,6 +28,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import com.wire.android.BuildConfig import com.wire.android.R import com.wire.android.model.Clickable @@ -124,10 +126,18 @@ fun ConversationParticipantItem( } }, subtitle = { + val userName = processUsername(uiParticipant) + // Availability status should be called after username by TalkBack + val subtitleModifier = uiParticipant.avatarData.getAvailabilityStatusDescriptionId()?.let { + val contentDescription = stringResource(it) + Modifier.semantics { this.contentDescription = "$userName, $contentDescription" } + } ?: Modifier + HighlightSubtitle( - subTitle = processUsername(uiParticipant), + subTitle = userName, searchQuery = searchQuery, - prefix = processUsernamePrefix(uiParticipant) + prefix = processUsernamePrefix(uiParticipant), + modifier = subtitleModifier ) }, actions = { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/HighLightSubtTitle.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/HighLightSubtTitle.kt index 1909cbda730..12e1a7c4d4a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/HighLightSubtTitle.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/HighLightSubtTitle.kt @@ -21,6 +21,7 @@ package com.wire.android.ui.home.conversations.search import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextOverflow @@ -33,6 +34,7 @@ import com.wire.android.util.QueryMatchExtractor @Composable fun HighlightSubtitle( subTitle: String, + modifier: Modifier = Modifier, searchQuery: String = String.EMPTY, prefix: String = "@" ) { @@ -76,6 +78,7 @@ fun HighlightSubtitle( } } }, + modifier = modifier, maxLines = 1, overflow = TextOverflow.Ellipsis ) @@ -85,7 +88,8 @@ fun HighlightSubtitle( style = MaterialTheme.wireTypography.subline01, color = MaterialTheme.wireColorScheme.secondaryText, maxLines = 1, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, + modifier = modifier ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUsersAndServicesScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUsersAndServicesScreen.kt index ff3801d6bf9..743832d4877 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUsersAndServicesScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUsersAndServicesScreen.kt @@ -117,8 +117,11 @@ fun SearchUsersAndServicesScreen( SearchPeopleScreenType.CONVERSATION_DETAILS -> NavigationIconType.Close(R.string.content_description_add_participants_close) - SearchPeopleScreenType.NEW_CONVERSATION -> NavigationIconType.Close() - SearchPeopleScreenType.NEW_GROUP_CONVERSATION -> NavigationIconType.Back() + SearchPeopleScreenType.NEW_CONVERSATION -> + NavigationIconType.Close(R.string.content_description_new_conversation_close_btn) + + SearchPeopleScreenType.NEW_GROUP_CONVERSATION -> + NavigationIconType.Back(R.string.content_description_new_group_conversation_back_btn) }, onNavigationPressed = onClose ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemFactory.kt index 44ac89e3f7a..571b408bab4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemFactory.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemFactory.kt @@ -32,6 +32,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics import com.wire.android.R import com.wire.android.model.Clickable import com.wire.android.model.UserAvatarData @@ -75,16 +76,24 @@ fun ConversationItemFactory( ) { val openConversationOptionDescription = stringResource(R.string.content_description_conversation_details_more_btn) val openUserProfileDescription = stringResource(R.string.content_description_open_user_profile_label) + val acceptOrIgnoreDescription = stringResource(R.string.content_description_accept_or_ignore_connection_label) val openConversationDescription = stringResource(R.string.content_description_open_conversation_label) val onConversationItemClick = remember(conversation) { when (val lastEvent = conversation.lastMessageContent) { - is UILastMessageContent.Connection -> Clickable( - enabled = true, - onClick = { openUserProfile(lastEvent.userId) }, - onLongClick = null, - onClickDescription = openUserProfileDescription, - onLongClickDescription = null - ) + is UILastMessageContent.Connection -> { + val onClickDescription = if (conversation.badgeEventType == BadgeEventType.ReceivedConnectionRequest) { + acceptOrIgnoreDescription + } else { + openUserProfileDescription + } + Clickable( + enabled = true, + onClick = { openUserProfile(lastEvent.userId) }, + onLongClick = null, + onClickDescription = onClickDescription, + onLongClickDescription = null + ) + } else -> Clickable( enabled = true, @@ -97,7 +106,7 @@ fun ConversationItemFactory( } GeneralConversationItem( - modifier = modifier, + modifier = modifier.semantics(mergeDescendants = true) { }, conversation = conversation, isSelectable = isSelectableItem, isChecked = isChecked, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageActions.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageActions.kt index 17f9ad78bbc..30f5621a889 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageActions.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageActions.kt @@ -108,7 +108,7 @@ fun MessageEditActions( WireTertiaryIconButton( onButtonClicked = onEditCancelButtonClicked, iconResource = R.drawable.ic_close, - contentDescription = R.string.content_description_close_button, + contentDescription = R.string.label_close, shape = CircleShape, minSize = MaterialTheme.wireDimensions.buttonCircleMinSize, minClickableSize = MaterialTheme.wireDimensions.buttonMinClickableSize, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioButtons.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioButtons.kt index 33e010dc9b1..487e0cc5171 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioButtons.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioButtons.kt @@ -74,7 +74,7 @@ fun RecordAudioButtonClose( WireTertiaryIconButton( onButtonClicked = onClick, iconResource = R.drawable.ic_close, - contentDescription = R.string.content_description_close_button, + contentDescription = R.string.label_close, shape = CircleShape, minSize = MaterialTheme.wireDimensions.buttonCircleMinSize, minClickableSize = MaterialTheme.wireDimensions.buttonMinClickableSize, diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/dialog/LogoutOptionsDialog.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/dialog/LogoutOptionsDialog.kt index 221b012bcb6..b335e3713e8 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/dialog/LogoutOptionsDialog.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/dialog/LogoutOptionsDialog.kt @@ -52,7 +52,8 @@ fun LogoutOptionsDialog( dismissButtonProperties = WireDialogButtonProperties( onClick = dialogState::dismiss, text = stringResource(id = R.string.label_cancel), - state = WireButtonState.Default + state = WireButtonState.Default, + description = stringResource(R.string.dialog_logout_wipe_data_cancel_description) ), optionButton1Properties = WireDialogButtonProperties( onClick = remember(state) { { logout(state.shouldWipeData).also { dialogState.dismiss() } } }, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0e080e0598c..ecf54741471 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -211,14 +211,18 @@ toggle setting Go back to conversation details Go back to conversation details + accept or ignore the request open profile open conversation open service change it open link Alert + Dropdown close dropdown open notification settings + Close new conversation view + Go back to new conversation view Go back to new conversation view Type group name Conversation options @@ -618,6 +622,7 @@ Clear Data? Delete all your personal information and conversations on this device + Cancel logout Set yourself to Available You will appear as Available to other people. You will receive notifications for incoming calls and for messages according to the Notifications setting in each conversation. diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/WireDialog.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/WireDialog.kt index b4e0ab63aa6..c16829ab2df 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/WireDialog.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/WireDialog.kt @@ -41,6 +41,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.paneTitle import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.AnnotatedString @@ -230,7 +231,7 @@ fun WireDialogContent( @Composable private fun TitleDialogSection(title: String, titleLoading: Boolean) { Row(verticalAlignment = Alignment.CenterVertically) { - Text(text = title, style = MaterialTheme.wireTypography.title02) + Text(text = title, style = MaterialTheme.wireTypography.title02, modifier = Modifier.semantics { heading() }) if (titleLoading) { WireCircularProgressIndicator(progressColor = MaterialTheme.wireColorScheme.onBackground) } @@ -281,13 +282,13 @@ private fun WireDialogButtonProperties?.getButton(modifier: Modifier = Modifier) Box(modifier = modifier) { when (type) { WireDialogButtonType.Primary -> - WirePrimaryButton(onClick = onClick, text = text, state = state, loading = loading) + WirePrimaryButton(onClick = onClick, text = text, state = state, loading = loading, description = description) WireDialogButtonType.Secondary -> - WireSecondaryButton(onClick = onClick, text = text, state = state, loading = loading) + WireSecondaryButton(onClick = onClick, text = text, state = state, loading = loading, description = description) WireDialogButtonType.Tertiary -> - WireTertiaryButton(onClick = onClick, text = text, state = state, loading = loading) + WireTertiaryButton(onClick = onClick, text = text, state = state, loading = loading, description = description) } } } @@ -300,7 +301,8 @@ data class WireDialogButtonProperties( val onClick: () -> Unit, val state: WireButtonState = WireButtonState.Default, val type: WireDialogButtonType = WireDialogButtonType.Secondary, - val loading: Boolean = false + val loading: Boolean = false, + val description: String? = null ) data class DialogTextSuffixLink(val linkText: String, val linkUrl: String) diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/ModalSheetHeaderItem.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/ModalSheetHeaderItem.kt index 1f00db1864c..f43ac73b191 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/ModalSheetHeaderItem.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/ModalSheetHeaderItem.kt @@ -30,7 +30,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Dp import com.wire.android.ui.common.dimensions @@ -60,7 +60,7 @@ fun ModalSheetHeaderItem(header: MenuModalSheetHeader = MenuModalSheetHeader.Gon Text( text = header.title, style = MaterialTheme.wireTypography.title02, - modifier = Modifier.semantics { contentDescription = header.title } + modifier = Modifier.semantics { heading() } ) } WireDivider() diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/button/WireButton.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/button/WireButton.kt index 00b8e95b805..0e441d40c33 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/button/WireButton.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/button/WireButton.kt @@ -33,7 +33,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonElevation -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LocalMinimumInteractiveComponentSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -49,6 +48,8 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.layout.layout import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.onClick import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextStyle @@ -65,7 +66,6 @@ import com.wire.android.ui.theme.wireTypography import java.lang.Integer.max import kotlin.math.roundToInt -@OptIn(ExperimentalMaterial3Api::class) @Composable fun WireButton( onClick: () -> Unit, @@ -91,7 +91,8 @@ fun WireButton( vertical = MaterialTheme.wireDimensions.buttonVerticalContentPadding ), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - onClickDescription: String? = null + onClickDescription: String? = null, + description: String? = null ) { val border = when { borderWidth > 0.dp -> BorderStroke(width = borderWidth, color = colors.outlineColor(state).value) @@ -124,7 +125,10 @@ fun WireButton( placeable.place(centerX, centerY) } } - .semantics { onClickDescription?.let { onClick(it) { false } } }, + .semantics { + onClickDescription?.let { onClick(it) { false } } + description?.let { contentDescription = description } + }, enabled = state != WireButtonState.Disabled, interactionSource = interactionSource, elevation = elevation, @@ -144,6 +148,7 @@ fun WireButton( textStyle = textStyle, state = state, colors = colors, + semanticIgnoreText = !description.isNullOrEmpty() ) } } @@ -161,6 +166,7 @@ private fun InnerButtonBox( textStyle: TextStyle = MaterialTheme.wireTypography.button03, state: WireButtonState = WireButtonState.Default, colors: WireButtonColors = wirePrimaryButtonColors(), + semanticIgnoreText: Boolean = false ) { val contentColor = colors.contentColor(state).value val leadingItem: (@Composable () -> Unit) = { leadingIcon?.let { Tint(contentColor = contentColor, content = it) } } @@ -198,7 +204,9 @@ private fun InnerButtonBox( ) { if (leadingIconAlignment == IconAlignment.Center) leadingItem() if (!text.isNullOrEmpty()) { + val modifier = if (semanticIgnoreText) Modifier.clearAndSetSemantics { } else Modifier Text( + modifier = modifier, text = text, style = textStyle, color = contentColor diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/button/WireItemLabel.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/button/WireItemLabel.kt index 8a5ae5ec23b..51bf9059129 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/button/WireItemLabel.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/button/WireItemLabel.kt @@ -33,6 +33,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview @@ -53,13 +54,14 @@ fun WireItemLabel( contentDescription: String = text ) = Box( modifier = modifier - .border(width = 1.dp, color = MaterialTheme.wireColorScheme.divider, shape = shape) + .border(width = 1.dp, color = MaterialTheme.wireColorScheme.secondaryButtonDisabledOutline, shape = shape) .padding(contentPadding) - .semantics(mergeDescendants = true) { this.contentDescription = contentDescription } + .semantics { this.contentDescription = contentDescription } .wrapContentWidth() .wrapContentHeight(), ) { Text( + modifier = Modifier.clearAndSetSemantics { }, text = text, style = MaterialTheme.wireTypography.label02, ) diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/button/WirePrimaryButton.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/button/WirePrimaryButton.kt index 48b08791764..7b1dc2af159 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/button/WirePrimaryButton.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/button/WirePrimaryButton.kt @@ -71,7 +71,8 @@ fun WirePrimaryButton( vertical = MaterialTheme.wireDimensions.buttonVerticalContentPadding ), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - onClickDescription: String? = null + onClickDescription: String? = null, + description: String? = null ) = WireButton( onClick = onClick, loading = loading, @@ -93,7 +94,8 @@ fun WirePrimaryButton( contentPadding = contentPadding, interactionSource = interactionSource, modifier = modifier, - onClickDescription = onClickDescription + onClickDescription = onClickDescription, + description = description ) @Preview(name = "Default WirePrimaryButton") diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/button/WireSecondaryButton.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/button/WireSecondaryButton.kt index 4045fa2d517..9fdf93fdb77 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/button/WireSecondaryButton.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/button/WireSecondaryButton.kt @@ -70,7 +70,8 @@ fun WireSecondaryButton( vertical = MaterialTheme.wireDimensions.buttonVerticalContentPadding ), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - onClickDescription: String? = null + onClickDescription: String? = null, + description: String? = null ) = WireButton( onClick = onClick, loading = loading, @@ -92,7 +93,8 @@ fun WireSecondaryButton( contentPadding = contentPadding, interactionSource = interactionSource, modifier = modifier, - onClickDescription = onClickDescription + onClickDescription = onClickDescription, + description = description ) @Preview(name = "Default WireSecondaryButton") diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/button/WireTertiaryButton.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/button/WireTertiaryButton.kt index bcd77ea67bb..80696e05ecf 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/button/WireTertiaryButton.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/button/WireTertiaryButton.kt @@ -66,7 +66,8 @@ fun WireTertiaryButton( vertical = MaterialTheme.wireDimensions.buttonVerticalContentPadding ), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - onClickDescription: String? = null + onClickDescription: String? = null, + description: String? = null ) = WireButton( onClick = onClick, loading = loading, @@ -88,7 +89,8 @@ fun WireTertiaryButton( contentPadding = contentPadding, interactionSource = interactionSource, modifier = modifier, - onClickDescription = onClickDescription + onClickDescription = onClickDescription, + description = description ) @Preview(name = "Default WireSecondaryButton") diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/topappbar/NavigationIconButton.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/topappbar/NavigationIconButton.kt index f0618adca53..dd0bb92a1b4 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/topappbar/NavigationIconButton.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/topappbar/NavigationIconButton.kt @@ -45,10 +45,10 @@ fun BackNavigationIconButton(onBackButtonClick: () -> Unit) { } sealed class NavigationIconType(val icon: ImageVector, @StringRes open val contentDescription: Int) { - data class Back(@StringRes override val contentDescription: Int = R.string.content_description_back_button) : + data class Back(@StringRes override val contentDescription: Int = R.string.content_description_left_arrow) : NavigationIconType(Icons.AutoMirrored.Filled.ArrowBack, contentDescription) - data class Close(@StringRes override val contentDescription: Int = R.string.content_description_close_button) : + data class Close(@StringRes override val contentDescription: Int = R.string.content_description_close) : NavigationIconType(Icons.Filled.Close, contentDescription) data object Menu : NavigationIconType(Icons.Filled.Menu, R.string.content_description_menu_button) diff --git a/core/ui-common/src/main/res/values/strings.xml b/core/ui-common/src/main/res/values/strings.xml index 88bc5693363..ae450d738d4 100644 --- a/core/ui-common/src/main/res/values/strings.xml +++ b/core/ui-common/src/main/res/values/strings.xml @@ -20,7 +20,9 @@ Please wait until the app is synchronized Please wait until the Internet connection is restored Back button + Go Back Close button + Close Main navigation Drop down arrow pending approval of connection request From e0ceff42ae4eb62129d48542cbed27e6e7b011ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Zag=C3=B3rski?= Date: Tue, 26 Nov 2024 17:28:12 +0100 Subject: [PATCH 04/15] =?UTF-8?q?feat:=20Track=20qr-code=20analytics=20#WP?= =?UTF-8?q?B-11679=20=F0=9F=8D=92=20(#3565)=20(#3664)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/userprofile/qr/SelfQRCodeScreen.kt | 31 +++++++++-- .../ui/userprofile/qr/SelfQRCodeViewModel.kt | 8 +++ .../userprofile/self/SelfUserProfileScreen.kt | 1 + .../self/SelfUserProfileViewModel.kt | 9 +++- .../userprofile/qr/SelfQRCodeViewModelTest.kt | 7 ++- .../SelfUserProfileViewModelArrangement.kt | 7 ++- .../feature/analytics/model/AnalyticsEvent.kt | 51 +++++++++++++++++++ 7 files changed, 106 insertions(+), 8 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeScreen.kt index 26b5d29ef57..b4e763d20c8 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeScreen.kt @@ -20,6 +20,7 @@ package com.wire.android.ui.userprofile.qr import android.annotation.SuppressLint import android.graphics.Bitmap import android.net.Uri +import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box @@ -58,6 +59,7 @@ import com.lightspark.composeqr.DotShape import com.lightspark.composeqr.QrCodeView import com.ramcosta.composedestinations.annotation.RootNavGraph import com.wire.android.R +import com.wire.android.feature.analytics.model.AnalyticsEvent import com.wire.android.navigation.Navigator import com.wire.android.navigation.WireDestination import com.wire.android.navigation.style.SlideNavigationAnimation @@ -90,6 +92,7 @@ fun SelfQRCodeScreen( SelfQRCodeContent( viewModel.selfQRCodeState, viewModel::shareQRAsset, + viewModel::trackAnalyticsEvent, navigator::navigateBack ) } @@ -98,16 +101,26 @@ fun SelfQRCodeScreen( private fun SelfQRCodeContent( state: SelfQRCodeState, shareQRAssetClick: suspend (Bitmap) -> Uri, + trackAnalyticsEvent: (AnalyticsEvent.QrCode.Modal) -> Unit, onBackClick: () -> Unit = {} ) { val coroutineScope = rememberCoroutineScope() val graphicsLayer = rememberGraphicsLayer() val context = LocalContext.current + + BackHandler { + trackAnalyticsEvent(AnalyticsEvent.QrCode.Modal.Back) + onBackClick() + } + WireScaffold( topBar = { WireCenterAlignedTopAppBar( title = stringResource(id = R.string.user_profile_qr_code_title), - onNavigationPressed = onBackClick, + onNavigationPressed = { + trackAnalyticsEvent(AnalyticsEvent.QrCode.Modal.Back) + onBackClick() + }, elevation = 0.dp ) } @@ -190,9 +203,10 @@ private fun SelfQRCodeContent( color = colorsScheme().secondaryText ) Spacer(modifier = Modifier.weight(1f)) - ShareLinkButton(state.userAccountProfileLink) + ShareLinkButton(state.userAccountProfileLink, trackAnalyticsEvent) VerticalSpace.x8() ShareQRCodeButton { + trackAnalyticsEvent(AnalyticsEvent.QrCode.Modal.ShareQrCode) coroutineScope.launch { val bitmap = graphicsLayer.toImageBitmap() val qrUri = shareQRAssetClick(bitmap.asAndroidBitmap()) @@ -219,7 +233,10 @@ fun ShareQRCodeButton(shareQRAssetClick: () -> Unit) { } @Composable -private fun ShareLinkButton(selfProfileUrl: String) { +private fun ShareLinkButton( + selfProfileUrl: String, + trackAnalyticsEvent: (AnalyticsEvent.QrCode.Modal) -> Unit +) { val context = LocalContext.current WirePrimaryButton( modifier = @@ -229,7 +246,10 @@ private fun ShareLinkButton(selfProfileUrl: String) { .padding(horizontal = dimensions().spacing16x) .testTag("Share link"), text = stringResource(R.string.user_profile_qr_code_share_link), - onClick = { context.shareLinkToProfile(selfProfileUrl) } + onClick = { + trackAnalyticsEvent(AnalyticsEvent.QrCode.Modal.ShareProfileLink) + context.shareLinkToProfile(selfProfileUrl) + } ) } @@ -245,7 +265,8 @@ fun PreviewSelfQRCodeContent() { handle = "userid", userProfileLink = "https://account.wire.com/user-profile/?id=aaaaaaa-222-3333-4444-55555555" ), - { "".toUri() } + { "".toUri() }, + { } ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModel.kt index 9dc471eb8b8..b996715e0a9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModel.kt @@ -28,6 +28,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.appLogger import com.wire.android.di.CurrentAccount +import com.wire.android.feature.analytics.AnonymousAnalyticsManager +import com.wire.android.feature.analytics.model.AnalyticsEvent import com.wire.android.ui.navArgs import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.getTempWritableAttachmentUri @@ -51,6 +53,7 @@ class SelfQRCodeViewModel @Inject constructor( private val selfServerLinks: SelfServerConfigUseCase, private val kaliumFileSystem: KaliumFileSystem, private val dispatchers: DispatcherProvider, + private val analyticsManager: AnonymousAnalyticsManager ) : ViewModel() { private val selfQrCodeNavArgs: SelfQrCodeNavArgs = savedStateHandle.navArgs() var selfQRCodeState by mutableStateOf(SelfQRCodeState(selfUserId, handle = selfQrCodeNavArgs.handle)) @@ -59,6 +62,7 @@ class SelfQRCodeViewModel @Inject constructor( get() = kaliumFileSystem.rootCachePath init { + trackAnalyticsEvent(AnalyticsEvent.QrCode.Modal.Displayed) viewModelScope.launch { getServerLinks() } @@ -83,6 +87,10 @@ class SelfQRCodeViewModel @Inject constructor( return job.await() } + fun trackAnalyticsEvent(event: AnalyticsEvent.QrCode.Modal) { + analyticsManager.sendEvent(event) + } + private suspend fun getTempWritableQRUri(tempCachePath: Path): Uri = withContext(dispatchers.io()) { val tempImagePath = "$tempCachePath/$TEMP_SELF_QR_FILENAME".toPath() return@withContext getTempWritableAttachmentUri(context, tempImagePath) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt index 49366b29a5e..04bf55420b3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt @@ -135,6 +135,7 @@ fun SelfUserProfileScreen( onLegalHoldLearnMoreClick = remember { { legalHoldSubjectDialogState.show(Unit) } }, onOtherAccountClick = { viewModelSelf.switchAccount(it, NavigationSwitchAccountActions(navigator::navigate)) }, onQrCodeClick = { + viewModelSelf.trackQrCodeClick() navigator.navigate(NavigationCommand(SelfQRCodeScreenDestination(viewModelSelf.userProfileState.userName))) }, isUserInCall = viewModelSelf::isUserInCall, diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt index b97a5e253e4..09b9d4ef760 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt @@ -33,6 +33,8 @@ import com.wire.android.feature.AccountSwitchUseCase import com.wire.android.feature.SwitchAccountActions import com.wire.android.feature.SwitchAccountParam import com.wire.android.feature.SwitchAccountResult +import com.wire.android.feature.analytics.AnonymousAnalyticsManager +import com.wire.android.feature.analytics.model.AnalyticsEvent import com.wire.android.mapper.OtherAccountMapper import com.wire.android.model.ImageAsset.UserAvatarAsset import com.wire.android.notification.NotificationChannelsManager @@ -103,7 +105,8 @@ class SelfUserProfileViewModel @Inject constructor( private val notificationChannelsManager: NotificationChannelsManager, private val notificationManager: WireNotificationManager, private val globalDataStore: GlobalDataStore, - private val qualifiedIdMapper: QualifiedIdMapper + private val qualifiedIdMapper: QualifiedIdMapper, + private val analyticsManager: AnonymousAnalyticsManager ) : ViewModel() { var userProfileState by mutableStateOf(SelfUserProfileState(userId = selfUserId, isAvatarLoading = true)) @@ -331,6 +334,10 @@ class SelfUserProfileViewModel @Inject constructor( userProfileState = userProfileState.copy(errorMessageCode = null) } + fun trackQrCodeClick() { + analyticsManager.sendEvent(AnalyticsEvent.QrCode.Click(!userProfileState.teamName.isNullOrBlank())) + } + sealed class ErrorCodes { object DownloadUserInfoError : ErrorCodes() } diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModelTest.kt index 3a9085cfd6e..5c690d7398a 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModelTest.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.SavedStateHandle import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.NavigationTestExtension import com.wire.android.config.TestDispatcherProvider +import com.wire.android.feature.analytics.AnonymousAnalyticsManager import com.wire.android.framework.FakeKaliumFileSystem import com.wire.android.framework.TestUser import com.wire.android.ui.navArgs @@ -50,6 +51,9 @@ class SelfQRCodeViewModelTest { @MockK lateinit var selfServerConfig: SelfServerConfigUseCase + @MockK + lateinit var analyticsManager: AnonymousAnalyticsManager + val context = mockk() init { @@ -66,7 +70,8 @@ class SelfQRCodeViewModelTest { selfUserId = TestUser.SELF_USER.id, selfServerLinks = selfServerConfig, kaliumFileSystem = fakeKaliumFileSystem, - dispatchers = TestDispatcherProvider() + dispatchers = TestDispatcherProvider(), + analyticsManager = analyticsManager ) val fakeKaliumFileSystem: FakeKaliumFileSystem = FakeKaliumFileSystem() diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelArrangement.kt index 87ddf9e9999..e9478646aa1 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelArrangement.kt @@ -23,6 +23,7 @@ import com.wire.android.datastore.GlobalDataStore import com.wire.android.datastore.UserDataStore import com.wire.android.di.AuthServerConfigProvider import com.wire.android.feature.AccountSwitchUseCase +import com.wire.android.feature.analytics.AnonymousAnalyticsManager import com.wire.android.framework.TestTeam import com.wire.android.framework.TestUser import com.wire.android.mapper.OtherAccountMapper @@ -90,6 +91,9 @@ class SelfUserProfileViewModelArrangement { @MockK lateinit var qualifiedIdMapper: QualifiedIdMapper + @MockK + lateinit var analyticsManager: AnonymousAnalyticsManager + private val viewModel by lazy { SelfUserProfileViewModel( selfUserId = TestUser.SELF_USER.id, @@ -112,7 +116,8 @@ class SelfUserProfileViewModelArrangement { notificationChannelsManager = notificationChannelsManager, notificationManager = notificationManager, globalDataStore = globalDataStore, - qualifiedIdMapper = qualifiedIdMapper + qualifiedIdMapper = qualifiedIdMapper, + analyticsManager = analyticsManager ) } diff --git a/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/model/AnalyticsEvent.kt b/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/model/AnalyticsEvent.kt index 418e9a2f895..cb1e4ecbcdd 100644 --- a/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/model/AnalyticsEvent.kt +++ b/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/model/AnalyticsEvent.kt @@ -26,6 +26,8 @@ import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_ import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_QUALITY_REVIEW_SCORE_KEY import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CONTRIBUTED_LOCATION import com.wire.android.feature.analytics.model.AnalyticsEventConstants.MESSAGE_ACTION_KEY +import com.wire.android.feature.analytics.model.AnalyticsEventConstants.QR_CODE_SEGMENTATION_USER_TYPE_PERSONAL +import com.wire.android.feature.analytics.model.AnalyticsEventConstants.QR_CODE_SEGMENTATION_USER_TYPE_TEAM interface AnalyticsEvent { /** @@ -183,6 +185,42 @@ interface AnalyticsEvent { override val messageAction: String = AnalyticsEventConstants.CONTRIBUTED_AUDIO } } + + sealed class QrCode : AnalyticsEvent { + data class Click(val isTeam: Boolean) : QrCode() { + override val key: String = AnalyticsEventConstants.QR_CODE_CLICK + + override fun toSegmentation(): Map { + val userType = if (isTeam) { + QR_CODE_SEGMENTATION_USER_TYPE_TEAM + } else { + QR_CODE_SEGMENTATION_USER_TYPE_PERSONAL + } + + return mapOf( + AnalyticsEventConstants.QR_CODE_SEGMENTATION_USER_TYPE to userType + ) + } + } + + sealed class Modal : QrCode() { + data object Displayed : Modal() { + override val key: String = AnalyticsEventConstants.QR_CODE_MODAL + } + + data object Back : Modal() { + override val key: String = AnalyticsEventConstants.QR_CODE_MODAL_BACK + } + + data object ShareProfileLink : Modal() { + override val key: String = AnalyticsEventConstants.QR_CODE_SHARE_PROFILE_LINK + } + + data object ShareQrCode : Modal() { + override val key: String = AnalyticsEventConstants.QR_CODE_SHARE_QR_CODE + } + } + } } object AnalyticsEventConstants { @@ -230,4 +268,17 @@ object AnalyticsEventConstants { const val CONTRIBUTED_VIDEO = "video" const val CONTRIBUTED_AUDIO = "audio" const val CONTRIBUTED_LOCATION = "location" + + /** + * Qr code + */ + const val QR_CODE_CLICK = "ui.QR-click" + const val QR_CODE_MODAL = "ui.share.profile" + const val QR_CODE_MODAL_BACK = "user.back.share-profile" + const val QR_CODE_SHARE_PROFILE_LINK = "user.share-profile" + const val QR_CODE_SHARE_QR_CODE = "user.QR-code" + + const val QR_CODE_SEGMENTATION_USER_TYPE = "user_type" + const val QR_CODE_SEGMENTATION_USER_TYPE_PERSONAL = "personal" + const val QR_CODE_SEGMENTATION_USER_TYPE_TEAM = "team" } From 334b8216caae0089792d7ed0a9651f1d257d7d3c Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Wed, 27 Nov 2024 09:33:30 +0100 Subject: [PATCH 05/15] feat: disable x86 support [WPB-11427] (#3600) --- .github/workflows/gradle-run-ui-tests.yml | 1 + app/build.gradle.kts | 8 ++++++++ buildSrc/src/main/kotlin/scripts/variants.gradle.kts | 1 + 3 files changed, 10 insertions(+) diff --git a/.github/workflows/gradle-run-ui-tests.yml b/.github/workflows/gradle-run-ui-tests.yml index 43ca3b9ce3b..aa665f635d9 100644 --- a/.github/workflows/gradle-run-ui-tests.yml +++ b/.github/workflows/gradle-run-ui-tests.yml @@ -56,6 +56,7 @@ jobs: with: api-level: ${{ matrix.api-level }} target: google_apis + arch: x86_64 script: ./gradlew runAcceptanceTests env: GITHUB_USER: ${{ github.actor }} diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 08f044e4fab..95f1951caee 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -61,6 +61,14 @@ private fun getFlavorsSettings(): NormalizedFlavorSettings = android { defaultConfig { + ndk { + abiFilters.apply { + add("armeabi-v7a") + add("arm64-v8a") + add("x86_64") + } + } + val datadogApiKeyKey = "DATADOG_CLIENT_TOKEN" val datadogApiKey: String? = System.getenv(datadogApiKeyKey) ?: project.getLocalProperty(datadogApiKeyKey, null) buildConfigField("String", datadogApiKeyKey, datadogApiKey?.let { "\"$it\"" } ?: "null") diff --git a/buildSrc/src/main/kotlin/scripts/variants.gradle.kts b/buildSrc/src/main/kotlin/scripts/variants.gradle.kts index b69ef44b9a3..5c7ed81c4ce 100644 --- a/buildSrc/src/main/kotlin/scripts/variants.gradle.kts +++ b/buildSrc/src/main/kotlin/scripts/variants.gradle.kts @@ -108,6 +108,7 @@ android { } buildTypes { + getByName(BuildTypes.DEBUG) { isMinifyEnabled = false applicationIdSuffix = ".${BuildTypes.DEBUG}" From a0c500a5930a38f64cba4e0569e3f54326d3a83e Mon Sep 17 00:00:00 2001 From: Yamil Medina Date: Wed, 27 Nov 2024 05:43:43 -0300 Subject: [PATCH 06/15] chore: bulletproofing crypto box to cc migration (WPB-14250) (#3658) --- .../kotlin/com/wire/android/feature/AccountSwitchUseCase.kt | 1 + .../main/kotlin/com/wire/android/ui/WireActivityViewModel.kt | 2 +- default.json | 2 +- kalium | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/feature/AccountSwitchUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/AccountSwitchUseCase.kt index 921e20b233c..4d59a2861b2 100644 --- a/app/src/main/kotlin/com/wire/android/feature/AccountSwitchUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/feature/AccountSwitchUseCase.kt @@ -167,6 +167,7 @@ class AccountSwitchUseCase @Inject constructor( LogoutReason.SELF_SOFT_LOGOUT, LogoutReason.SELF_HARD_LOGOUT -> { deleteSession(invalidAccount.userId) } + LogoutReason.MIGRATION_TO_CC_FAILED, LogoutReason.DELETED_ACCOUNT, LogoutReason.REMOVED_CLIENT, LogoutReason.SESSION_EXPIRED -> deleteSession(invalidAccount.userId) diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt index 4fefdb9e261..624b6aa74a5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt @@ -292,7 +292,7 @@ class WireActivityViewModel @Inject constructor( // Self logout is handled from the Self user profile screen directly } - LogoutReason.REMOVED_CLIENT -> + LogoutReason.MIGRATION_TO_CC_FAILED, LogoutReason.REMOVED_CLIENT -> globalAppState = globalAppState.copy(blockUserUI = CurrentSessionErrorState.RemovedClient) diff --git a/default.json b/default.json index 5787cb41974..b3c0cdc263a 100644 --- a/default.json +++ b/default.json @@ -29,7 +29,7 @@ "default_backend_url_blacklist": "https://clientblacklist.wire.com/staging", "default_backend_url_website": "https://wire.com", "default_backend_title": "wire-staging", - "encrypt_proteus_storage": true, + "encrypt_proteus_storage": false, "analytics_enabled": false, "picture_in_picture_enabled": true, "analytics_app_key": "8ffae535f1836ed5f58fd5c8a11c00eca07c5438", diff --git a/kalium b/kalium index 7440f44a015..5b2361e4f96 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 7440f44a0153dd4527e88991610db0260fab4762 +Subproject commit 5b2361e4f96ef7f51410ff97fe6e1a907e84d455 From 4c4b86f4ab535c6f1b0cdfeaeb348aaf53c5d480 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Wed, 27 Nov 2024 16:19:17 +0100 Subject: [PATCH 07/15] chore: fix kalium ref --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 5b2361e4f96..a307ae3911f 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 5b2361e4f96ef7f51410ff97fe6e1a907e84d455 +Subproject commit a307ae3911f6c62188d7b66cf55b15c9823990f0 From 8e66a7b153838d5ffab1a89098de868575dca928 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Wed, 27 Nov 2024 16:58:03 +0100 Subject: [PATCH 08/15] fix: set Foreground Service Behavior IMMEDIATE (WPB-5512) (#3666) --- .../com/wire/android/notification/CallNotificationManager.kt | 3 +++ .../com/wire/android/services/PersistentWebSocketService.kt | 1 + 2 files changed, 4 insertions(+) diff --git a/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt b/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt index aebd67bb1ed..a01b02beea0 100644 --- a/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt +++ b/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt @@ -23,6 +23,7 @@ import android.app.Notification import android.content.Context import android.service.notification.StatusBarNotification import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE import androidx.core.app.NotificationManagerCompat import com.wire.android.R import com.wire.android.appLogger @@ -199,6 +200,7 @@ class CallNotificationBuilder @Inject constructor( .setOngoing(true) .setSilent(true) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setForegroundServiceBehavior(FOREGROUND_SERVICE_IMMEDIATE) .addAction(getHangUpCallAction(context, conversationIdString, userIdString)) .setFullScreenIntent(outgoingCallPendingIntent(context, conversationIdString), true) .setContentIntent(outgoingCallPendingIntent(context, conversationIdString)) @@ -254,6 +256,7 @@ class CallNotificationBuilder @Inject constructor( .setAutoCancel(true) .setOngoing(true) .setUsesChronometer(true) + .setForegroundServiceBehavior(FOREGROUND_SERVICE_IMMEDIATE) .addAction(getHangUpCallAction(context, conversationIdString, userIdString)) .addAction(getOpenOngoingCallAction(context, conversationIdString)) .setFullScreenIntent(openOngoingCallPendingIntent(context, conversationIdString), true) diff --git a/app/src/main/kotlin/com/wire/android/services/PersistentWebSocketService.kt b/app/src/main/kotlin/com/wire/android/services/PersistentWebSocketService.kt index 75087eaeac7..4d2f5c4c5f4 100644 --- a/app/src/main/kotlin/com/wire/android/services/PersistentWebSocketService.kt +++ b/app/src/main/kotlin/com/wire/android/services/PersistentWebSocketService.kt @@ -129,6 +129,7 @@ class PersistentWebSocketService : Service() { .setSmallIcon(R.drawable.websocket_notification_icon_small) .setContentIntent(openAppPendingIntent(this)) .setCategory(NotificationCompat.CATEGORY_SERVICE) + .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) .setAutoCancel(false) .setOngoing(true) .build() From dc825b0e9f85f94fd15e3f4aa68a382c894c6a45 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Fri, 27 Sep 2024 12:17:46 +0200 Subject: [PATCH 09/15] feat: enable countly on prod build (cherry picked from commit dc785a816dc6b4bf4c63ece616b66e12a526d438) --- default.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default.json b/default.json index b3c0cdc263a..6463e8ff2d5 100644 --- a/default.json +++ b/default.json @@ -7,7 +7,7 @@ "application_is_private_build": false, "development_api_enabled": false, "mls_support_enabled": false, - "analytics_enabled": false, + "analytics_enabled": true, "analytics_app_key": "4483f7a58ae3e70b3780319c4ccb5c88a037be49", "analytics_server_url": "https://countly.wire.com/" }, From 15a1b5ee6c8f36f5c03c86851a72245b45f8392d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= <30429749+saleniuk@users.noreply.github.com> Date: Thu, 28 Nov 2024 11:44:22 +0100 Subject: [PATCH 10/15] fix: crash when saving bottom sheet state [WPB-14433] (#3670) --- .../com/wire/android/mapper/ContactMapper.kt | 8 +- .../wire/android/mapper/ConversationMapper.kt | 6 +- .../com/wire/android/mapper/MessageMapper.kt | 5 +- .../wire/android/mapper/OtherAccountMapper.kt | 7 +- .../mapper/RegularMessageContentMapper.kt | 4 - .../mapper/SystemMessageContentMapper.kt | 6 +- .../android/mapper/UICallParticipantMapper.kt | 4 +- .../android/mapper/UIParticipantMapper.kt | 10 +- .../com/wire/android/model/ImageAsset.kt | 54 +- .../com/wire/android/model/UserAvatarData.kt | 3 + .../ui/calling/SharedCallingViewModel.kt | 4 +- .../com/wire/android/ui/home/HomeViewModel.kt | 7 +- .../conversations/ConversationMemberExt.kt | 15 +- .../info/ConversationInfoViewModel.kt | 4 +- .../messages/item/SystemMessageItem.kt | 29 +- .../messages/item/SystemMessageItemLeading.kt | 4 +- .../ui/home/conversations/mock/Mock.kt | 33 +- .../ui/home/conversations/model/UIMessage.kt | 598 ++++++++++-------- .../conversations/model/UIQuotedMessage.kt | 6 +- .../GetConversationsFromSearchUseCase.kt | 4 +- .../ConversationListViewModel.kt | 3 - .../conversationslist/model/BadgeEventType.kt | 41 +- .../model/ConversationItem.kt | 6 + .../ui/home/gallery/MediaGalleryViewModel.kt | 3 - .../ImportMediaAuthenticatedViewModel.kt | 4 +- .../other/OtherUserProfileScreenViewModel.kt | 4 +- .../self/SelfUserProfileViewModel.kt | 4 +- .../service/ServiceDetailsViewModel.kt | 4 +- .../util/ui/LocalizedStringResource.kt | 33 +- .../kotlin/com/wire/android/util/ui/UIText.kt | 11 +- .../wire/android/mapper/MessageMapperTest.kt | 6 +- .../android/mapper/OtherAccountMapperTest.kt | 12 +- .../mapper/RegularMessageContentMapperTest.kt | 6 +- .../mapper/UICallParticipantMapperTest.kt | 3 +- .../android/mapper/UIParticipantMapperTest.kt | 12 +- .../com/wire/android/model/ImageAssetTest.kt | 11 +- .../android/navigation/NavigationUtilsTest.kt | 13 +- .../ui/calling/SharedCallingViewModelTest.kt | 10 +- .../ConnectionActionButtonViewModelTest.kt | 4 - .../wire/android/ui/home/HomeViewModelTest.kt | 4 - ...eParticipantsForConversationUseCaseTest.kt | 5 +- .../ConversationInfoViewModelArrangement.kt | 5 - .../GetConversationsFromSearchUseCaseTest.kt | 6 +- .../ConversationListViewModelTest.kt | 5 - .../home/gallery/MediaGalleryViewModelTest.kt | 5 - .../ImportMediaAuthenticatedViewModelTest.kt | 5 - .../OtherUserProfileViewModelArrangement.kt | 7 +- .../SelfUserProfileViewModelArrangement.kt | 4 - .../service/ServiceDetailsViewModelTest.kt | 5 - .../android/util/ui/AssetImageFetcherTest.kt | 22 +- core/ui-common/build.gradle.kts | 3 + .../common/bottomsheet/WireModalSheetState.kt | 59 +- kalium | 2 +- 53 files changed, 571 insertions(+), 567 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/mapper/ContactMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/ContactMapper.kt index af690cfbd0a..3644579c3b8 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/ContactMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/ContactMapper.kt @@ -25,7 +25,6 @@ import com.wire.android.ui.home.conversationslist.model.Membership import com.wire.android.ui.home.newconversation.model.Contact import com.wire.android.ui.userprofile.common.UsernameMapper import com.wire.android.util.EMPTY -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.publicuser.model.UserSearchDetails import com.wire.kalium.logic.data.service.ServiceDetails import com.wire.kalium.logic.data.user.ConnectionState @@ -36,7 +35,6 @@ import javax.inject.Inject class ContactMapper @Inject constructor( private val userTypeMapper: UserTypeMapper, - private val wireSessionImageLoader: WireSessionImageLoader, ) { fun fromOtherUser(otherUser: OtherUser): Contact { @@ -48,7 +46,7 @@ class ContactMapper handle = handle.orEmpty(), label = UsernameMapper.fromOtherUser(otherUser), avatarData = UserAvatarData( - asset = previewPicture?.let { ImageAsset.UserAvatarAsset(wireSessionImageLoader, it) }, + asset = previewPicture?.let { ImageAsset.UserAvatarAsset(it) }, connectionState = connectionStatus, nameBasedAvatar = NameBasedAvatar(fullName = name, accentColor = otherUser.accentId) ), @@ -67,7 +65,7 @@ class ContactMapper handle = String.EMPTY, label = String.EMPTY, avatarData = UserAvatarData( - asset = previewAssetId?.let { ImageAsset.UserAvatarAsset(wireSessionImageLoader, it) }, + asset = previewAssetId?.let { ImageAsset.UserAvatarAsset(it) }, membership = Membership.Service ), membership = Membership.Service, @@ -85,7 +83,7 @@ class ContactMapper handle = handle.orEmpty(), label = mapUserHandle(user), avatarData = UserAvatarData( - asset = previewAssetId?.let { ImageAsset.UserAvatarAsset(wireSessionImageLoader, it) }, + asset = previewAssetId?.let { ImageAsset.UserAvatarAsset(it) }, nameBasedAvatar = NameBasedAvatar(fullName = name, accentColor = -1) ), membership = userTypeMapper.toMembership(type), diff --git a/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt index fb7ca3929a0..ce28fe8aaa7 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt @@ -26,7 +26,6 @@ import com.wire.android.ui.home.conversationslist.model.BlockState import com.wire.android.ui.home.conversationslist.model.ConversationInfo import com.wire.android.ui.home.conversationslist.model.ConversationItem import com.wire.android.ui.home.conversationslist.showLegalHoldIndicator -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.conversation.ConversationDetails.Connection import com.wire.kalium.logic.data.conversation.ConversationDetails.Group import com.wire.kalium.logic.data.conversation.ConversationDetails.OneOne @@ -40,7 +39,6 @@ import com.wire.kalium.logic.data.user.UserAvailabilityStatus @Suppress("LongMethod") fun ConversationDetailsWithEvents.toConversationItem( - wireSessionImageLoader: WireSessionImageLoader, userTypeMapper: UserTypeMapper, searchQuery: String, ): ConversationItem = when (val conversationDetails = this.conversationDetails) { @@ -71,7 +69,7 @@ fun ConversationDetailsWithEvents.toConversationItem( is OneOne -> { ConversationItem.PrivateConversation( userAvatarData = UserAvatarData( - asset = conversationDetails.otherUser.previewPicture?.let { UserAvatarAsset(wireSessionImageLoader, it) }, + asset = conversationDetails.otherUser.previewPicture?.let { UserAvatarAsset(it) }, availabilityStatus = conversationDetails.otherUser.availabilityStatus, connectionState = conversationDetails.otherUser.connectionStatus, nameBasedAvatar = NameBasedAvatar(conversationDetails.otherUser.name, conversationDetails.otherUser.accentId) @@ -107,7 +105,7 @@ fun ConversationDetailsWithEvents.toConversationItem( is Connection -> { ConversationItem.ConnectionConversation( userAvatarData = UserAvatarData( - asset = conversationDetails.otherUser?.previewPicture?.let { UserAvatarAsset(wireSessionImageLoader, it) }, + asset = conversationDetails.otherUser?.previewPicture?.let { UserAvatarAsset(it) }, availabilityStatus = conversationDetails.otherUser?.availabilityStatus ?: UserAvailabilityStatus.NONE, nameBasedAvatar = NameBasedAvatar(conversationDetails.otherUser?.name, conversationDetails.otherUser?.accentId ?: -1) ), 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 24bb43d7d2f..0f3b1e48283 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/MessageMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/MessageMapper.kt @@ -37,7 +37,6 @@ import com.wire.android.ui.home.conversationslist.model.Membership import com.wire.android.ui.theme.Accent import com.wire.android.util.time.ISOFormatter import com.wire.android.util.ui.UIText -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.message.DeliveryStatus import com.wire.kalium.logic.data.message.Message import com.wire.kalium.logic.data.message.MessageContent @@ -52,8 +51,6 @@ class MessageMapper @Inject constructor( private val userTypeMapper: UserTypeMapper, private val messageContentMapper: MessageContentMapper, private val isoFormatter: ISOFormatter, - // TODO(qol): a message mapper should not depend on a UI related component - private val wireSessionImageLoader: WireSessionImageLoader ) { fun memberIdList(messages: List): List = messages.flatMap { message -> @@ -200,7 +197,7 @@ class MessageMapper @Inject constructor( } private fun getUserAvatarData(sender: User?) = UserAvatarData( - asset = sender?.previewAsset(wireSessionImageLoader), + asset = sender?.previewAsset(), availabilityStatus = sender?.availabilityStatus ?: UserAvailabilityStatus.NONE, membership = sender?.userType?.let { userTypeMapper.toMembership(it) } ?: Membership.None, connectionState = getConnectionState(sender), diff --git a/app/src/main/kotlin/com/wire/android/mapper/OtherAccountMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/OtherAccountMapper.kt index 60a9077efab..3a9b0d2889e 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/OtherAccountMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/OtherAccountMapper.kt @@ -20,18 +20,15 @@ package com.wire.android.mapper import com.wire.android.ui.home.conversations.avatar import com.wire.android.ui.userprofile.self.model.OtherAccount -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.team.Team import com.wire.kalium.logic.data.user.SelfUser import javax.inject.Inject -class OtherAccountMapper @Inject constructor( - private val wireSessionImageLoader: WireSessionImageLoader -) { +class OtherAccountMapper @Inject constructor() { fun toOtherAccount(selfUser: SelfUser, team: Team?): OtherAccount = OtherAccount( id = selfUser.id, fullName = selfUser.name ?: "", - avatarData = selfUser.avatar(wireSessionImageLoader, selfUser.connectionStatus), + avatarData = selfUser.avatar(selfUser.connectionStatus), teamName = team?.name ) } diff --git a/app/src/main/kotlin/com/wire/android/mapper/RegularMessageContentMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/RegularMessageContentMapper.kt index 6336f558bb4..eabb1d96bec 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/RegularMessageContentMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/RegularMessageContentMapper.kt @@ -29,7 +29,6 @@ import com.wire.android.ui.home.conversations.model.UIMessageContent import com.wire.android.ui.home.conversations.model.UIQuotedMessage import com.wire.android.util.time.ISOFormatter import com.wire.android.util.ui.UIText -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.asset.AttachmentType import com.wire.kalium.logic.data.asset.isDisplayableImageMimeType import com.wire.kalium.logic.data.id.ConversationId @@ -50,7 +49,6 @@ import javax.inject.Inject @Suppress("TooManyFunctions") class RegularMessageMapper @Inject constructor( private val messageResourceProvider: MessageResourceProvider, - private val wireSessionImageLoader: WireSessionImageLoader, private val isoFormatter: ISOFormatter, ) { @@ -207,7 +205,6 @@ class RegularMessageMapper @Inject constructor( is MessageContent.QuotedMessageDetails.Asset -> when (AttachmentType.fromMimeTypeString(quotedContent.assetMimeType)) { AttachmentType.IMAGE -> UIQuotedMessage.UIQuotedData.DisplayableImage( ImageAsset.PrivateAsset( - wireSessionImageLoader, conversationId, it.messageId, it.isQuotingSelfUser @@ -249,7 +246,6 @@ class RegularMessageMapper @Inject constructor( UIMessageContent.ImageMessage( assetId = AssetId(remoteData.assetId, remoteData.assetDomain.orEmpty()), asset = ImageAsset.PrivateAsset( - wireSessionImageLoader, message.conversationId, message.id, sender is SelfUser 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 611c6f1d0eb..80183256282 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/SystemMessageContentMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/SystemMessageContentMapper.kt @@ -185,7 +185,7 @@ class SystemMessageContentMapper @Inject constructor( private fun mapTeamMemberRemovedMessage( content: MessageContent.TeamMemberRemoved - ): UIMessageContent.SystemMessage = UIMessageContent.SystemMessage.TeamMemberRemoved_Legacy(content) + ): UIMessageContent.SystemMessage = UIMessageContent.SystemMessage.TeamMemberRemoved_Legacy(content.userName) private fun mapConversationRenamedMessage( senderUserId: UserId, @@ -197,7 +197,7 @@ class SystemMessageContentMapper @Inject constructor( user = sender, type = SelfNameType.ResourceTitleCase ) - return UIMessageContent.SystemMessage.RenamedConversation(authorName, content) + return UIMessageContent.SystemMessage.RenamedConversation(authorName, content.conversationName) } fun mapMemberChangeMessage( @@ -271,7 +271,7 @@ class SystemMessageContentMapper @Inject constructor( UIMessageContent.SystemMessage.HistoryLost private fun mapMLSWrongEpochWarning(): UIMessageContent.SystemMessage = - UIMessageContent.SystemMessage.MLSWrongEpochWarning() + UIMessageContent.SystemMessage.MLSWrongEpochWarning private fun mapConversationHistoryListProtocolChanged(): UIMessageContent.SystemMessage = UIMessageContent.SystemMessage.HistoryLostProtocolChanged diff --git a/app/src/main/kotlin/com/wire/android/mapper/UICallParticipantMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/UICallParticipantMapper.kt index 4b84d17ca8a..3ce437b99df 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/UICallParticipantMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/UICallParticipantMapper.kt @@ -20,12 +20,10 @@ package com.wire.android.mapper import com.wire.android.model.ImageAsset import com.wire.android.ui.calling.model.UICallParticipant -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.call.Participant import javax.inject.Inject class UICallParticipantMapper @Inject constructor( - private val wireSessionImageLoader: WireSessionImageLoader, private val userTypeMapper: UserTypeMapper, ) { fun toUICallParticipant(participant: Participant) = UICallParticipant( @@ -36,7 +34,7 @@ class UICallParticipantMapper @Inject constructor( isSpeaking = participant.isSpeaking, isCameraOn = participant.isCameraOn, isSharingScreen = participant.isSharingScreen, - avatar = participant.avatarAssetId?.let { ImageAsset.UserAvatarAsset(wireSessionImageLoader, it) }, + avatar = participant.avatarAssetId?.let { ImageAsset.UserAvatarAsset(it) }, membership = userTypeMapper.toMembership(participant.userType), hasEstablishedAudio = participant.hasEstablishedAudio, accentId = participant.accentId diff --git a/app/src/main/kotlin/com/wire/android/mapper/UIParticipantMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/UIParticipantMapper.kt index 87541dd46ab..3610b892df8 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/UIParticipantMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/UIParticipantMapper.kt @@ -21,7 +21,6 @@ package com.wire.android.mapper import com.wire.android.ui.home.conversations.avatar import com.wire.android.ui.home.conversations.details.participants.model.UIParticipant import com.wire.android.ui.home.conversations.previewAsset -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.message.UserSummary import com.wire.kalium.logic.data.message.reaction.MessageReaction import com.wire.kalium.logic.data.message.receipt.DetailedReceipt @@ -33,7 +32,6 @@ import javax.inject.Inject class UIParticipantMapper @Inject constructor( private val userTypeMapper: UserTypeMapper, - private val wireSessionImageLoader: WireSessionImageLoader ) { fun toUIParticipant(user: User, isMLSVerified: Boolean = false): UIParticipant = with(user) { val (userType, connectionState, unavailable) = when (this) { @@ -45,7 +43,7 @@ class UIParticipantMapper @Inject constructor( id = id, name = name.orEmpty(), handle = handle.orEmpty(), - avatarData = avatar(wireSessionImageLoader, connectionState), + avatarData = avatar(connectionState), isSelf = user is SelfUser, isService = userType == UserType.SERVICE, membership = userTypeMapper.toMembership(userType), @@ -67,7 +65,7 @@ class UIParticipantMapper @Inject constructor( id = userSummary.userId, name = userSummary.userName.orEmpty(), handle = userSummary.userHandle.orEmpty(), - avatarData = userSummary.previewAsset(wireSessionImageLoader), + avatarData = userSummary.previewAsset(), membership = userTypeMapper.toMembership(userSummary.userType), unavailable = !userSummary.isUserDeleted && userSummary.userName.orEmpty().isEmpty(), isDeleted = userSummary.isUserDeleted, @@ -83,7 +81,7 @@ class UIParticipantMapper @Inject constructor( id = userSummary.userId, name = userSummary.userName.orEmpty(), handle = userSummary.userHandle.orEmpty(), - avatarData = userSummary.previewAsset(wireSessionImageLoader), + avatarData = userSummary.previewAsset(), membership = userTypeMapper.toMembership(userSummary.userType), unavailable = !userSummary.isUserDeleted && userSummary.userName.orEmpty().isEmpty(), isDeleted = userSummary.isUserDeleted, @@ -100,7 +98,7 @@ class UIParticipantMapper @Inject constructor( id = userSummary.userId, name = userSummary.userName.orEmpty(), handle = userSummary.userHandle.orEmpty(), - avatarData = previewAsset(wireSessionImageLoader), + avatarData = previewAsset(), membership = userTypeMapper.toMembership(userSummary.userType), unavailable = !userSummary.isUserDeleted && userSummary.userName.orEmpty().isEmpty(), isDeleted = userSummary.isUserDeleted, diff --git a/app/src/main/kotlin/com/wire/android/model/ImageAsset.kt b/app/src/main/kotlin/com/wire/android/model/ImageAsset.kt index 358345a8865..b39c78d5095 100644 --- a/app/src/main/kotlin/com/wire/android/model/ImageAsset.kt +++ b/app/src/main/kotlin/com/wire/android/model/ImageAsset.kt @@ -18,18 +18,33 @@ package com.wire.android.model +import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.painterResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import com.wire.android.R +import com.wire.android.ui.LocalActivity import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.user.UserAssetId +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder import okio.Path +import okio.Path.Companion.toPath +import javax.inject.Inject @Stable +@Serializable sealed class ImageAsset { /** @@ -37,12 +52,13 @@ sealed class ImageAsset { * message, i.e. some preview images that the user selected from local device gallery. */ @Stable + @Serializable data class Local( - val dataPath: Path, + val dataPath: @Serializable(with = PathAsStringSerializer::class) Path, val idKey: String ) : ImageAsset() - sealed class Remote(private val imageLoader: WireSessionImageLoader) : ImageAsset() { + sealed class Remote : ImageAsset() { /** * Value that uniquely identifies this Asset, @@ -56,39 +72,53 @@ sealed class ImageAsset { withCrossfadeAnimation: Boolean = false ) = when { LocalInspectionMode.current -> painterResource(id = R.drawable.ic_welcome_1) - else -> imageLoader.paint(asset = this, fallbackData = fallbackData, withCrossfadeAnimation = withCrossfadeAnimation) + else -> { + hiltViewModel( + // limit the scope of the ViewModel to the current activity so that there's one image loader instance for the Activity + viewModelStoreOwner = checkNotNull(LocalActivity.current as? AppCompatActivity ?: LocalViewModelStoreOwner.current) { + "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner" + }, + key = "remote_asset_image_loader" + ).imageLoader.paint(asset = this, fallbackData = fallbackData, withCrossfadeAnimation = withCrossfadeAnimation) + } } } @Stable + @Serializable data class UserAvatarAsset( - private val imageLoader: WireSessionImageLoader, val userAssetId: UserAssetId - ) : Remote(imageLoader) { + ) : Remote() { override val uniqueKey: String get() = userAssetId.toString() } @Stable + @Serializable data class PrivateAsset( - private val imageLoader: WireSessionImageLoader, val conversationId: ConversationId, val messageId: String, val isSelfAsset: Boolean, val isEphemeral: Boolean = false - ) : Remote(imageLoader) { + ) : Remote() { override fun toString(): String = "$conversationId:$messageId:$isSelfAsset:$isEphemeral" override val uniqueKey: String get() = toString() } } -fun String.parseIntoPrivateImageAsset( - imageLoader: WireSessionImageLoader, - qualifiedIdMapper: QualifiedIdMapper, -): ImageAsset.PrivateAsset { +fun String.parseIntoPrivateImageAsset(qualifiedIdMapper: QualifiedIdMapper): ImageAsset.PrivateAsset { val (conversationIdString, messageId, isSelfAsset, isEphemeral) = split(":") val conversationIdParam = qualifiedIdMapper.fromStringToQualifiedID(conversationIdString) - return ImageAsset.PrivateAsset(imageLoader, conversationIdParam, messageId, isSelfAsset.toBoolean(), isEphemeral.toBoolean()) + return ImageAsset.PrivateAsset(conversationIdParam, messageId, isSelfAsset.toBoolean(), isEphemeral.toBoolean()) } + +object PathAsStringSerializer : KSerializer { + override val descriptor = PrimitiveSerialDescriptor("Path", PrimitiveKind.STRING) + override fun serialize(encoder: Encoder, value: Path) = encoder.encodeString(value.toString()) + override fun deserialize(decoder: Decoder): Path = decoder.decodeString().toPath(normalize = true) +} + +@HiltViewModel +class RemoteAssetImageViewModel @Inject constructor(val imageLoader: WireSessionImageLoader) : ViewModel() diff --git a/app/src/main/kotlin/com/wire/android/model/UserAvatarData.kt b/app/src/main/kotlin/com/wire/android/model/UserAvatarData.kt index 372e2ee474f..ed878820421 100644 --- a/app/src/main/kotlin/com/wire/android/model/UserAvatarData.kt +++ b/app/src/main/kotlin/com/wire/android/model/UserAvatarData.kt @@ -24,8 +24,10 @@ import com.wire.android.ui.home.conversationslist.model.Membership import com.wire.android.util.EMPTY import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.UserAvailabilityStatus +import kotlinx.serialization.Serializable @Stable +@Serializable data class UserAvatarData( val asset: ImageAsset.UserAvatarAsset? = null, val availabilityStatus: UserAvailabilityStatus = UserAvailabilityStatus.NONE, @@ -50,6 +52,7 @@ data class UserAvatarData( /** * Holder that can be used to generate an avatar based on the user's full name initials and accent color. */ +@Serializable data class NameBasedAvatar(val fullName: String?, val accentColor: Int) { val initials: String get() { diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/SharedCallingViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/calling/SharedCallingViewModel.kt index 975bfae950a..807f0600b43 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/SharedCallingViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/SharedCallingViewModel.kt @@ -31,7 +31,6 @@ import com.wire.android.media.CallRinger import com.wire.android.model.ImageAsset import com.wire.android.ui.calling.model.UICallParticipant import com.wire.android.util.dispatchers.DispatcherProvider -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.call.Call import com.wire.kalium.logic.data.call.ConversationTypeForCall import com.wire.kalium.logic.data.call.VideoState @@ -86,7 +85,6 @@ class SharedCallingViewModel @AssistedInject constructor( private val observeSpeaker: ObserveSpeakerUseCase, private val callRinger: CallRinger, private val uiCallParticipantMapper: UICallParticipantMapper, - private val wireSessionImageLoader: WireSessionImageLoader, private val userTypeMapper: UserTypeMapper, private val dispatchers: DispatcherProvider ) : ViewModel() { @@ -137,7 +135,7 @@ class SharedCallingViewModel @AssistedInject constructor( callState.copy( conversationName = getConversationName(details.otherUser.name), avatarAssetId = details.otherUser.completePicture?.let { assetId -> - ImageAsset.UserAvatarAsset(wireSessionImageLoader, assetId) + ImageAsset.UserAvatarAsset(assetId) }, conversationTypeForCall = ConversationTypeForCall.OneOnOne, membership = userTypeMapper.toMembership(details.otherUser.userType), diff --git a/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModel.kt index c3207bcf7fa..67fb38f8cdf 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModel.kt @@ -29,7 +29,6 @@ import com.wire.android.model.ImageAsset.UserAvatarAsset import com.wire.android.model.NameBasedAvatar import com.wire.android.model.UserAvatarData import com.wire.android.navigation.SavedStateViewModel -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.feature.client.NeedsToRegisterClientUseCase import com.wire.kalium.logic.feature.legalhold.LegalHoldStateForSelfUser import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldStateForSelfUserUseCase @@ -48,7 +47,6 @@ class HomeViewModel @Inject constructor( private val getSelf: GetSelfUserUseCase, private val needsToRegisterClient: NeedsToRegisterClientUseCase, private val observeLegalHoldStatusForSelfUser: ObserveLegalHoldStateForSelfUserUseCase, - private val wireSessionImageLoader: WireSessionImageLoader, private val shouldTriggerMigrationForUser: ShouldTriggerMigrationForUserUserCase ) : SavedStateViewModel(savedStateHandle) { @@ -99,10 +97,7 @@ class HomeViewModel @Inject constructor( homeState = homeState.copy( userAvatarData = UserAvatarData( asset = selfUser.previewPicture?.let { - UserAvatarAsset( - wireSessionImageLoader, - it - ) + UserAvatarAsset(it) }, availabilityStatus = selfUser.availabilityStatus, nameBasedAvatar = NameBasedAvatar(selfUser.name, selfUser.accentId) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationMemberExt.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationMemberExt.kt index f612d4ea10f..63c309e0006 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationMemberExt.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationMemberExt.kt @@ -22,7 +22,6 @@ import com.wire.android.model.ImageAsset.UserAvatarAsset import com.wire.android.model.NameBasedAvatar import com.wire.android.model.UserAvatarData import com.wire.android.ui.home.conversationslist.model.Membership -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.conversation.MemberDetails import com.wire.kalium.logic.data.message.UserSummary import com.wire.kalium.logic.data.user.ConnectionState @@ -51,24 +50,22 @@ val MemberDetails.availabilityStatus: UserAvailabilityStatus is SelfUser -> (user as SelfUser).availabilityStatus } -fun User.previewAsset(wireSessionImageLoader: WireSessionImageLoader): UserAvatarAsset? = when (this) { +fun User.previewAsset(): UserAvatarAsset? = when (this) { is OtherUser -> previewPicture is SelfUser -> previewPicture -}?.let { UserAvatarAsset(wireSessionImageLoader, it) } +}?.let { UserAvatarAsset(it) } -fun User.avatar(wireSessionImageLoader: WireSessionImageLoader, connectionState: ConnectionState?): UserAvatarData = +fun User.avatar(connectionState: ConnectionState?): UserAvatarData = UserAvatarData( - asset = this.previewAsset(wireSessionImageLoader), + asset = this.previewAsset(), availabilityStatus = availabilityStatus, connectionState = connectionState, membership = if (userType == UserType.SERVICE) Membership.Service else Membership.None, nameBasedAvatar = NameBasedAvatar(fullName = name, accentColor = accentId) ) -fun UserSummary.previewAsset( - wireSessionImageLoader: WireSessionImageLoader -) = UserAvatarData( - asset = this.userPreviewAssetId?.let { UserAvatarAsset(wireSessionImageLoader, it) }, +fun UserSummary.previewAsset() = UserAvatarData( + asset = this.userPreviewAssetId?.let { UserAvatarAsset(it) }, availabilityStatus = this.availabilityStatus, connectionState = this.connectionStatus, nameBasedAvatar = NameBasedAvatar(fullName = userName, accentColor = accentId) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt index 5e36411a498..50fe76c34f7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt @@ -31,7 +31,6 @@ import com.wire.android.navigation.SavedStateViewModel import com.wire.android.ui.home.conversations.ConversationNavArgs import com.wire.android.ui.navArgs 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.StorageFailure import com.wire.kalium.logic.data.conversation.ConversationDetails @@ -52,7 +51,6 @@ class ConversationInfoViewModel @Inject constructor( override val savedStateHandle: SavedStateHandle, private val observeConversationDetails: ObserveConversationDetailsUseCase, private val fetchConversationMLSVerificationStatus: FetchConversationMLSVerificationStatusUseCase, - private val wireSessionImageLoader: WireSessionImageLoader, @CurrentAccount private val selfUserId: UserId, ) : SavedStateViewModel(savedStateHandle) { @@ -154,7 +152,7 @@ class ConversationInfoViewModel @Inject constructor( is ConversationDetails.OneOne -> ConversationAvatar.OneOne( conversationDetails.otherUser.previewPicture?.let { - ImageAsset.UserAvatarAsset(wireSessionImageLoader, it) + ImageAsset.UserAvatarAsset(it) }, conversationDetails.otherUser.availabilityStatus ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/SystemMessageItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/SystemMessageItem.kt index 486e07f5dc2..5eebbcc2826 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/SystemMessageItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/SystemMessageItem.kt @@ -236,7 +236,7 @@ fun SystemMessage.annotatedString( 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.RenamedConversation -> arrayOf(author.asString(res).markdownBold(), conversationName.markdownBold()) is SystemMessage.CryptoSessionReset -> arrayOf(author.asString(res).markdownBold()) is SystemMessage.NewConversationReceiptMode -> arrayOf(receiptMode.asString(res).markdownBold()) is SystemMessage.ConversationReceiptModeChanged -> arrayOf( @@ -244,7 +244,7 @@ fun SystemMessage.annotatedString( receiptMode.asString(res).markdownBold() ) - is SystemMessage.TeamMemberRemoved_Legacy -> arrayOf(content.userName) + is SystemMessage.TeamMemberRemoved_Legacy -> arrayOf(userName) is SystemMessage.Knock -> arrayOf(author.asString(res).markdownBold()) is SystemMessage.HistoryLost -> arrayOf() is SystemMessage.MLSWrongEpochWarning -> arrayOf() @@ -275,15 +275,15 @@ fun SystemMessage.annotatedString( arrayOf(memberNames.limitUserNamesList(res, true).toUserNamesListMarkdownString(res)) } ?: arrayOf() } - val markdownString = when (stringResId) { - is LocalizedStringResource.PluralResource -> res.getQuantityString( - (stringResId as LocalizedStringResource.PluralResource).id, - (stringResId as LocalizedStringResource.PluralResource).quantity, + val markdownString = when (stringRes) { + is LocalizedStringResource.Plural -> res.getQuantityString( + (stringRes as LocalizedStringResource.Plural).id, + (stringRes as LocalizedStringResource.Plural).quantity, *markdownArgs ) - is LocalizedStringResource.StringResource -> res.getString( - (stringResId as LocalizedStringResource.StringResource).id, + is LocalizedStringResource.String -> res.getString( + (stringRes as LocalizedStringResource.String).id, *markdownArgs ) } @@ -322,18 +322,7 @@ private fun SystemMessage.MemberFailedToAdd.toFailedToAddMarkdownText( if (isMultipleUsersFailure) failedToAddAnnotatedText.append("\n\n") failedToAddAnnotatedText.append( markdownText( - when (stringResId) { - is LocalizedStringResource.PluralResource -> res.getQuantityString( - stringResId.id, - stringResId.quantity, - stringResId.formatArgs - ) - - is LocalizedStringResource.StringResource -> res.getString( - stringResId.id, - memberNames.limitUserNamesList(res, true).toUserNamesListMarkdownString(res) - ) - }, + res.getString(stringRes.id, memberNames.limitUserNamesList(res, true).toUserNamesListMarkdownString(res)), normalStyle, boldStyle, normalColor, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/SystemMessageItemLeading.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/SystemMessageItemLeading.kt index 3d89e323a07..90d90139d63 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/SystemMessageItemLeading.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/SystemMessageItemLeading.kt @@ -30,9 +30,9 @@ import com.wire.android.ui.home.conversations.model.UIMessageContent.SystemMessa @Composable fun SystemMessageItemLeading(messageContent: SystemMessage, modifier: Modifier = Modifier) { - if (messageContent.iconResId != null) { + messageContent.iconResId?.let { iconResId -> Image( - painter = painterResource(id = messageContent.iconResId), + painter = painterResource(id = iconResId), contentDescription = null, colorFilter = getColorFilter(messageContent), modifier = modifier.size( 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 11ac2b1979a..e07fae0f5f1 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 @@ -18,14 +18,6 @@ package com.wire.android.ui.home.conversations.mock -import coil.ComponentRegistry -import coil.ImageLoader -import coil.disk.DiskCache -import coil.memory.MemoryCache -import coil.request.DefaultRequestOptions -import coil.request.Disposable -import coil.request.ImageRequest -import coil.request.ImageResult import com.wire.android.model.ImageAsset import com.wire.android.model.ImageAsset.UserAvatarAsset import com.wire.android.model.UserAvatarData @@ -43,17 +35,12 @@ import com.wire.android.ui.home.conversations.model.UIMessageContent import com.wire.android.ui.home.conversations.model.messagetypes.asset.UIAssetMessage 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.id.QualifiedID import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.UserAssetId import com.wire.kalium.logic.data.user.UserAvailabilityStatus -import com.wire.kalium.network.NetworkState -import com.wire.kalium.network.NetworkStateObserver -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.datetime.Clock import kotlinx.datetime.Instant import okio.Path.Companion.toPath @@ -282,25 +269,10 @@ val mockUsersUITexts = listOf( "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") - override val diskCache: DiskCache get() = TODO("Not yet implemented") - override val memoryCache: MemoryCache get() = TODO("Not yet implemented") - override fun enqueue(request: ImageRequest): Disposable = TODO("Not yet implemented") - override suspend fun execute(request: ImageRequest): ImageResult = TODO("Not yet implemented") - override fun newBuilder(): ImageLoader.Builder = TODO("Not yet implemented") - override fun shutdown() = TODO("Not yet implemented") -}, - object : NetworkStateObserver { - override fun observeNetworkState(): StateFlow = MutableStateFlow(NetworkState.ConnectedWithInternet) - } -) - fun mockAssetMessage(assetId: String = "asset1", messageId: String = "msg1") = UIMessage.Regular( conversationId = ConversationId("value", "domain"), userAvatarData = UserAvatarData( - UserAvatarAsset(mockImageLoader, UserAssetId("a", "domain")), + UserAvatarAsset(UserAssetId("a", "domain")), UserAvailabilityStatus.AVAILABLE ), header = MessageHeader( @@ -329,7 +301,7 @@ fun mockAssetMessage(assetId: String = "asset1", messageId: String = "msg1") = U fun mockAssetAudioMessage(assetId: String = "asset1", messageId: String = "msg1") = UIMessage.Regular( conversationId = ConversationId("value", "domain"), userAvatarData = UserAvatarData( - UserAvatarAsset(mockImageLoader, UserAssetId("a", "domain")), + UserAvatarAsset(UserAssetId("a", "domain")), UserAvailabilityStatus.AVAILABLE ), header = MessageHeader( @@ -375,7 +347,6 @@ fun mockedImg() = UIMessageContent.ImageMessage( ) fun mockedPrivateAsset() = ImageAsset.PrivateAsset( - imageLoader = mockImageLoader, conversationId = ConversationId("id", "domain"), messageId = "messageId", isSelfAsset = true 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 c3f70e439d8..76a124fc994 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 @@ -19,7 +19,6 @@ package com.wire.android.ui.home.conversations.model import android.content.res.Resources -import androidx.annotation.DrawableRes import androidx.annotation.PluralsRes import androidx.annotation.StringRes import androidx.compose.runtime.Stable @@ -41,7 +40,6 @@ import com.wire.kalium.logic.data.conversation.ClientId import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.message.Message -import com.wire.kalium.logic.data.message.MessageContent import com.wire.kalium.logic.data.user.AssetId import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.UserId @@ -53,8 +51,10 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.toImmutableList import kotlinx.datetime.Instant +import kotlinx.serialization.Serializable import kotlin.time.Duration +@Serializable sealed interface UIMessage { val conversationId: ConversationId val header: MessageHeader @@ -64,6 +64,7 @@ sealed interface UIMessage { val decryptionFailed: Boolean val isPending: Boolean + @Serializable data class Regular( override val conversationId: ConversationId, override val header: MessageHeader, @@ -110,6 +111,7 @@ sealed interface UIMessage { val isLocation: Boolean = messageContent is UIMessageContent.Location } + @Serializable data class System( override val conversationId: ConversationId, override val header: MessageHeader, @@ -124,6 +126,7 @@ sealed interface UIMessage { } @Stable +@Serializable data class MessageHeader( val username: UIText, val membership: Membership, @@ -141,68 +144,84 @@ data class MessageHeader( ) @Stable +@Serializable data class MessageFooter( val messageId: String, val reactions: Map = emptyMap(), val ownReactions: Set = emptySet() ) -sealed class ExpirationStatus { +@Serializable +sealed interface ExpirationStatus { + + @Serializable data class Expirable( val expireAfter: Duration, val selfDeletionStatus: Message.ExpirationData.SelfDeletionStatus - ) : ExpirationStatus() + ) : ExpirationStatus - object NotExpirable : ExpirationStatus() + @Serializable + data object NotExpirable : ExpirationStatus } -sealed class MessageEditStatus { - object NonEdited : MessageEditStatus() - data class Edited(val formattedEditTimeStamp: String) : MessageEditStatus() +@Serializable +sealed interface MessageEditStatus { + + @Serializable + data object NonEdited : MessageEditStatus + + @Serializable + data class Edited(val formattedEditTimeStamp: String) : MessageEditStatus } -sealed class MessageFlowStatus { - - data object Sending : MessageFlowStatus() - data object Sent : MessageFlowStatus() - sealed class Failure(val errorText: UIText) : MessageFlowStatus() { - sealed class Send(errorText: UIText) : Failure(errorText) { - data class Locally(val isEdited: Boolean) : Send( - if (isEdited) { - UIText.StringResource(R.string.label_message_edit_sent_failure) - } else { - UIText.StringResource(R.string.label_message_sent_failure) - } - ) +@Serializable +sealed interface MessageFlowStatus { - data class Remotely(val isEdited: Boolean, val backendWithFailure: String) : Send( - if (isEdited) { - UIText.StringResource( - R.string.label_message_edit_sent_remotely_failure, - backendWithFailure - ) - } else { - UIText.StringResource( - R.string.label_message_sent_remotely_failure, - backendWithFailure - ) - } - ) + @Serializable + data object Sending : MessageFlowStatus + + @Serializable + data object Sent : MessageFlowStatus + + @Serializable + sealed interface Failure : MessageFlowStatus { + val errorText: UIText + + @Serializable + sealed interface Send : Failure { + + @Serializable + data class Locally(val isEdited: Boolean) : Send { + override val errorText: UIText = when { + isEdited -> UIText.StringResource(R.string.label_message_edit_sent_failure) + else -> UIText.StringResource(R.string.label_message_sent_failure) + } + } + + @Serializable + data class Remotely(val isEdited: Boolean, val backendWithFailure: String) : Send { + override val errorText: UIText = when { + isEdited -> UIText.StringResource(R.string.label_message_edit_sent_remotely_failure, backendWithFailure) + else -> UIText.StringResource(R.string.label_message_sent_remotely_failure, backendWithFailure) + } + } } - data class Decryption(val isDecryptionResolved: Boolean, private val errorCode: Int?) : Failure( - errorCode?.let { + @Serializable + data class Decryption(val isDecryptionResolved: Boolean, private val errorCode: Int?) : Failure { + override val errorText: UIText = errorCode?.let { UIText.StringResource(R.string.label_message_decryption_failure_message_with_error_code, it) } ?: UIText.StringResource(R.string.label_message_decryption_failure_message) - ) + } } - data object Delivered : MessageFlowStatus() + data object Delivered : MessageFlowStatus - data class Read(val count: Long) : MessageFlowStatus() + data class Read(val count: Long) : MessageFlowStatus } @Stable +@Serializable data class MessageStatus( val flowStatus: MessageFlowStatus, val expirationStatus: ExpirationStatus, @@ -223,42 +242,54 @@ data class MessageStatus( } @Stable -sealed class UILastMessageContent { - object None : UILastMessageContent() +@Serializable +sealed interface UILastMessageContent { + + @Serializable + data object None : UILastMessageContent - data class TextMessage(val messageBody: MessageBody) : UILastMessageContent() + @Serializable + data class TextMessage(val messageBody: MessageBody) : UILastMessageContent + @Serializable data class SenderWithMessage( val sender: UIText, val message: UIText, val separator: String = MarkdownConstants.NON_BREAKING_SPACE - ) : UILastMessageContent() + ) : UILastMessageContent + @Serializable data class MultipleMessage( val messages: List, val separator: String = MarkdownConstants.NON_BREAKING_SPACE - ) : UILastMessageContent() + ) : UILastMessageContent - data class Connection(val connectionState: ConnectionState, val userId: UserId) : UILastMessageContent() + @Serializable + data class Connection(val connectionState: ConnectionState, val userId: UserId) : UILastMessageContent - data class VerificationChanged(@StringRes val textResId: Int) : UILastMessageContent() + @Serializable + data class VerificationChanged(@StringRes val textResId: Int) : UILastMessageContent } -sealed class UIMessageContent { +@Serializable +sealed interface UIMessageContent { - sealed class Regular : UIMessageContent() + @Serializable + sealed class Regular : UIMessageContent /** * IncompleteAssetMessage is a displayable asset that's missing the remote data. * Sometimes client receives two events about the same asset, first one with only part of the data ("preview" type from web), * so such asset shouldn't be shown until all the required data is received. */ - object IncompleteAssetMessage : UIMessageContent() + @Serializable + data object IncompleteAssetMessage : UIMessageContent interface PartialDeliverable { val deliveryStatus: DeliveryStatusContent } + @Serializable data class TextMessage( val messageBody: MessageBody, override val deliveryStatus: DeliveryStatusContent = DeliveryStatusContent.CompleteDelivery @@ -266,6 +297,7 @@ sealed class UIMessageContent { override fun textToCopy(resources: Resources): String = messageBody.message.asString(resources) } + @Serializable data class Composite( val messageBody: MessageBody?, val buttonList: PersistentList @@ -273,8 +305,10 @@ sealed class UIMessageContent { override fun textToCopy(resources: Resources): String? = messageBody?.message?.asString(resources) } - object Deleted : Regular() + @Serializable + data object Deleted : Regular() + @Serializable data class RestrictedAsset( val mimeType: String, val assetSizeInBytes: Long, @@ -283,6 +317,7 @@ sealed class UIMessageContent { ) : Regular(), PartialDeliverable @Stable + @Serializable data class AssetMessage( val assetName: String, val assetExtension: String, @@ -291,6 +326,7 @@ sealed class UIMessageContent { override val deliveryStatus: DeliveryStatusContent = DeliveryStatusContent.CompleteDelivery ) : Regular(), PartialDeliverable + @Serializable data class ImageMessage( val assetId: AssetId, val asset: ImageAsset.PrivateAsset?, @@ -300,6 +336,7 @@ sealed class UIMessageContent { ) : Regular(), PartialDeliverable @Stable + @Serializable data class AudioAssetMessage( val assetName: String, val assetExtension: String, @@ -309,6 +346,7 @@ sealed class UIMessageContent { ) : Regular(), PartialDeliverable @Stable + @Serializable data class Location( val latitude: Float, val longitude: Float, @@ -318,308 +356,363 @@ sealed class UIMessageContent { override val deliveryStatus: DeliveryStatusContent = DeliveryStatusContent.CompleteDelivery ) : Regular(), PartialDeliverable - sealed class SystemMessage( - @DrawableRes val iconResId: Int?, - open val stringResId: LocalizedStringResource, - @StringRes val learnMoreResId: Int? = null, - val isSmallIcon: Boolean = true, - ) : UIMessageContent() { - - constructor( - @DrawableRes iconResId: Int?, - @StringRes stringResId: Int, - isSmallIcon: Boolean = true, - @StringRes learnMoreResId: Int? = null - ) : this(iconResId, LocalizedStringResource.StringResource(stringResId), learnMoreResId, isSmallIcon) - - constructor( - @DrawableRes iconResId: Int?, - @PluralsRes stringResId: Int, - quantity: Int, - formatArgs: List, - isSmallIcon: Boolean = true, - @StringRes learnMoreResId: Int? = null - ) : this( - iconResId, - LocalizedStringResource.PluralResource(stringResId, quantity, formatArgs.toTypedArray()), - learnMoreResId, - isSmallIcon - ) + @Serializable + sealed interface SystemMessage : UIMessageContent { + val iconResId: Int? + val stringRes: LocalizedStringResource + val learnMoreResId: Int? get() = null + val isSmallIcon: Boolean get() = true - data class Knock(val author: UIText, val isSelfTriggered: Boolean) : SystemMessage( - R.drawable.ic_ping, - if (isSelfTriggered) R.string.label_system_message_self_user_knock else R.string.label_system_message_other_user_knock - ) + @Serializable + data class Knock( + val author: UIText, + val isSelfTriggered: Boolean + ) : SystemMessage { + override val iconResId = R.drawable.ic_ping + override val stringRes = when { + isSelfTriggered -> R.string.label_system_message_self_user_knock + else -> R.string.label_system_message_other_user_knock + }.toLocalizedStringResource() + } + @Serializable data class MemberAdded( val author: UIText, val memberNames: List, val isSelfTriggered: Boolean = false - ) : SystemMessage( - R.drawable.ic_add, - if (isSelfTriggered) R.string.label_system_message_added_by_self else R.string.label_system_message_added_by_other - ) + ) : SystemMessage { + override val iconResId = R.drawable.ic_add + override val stringRes = when { + isSelfTriggered -> R.string.label_system_message_added_by_self + else -> R.string.label_system_message_added_by_other + }.toLocalizedStringResource() + } + @Serializable data class MemberJoined( val author: UIText, val isSelfTriggered: Boolean = false - ) : SystemMessage( - R.drawable.ic_add, - if (isSelfTriggered) { - R.string.label_system_message_joined_the_conversation_by_self - } else { - R.string.label_system_message_joined_the_conversation_by_other - } - ) + ) : SystemMessage { + override val iconResId = R.drawable.ic_add + override val stringRes = when { + isSelfTriggered -> R.string.label_system_message_joined_the_conversation_by_self + else -> R.string.label_system_message_joined_the_conversation_by_other + }.toLocalizedStringResource() + } + @Serializable data class MemberRemoved( val author: UIText, val memberNames: List, val isSelfTriggered: Boolean = false - ) : SystemMessage( - R.drawable.ic_minus, - if (isSelfTriggered) R.string.label_system_message_removed_by_self else R.string.label_system_message_removed_by_other - ) + ) : SystemMessage { + override val iconResId = R.drawable.ic_minus + override val stringRes = when { + isSelfTriggered -> R.string.label_system_message_removed_by_self + else -> R.string.label_system_message_removed_by_other + }.toLocalizedStringResource() + } + @Serializable data class TeamMemberRemoved( val author: UIText, val memberNames: List, - ) : SystemMessage( - R.drawable.ic_minus, - R.plurals.label_system_message_team_member_left, - quantity = memberNames.size, - formatArgs = memberNames - ) + ) : SystemMessage { + override val iconResId = R.drawable.ic_minus + override val stringRes = R.plurals.label_system_message_team_member_left.toLocalizedPluralResource(memberNames.size) + } + @Serializable data class MemberLeft( val author: UIText, val isSelfTriggered: Boolean = false - ) : SystemMessage( - R.drawable.ic_minus, - if (isSelfTriggered) { - R.string.label_system_message_left_the_conversation_by_self - } else { - R.string.label_system_message_left_the_conversation_by_other - } - ) + ) : SystemMessage { + override val iconResId = R.drawable.ic_minus + override val stringRes = when { + isSelfTriggered -> R.string.label_system_message_left_the_conversation_by_self + else -> R.string.label_system_message_left_the_conversation_by_other + }.toLocalizedStringResource() + } + @Serializable data class FederationMemberRemoved( val memberNames: List - ) : SystemMessage( - R.drawable.ic_minus, - if (memberNames.size > 1) { - R.string.label_system_message_federation_many_member_removed - } else { - R.string.label_system_message_federation_one_member_removed - } - ) + ) : SystemMessage { + override val iconResId = R.drawable.ic_minus + override val stringRes = when { + memberNames.size > 1 -> R.string.label_system_message_federation_many_member_removed + else -> R.string.label_system_message_federation_one_member_removed + }.toLocalizedStringResource() + } + @Serializable data class FederationStopped( val domainList: List - ) : SystemMessage( - R.drawable.ic_info, - if (domainList.size > 1) { - R.string.label_system_message_federation_conection_removed - } else { - R.string.label_system_message_federation_removed - }, - learnMoreResId = R.string.url_federation_support - ) + ) : SystemMessage { + override val iconResId = R.drawable.ic_info + override val stringRes = when { + domainList.size > 1 -> R.string.label_system_message_federation_conection_removed + else -> R.string.label_system_message_federation_removed + }.toLocalizedStringResource() + override val learnMoreResId = R.string.url_federation_support + } + + @Serializable + sealed interface MissedCall : SystemMessage { + val author: UIText + override val iconResId get() = R.drawable.ic_call_end + override val isSmallIcon get() = false - sealed class MissedCall( - open val author: UIText, - @StringRes stringResId: Int, - ) : SystemMessage(R.drawable.ic_call_end, stringResId, isSmallIcon = false) { + @Serializable + data class YouCalled(override val author: UIText) : MissedCall { + override val stringRes = R.string.label_system_message_you_called.toLocalizedStringResource() + } - 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) + @Serializable + data class OtherCalled(override val author: UIText) : MissedCall { + override val stringRes = R.string.label_system_message_other_called.toLocalizedStringResource() + } } - data class RenamedConversation(val author: UIText, val content: MessageContent.ConversationRenamed) : - SystemMessage(R.drawable.ic_edit, R.string.label_system_message_renamed_the_conversation) + @Serializable + data class RenamedConversation( + val author: UIText, + val conversationName: String + ) : SystemMessage { + override val iconResId = R.drawable.ic_edit + override val stringRes = R.string.label_system_message_renamed_the_conversation.toLocalizedStringResource() + } @Deprecated("Use TeamMemberRemoved") @Suppress("ClassNaming") - data class TeamMemberRemoved_Legacy(val content: MessageContent.TeamMemberRemoved) : - SystemMessage( - R.drawable.ic_minus, - R.plurals.label_system_message_team_member_left, - quantity = 0, - formatArgs = emptyList(), - true - ) + @Serializable + data class TeamMemberRemoved_Legacy( + val userName: String + ) : SystemMessage { + override val iconResId = R.drawable.ic_minus + override val stringRes = R.plurals.label_system_message_team_member_left.toLocalizedPluralResource(0) + } - data class CryptoSessionReset(val author: UIText) : - SystemMessage(R.drawable.ic_info, R.string.label_system_message_session_reset) + @Serializable + data class CryptoSessionReset( + val author: UIText + ) : SystemMessage { + override val iconResId = R.drawable.ic_info + override val stringRes = R.string.label_system_message_session_reset.toLocalizedStringResource() + } + @Serializable data class NewConversationReceiptMode( val receiptMode: UIText - ) : SystemMessage(R.drawable.ic_view, R.string.label_system_message_new_conversation_receipt_mode) + ) : SystemMessage { + override val iconResId = R.drawable.ic_view + override val stringRes = R.string.label_system_message_new_conversation_receipt_mode.toLocalizedStringResource() + } + @Serializable data class ConversationReceiptModeChanged( val author: UIText, val receiptMode: UIText, val isAuthorSelfUser: Boolean = false - ) : SystemMessage( - R.drawable.ic_view, - if (isAuthorSelfUser) { - R.string.label_system_message_read_receipt_changed_by_self - } else { - R.string.label_system_message_read_receipt_changed_by_other - } - ) + ) : SystemMessage { + override val iconResId = R.drawable.ic_view + override val stringRes = when { + isAuthorSelfUser -> R.string.label_system_message_read_receipt_changed_by_self + else -> R.string.label_system_message_read_receipt_changed_by_other + }.toLocalizedStringResource() + } + @Serializable data class ConversationMessageTimerActivated( val author: UIText, val isAuthorSelfUser: Boolean = false, val selfDeletionDuration: SelfDeletionDuration - ) : SystemMessage( - R.drawable.ic_timer, - if (isAuthorSelfUser) { - R.string.label_system_message_conversation_message_timer_activated_by_self - } else { - R.string.label_system_message_conversation_message_timer_activated_by_other - } - ) + ) : SystemMessage { + override val iconResId = R.drawable.ic_timer + override val stringRes = when { + isAuthorSelfUser -> R.string.label_system_message_conversation_message_timer_activated_by_self + else -> R.string.label_system_message_conversation_message_timer_activated_by_other + }.toLocalizedStringResource() + } + @Serializable data class ConversationMessageTimerDeactivated( val author: UIText, val isAuthorSelfUser: Boolean = false - ) : SystemMessage( - R.drawable.ic_timer, - if (isAuthorSelfUser) { - R.string.label_system_message_conversation_message_timer_deactivated_by_self - } else { - R.string.label_system_message_conversation_message_timer_deactivated_by_other - } - ) + ) : SystemMessage { + override val iconResId = R.drawable.ic_timer + override val stringRes = when { + isAuthorSelfUser -> R.string.label_system_message_conversation_message_timer_deactivated_by_self + else -> R.string.label_system_message_conversation_message_timer_deactivated_by_other + }.toLocalizedStringResource() + } - class MLSWrongEpochWarning : SystemMessage( - iconResId = R.drawable.ic_info, - stringResId = R.string.label_system_message_conversation_mls_wrong_epoch_error_handled, - learnMoreResId = R.string.url_system_message_learn_more_about_mls - ) + @Serializable + data object MLSWrongEpochWarning : SystemMessage { + override val iconResId = R.drawable.ic_info + override val stringRes = R.string.label_system_message_conversation_mls_wrong_epoch_error_handled.toLocalizedStringResource() + override val learnMoreResId = R.string.url_system_message_learn_more_about_mls + } + @Serializable data class ConversationProtocolChanged( val protocol: Conversation.Protocol - ) : SystemMessage( - iconResId = R.drawable.ic_info, - stringResId = when (protocol) { + ) : SystemMessage { + override val iconResId = R.drawable.ic_info + override val stringRes = when (protocol) { Conversation.Protocol.PROTEUS -> R.string.label_system_message_conversation_protocol_changed_proteus Conversation.Protocol.MIXED -> R.string.label_system_message_conversation_protocol_changed_mixed Conversation.Protocol.MLS -> R.string.label_system_message_conversation_protocol_changed_mls - }, - learnMoreResId = when (protocol) { + }.toLocalizedStringResource() + override val learnMoreResId = when (protocol) { Conversation.Protocol.PROTEUS -> null Conversation.Protocol.MIXED -> null Conversation.Protocol.MLS -> R.string.url_system_message_learn_more_about_mls } - ) + } - data object ConversationProtocolChangedWithCallOngoing : SystemMessage( - R.drawable.ic_info, - R.string.label_system_message_conversation_protocol_changed_during_a_call - ) + @Serializable + data object ConversationProtocolChangedWithCallOngoing : SystemMessage { + override val iconResId = R.drawable.ic_info + override val stringRes = R.string.label_system_message_conversation_protocol_changed_during_a_call.toLocalizedStringResource() + } - object HistoryLost : SystemMessage( - R.drawable.ic_info, - R.string.label_system_message_conversation_history_lost - ) + @Serializable + data object HistoryLost : SystemMessage { + override val iconResId = R.drawable.ic_info + override val stringRes = R.string.label_system_message_conversation_history_lost.toLocalizedStringResource() + } - object HistoryLostProtocolChanged : SystemMessage( - R.drawable.ic_info, - R.string.label_system_message_conversation_history_lost_protocol_changed - ) + @Serializable + data object HistoryLostProtocolChanged : SystemMessage { + override val iconResId = R.drawable.ic_info + override val stringRes = R.string.label_system_message_conversation_history_lost_protocol_changed.toLocalizedStringResource() + } + @Serializable data class ConversationMessageCreated( val author: UIText, val isAuthorSelfUser: Boolean = false, val date: String - ) : SystemMessage( - R.drawable.ic_conversation, - if (isAuthorSelfUser) { - R.string.label_system_message_conversation_started_by_self - } else { - R.string.label_system_message_conversation_started_by_other - } - ) + ) : SystemMessage { + override val iconResId = R.drawable.ic_conversation + override val stringRes = when { + isAuthorSelfUser -> R.string.label_system_message_conversation_started_by_self + else -> R.string.label_system_message_conversation_started_by_other + }.toLocalizedStringResource() + } + @Serializable data class ConversationStartedWithMembers( val memberNames: List - ) : SystemMessage( - R.drawable.ic_contact, - R.string.label_system_message_conversation_started_with_members - ) + ) : SystemMessage { + override val iconResId = R.drawable.ic_contact + override val stringRes = R.string.label_system_message_conversation_started_with_members.toLocalizedStringResource() + } + @Serializable data class MemberFailedToAdd( val memberNames: List, val type: Type, - ) : SystemMessage( - R.drawable.ic_info, - if (memberNames.size > 1) { - R.string.label_system_message_conversation_failed_add_many_members_details - } else { - R.string.label_system_message_conversation_failed_add_one_member_details - }, - learnMoreResId = when (type) { + ) : SystemMessage { + override val iconResId = R.drawable.ic_info + override val stringRes = when { + memberNames.size > 1 -> R.string.label_system_message_conversation_failed_add_many_members_details + else -> R.string.label_system_message_conversation_failed_add_one_member_details + }.toLocalizedStringResource() + override val learnMoreResId = when (type) { Type.Federation -> R.string.url_message_details_offline_backends_learn_more Type.LegalHold -> R.string.url_legal_hold_learn_more Type.Unknown -> null } - - ) { val usersCount = memberNames.size enum class Type { Federation, LegalHold, Unknown; } } - data class ConversationDegraded(val protocol: Conversation.Protocol) : SystemMessage( - iconResId = if (protocol == Conversation.Protocol.MLS) R.drawable.ic_conversation_degraded_mls - else R.drawable.ic_shield_holo, - stringResId = if (protocol == Conversation.Protocol.MLS) R.string.label_system_message_conversation_degraded_mls - else R.string.label_system_message_conversation_degraded_proteus - ) + @Serializable + data class ConversationDegraded( + val protocol: Conversation.Protocol + ) : SystemMessage { + override val iconResId = + if (protocol == Conversation.Protocol.MLS) R.drawable.ic_conversation_degraded_mls + else R.drawable.ic_shield_holo + override val stringRes = LocalizedStringResource.String( + if (protocol == Conversation.Protocol.MLS) R.string.label_system_message_conversation_degraded_mls + else R.string.label_system_message_conversation_degraded_proteus + ) + } - data class ConversationVerified(val protocol: Conversation.Protocol) : SystemMessage( - iconResId = if (protocol == Conversation.Protocol.MLS) R.drawable.ic_certificate_valid_mls - else R.drawable.ic_certificate_valid_proteus, - stringResId = if (protocol == Conversation.Protocol.MLS) R.string.label_system_message_conversation_verified_mls - else R.string.label_system_message_conversation_verified_proteus - ) + @Serializable + data class ConversationVerified( + val protocol: Conversation.Protocol + ) : SystemMessage { + override val iconResId = + if (protocol == Conversation.Protocol.MLS) R.drawable.ic_certificate_valid_mls + else R.drawable.ic_certificate_valid_proteus + override val stringRes = LocalizedStringResource.String( + if (protocol == Conversation.Protocol.MLS) R.string.label_system_message_conversation_verified_mls + else R.string.label_system_message_conversation_verified_proteus + ) + } - data object ConversationMessageCreatedUnverifiedWarning : SystemMessage( - R.drawable.ic_info, - R.string.label_system_message_conversation_started_sensitive_information - ) + @Serializable + data object ConversationMessageCreatedUnverifiedWarning : SystemMessage { + override val iconResId = R.drawable.ic_info + override val stringRes = LocalizedStringResource.String( + R.string.label_system_message_conversation_started_sensitive_information + ) + } + + @Serializable + sealed interface LegalHold : SystemMessage { + val memberNames: List? get() = null + override val iconResId get() = R.drawable.ic_legal_hold - sealed class LegalHold( - stringResId: LocalizedStringResource.StringResource, - @StringRes learnMoreResId: Int? = null, - open val memberNames: List? = null, - ) : SystemMessage(R.drawable.ic_legal_hold, stringResId, learnMoreResId) { + @Serializable + sealed interface Enabled : LegalHold { + override val learnMoreResId get() = R.string.url_legal_hold_learn_more - sealed class Enabled(override val stringResId: LocalizedStringResource.StringResource) : - LegalHold(stringResId, R.string.url_legal_hold_learn_more) { + @Serializable + data object Self : Enabled { + override val stringRes = LocalizedStringResource.String(R.string.legal_hold_system_message_enabled_self) + } - constructor(@StringRes stringResId: Int) : this(LocalizedStringResource.StringResource(stringResId)) + @Serializable + data class Others(override val memberNames: List) : Enabled { + override val stringRes = LocalizedStringResource.String(R.string.legal_hold_system_message_enabled_others) + } - 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) + @Serializable + data object Conversation : Enabled { + override val stringRes = LocalizedStringResource.String(R.string.legal_hold_system_message_enabled_conversation) + } } - sealed class Disabled(override val stringResId: LocalizedStringResource.StringResource) : LegalHold(stringResId, null) { + @Serializable + sealed interface Disabled : LegalHold { - constructor(@StringRes stringResId: Int) : this(LocalizedStringResource.StringResource(stringResId)) + @Serializable + data object Self : Disabled { + override val stringRes = LocalizedStringResource.String(R.string.legal_hold_system_message_disabled_self) + } - 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) + @Serializable + data class Others(override val memberNames: List) : Disabled { + override val stringRes = LocalizedStringResource.String(R.string.legal_hold_system_message_disabled_others) + } + + @Serializable + data object Conversation : Disabled { + override val stringRes = + LocalizedStringResource.String(R.string.legal_hold_system_message_disabled_conversation) + } } } } } +@Serializable data class MessageBody( val message: UIText, val quotedMessage: UIQuotedMessage? = null @@ -629,6 +722,7 @@ enum class MessageSource { Self, OtherUser } +@Serializable data class MessageTime(val instant: Instant) { val utcISO: String = instant.toIsoDateTimeString() val formattedDate: String = utcISO.uiMessageDateTime() ?: "" @@ -638,7 +732,10 @@ data class MessageTime(val instant: Instant) { } @Stable +@Serializable sealed interface DeliveryStatusContent { + + @Serializable class PartialDelivery( val failedRecipients: ImmutableList = persistentListOf(), val noClients: ImmutableMap> = persistentMapOf(), @@ -651,14 +748,19 @@ sealed interface DeliveryStatusContent { val totalUsersWithFailures by lazy { (failedRecipients.size + noClients.values.distinct().sumOf { it.size }) } } + @Serializable data object CompleteDelivery : DeliveryStatusContent } @Stable +@Serializable data class MessageButton( val id: String, val text: String, val isSelected: Boolean, ) +private fun @receiver:StringRes Int.toLocalizedStringResource() = LocalizedStringResource.String(this) +private fun @receiver:PluralsRes Int.toLocalizedPluralResource(quantity: Int) = LocalizedStringResource.Plural(this, quantity) + const val DEFAULT_LOCATION_ZOOM = 20 diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIQuotedMessage.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIQuotedMessage.kt index a8225875d6b..04b90d310ab 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIQuotedMessage.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIQuotedMessage.kt @@ -21,11 +21,15 @@ import com.wire.android.appLogger import com.wire.android.model.ImageAsset import com.wire.android.util.ui.UIText import com.wire.kalium.logic.data.user.UserId +import kotlinx.serialization.Serializable +@Serializable sealed class UIQuotedMessage { - object UnavailableData : UIQuotedMessage() + @Serializable + data object UnavailableData : UIQuotedMessage() + @Serializable data class UIQuotedData( val messageId: String, val senderId: UserId, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCase.kt index e1dba33656d..8476a67e2dd 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCase.kt @@ -25,7 +25,6 @@ import com.wire.android.mapper.UserTypeMapper import com.wire.android.mapper.toConversationItem import com.wire.android.ui.home.conversationslist.model.ConversationItem import com.wire.android.util.dispatchers.DispatcherProvider -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.conversation.ConversationQueryConfig import com.wire.kalium.logic.feature.conversation.GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase import kotlinx.coroutines.flow.Flow @@ -35,7 +34,6 @@ import javax.inject.Inject class GetConversationsFromSearchUseCase @Inject constructor( private val useCase: GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase, - private val wireSessionImageLoader: WireSessionImageLoader, private val userTypeMapper: UserTypeMapper, private val dispatchers: DispatcherProvider, ) { @@ -62,7 +60,7 @@ class GetConversationsFromSearchUseCase @Inject constructor( startingOffset = 0L, ).map { pagingData -> pagingData.map { - it.toConversationItem(wireSessionImageLoader, userTypeMapper, searchQuery) + it.toConversationItem(userTypeMapper, searchQuery) } }.flowOn(dispatchers.io()) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt index 58e3ca12346..a3a45395f54 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt @@ -44,7 +44,6 @@ import com.wire.android.ui.home.conversationslist.model.ConversationsSource import com.wire.android.ui.home.conversationslist.model.DialogState import com.wire.android.ui.home.conversationslist.model.GroupDialogState import com.wire.android.util.dispatchers.DispatcherProvider -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.MutedConversationStatus import com.wire.kalium.logic.data.id.ConversationId @@ -129,7 +128,6 @@ class ConversationListViewModelImpl @AssistedInject constructor( private val refreshUsersWithoutMetadata: RefreshUsersWithoutMetadataUseCase, private val refreshConversationsWithoutMetadata: RefreshConversationsWithoutMetadataUseCase, private val updateConversationArchivedStatus: UpdateConversationArchivedStatusUseCase, - private val wireSessionImageLoader: WireSessionImageLoader, private val userTypeMapper: UserTypeMapper, ) : ConversationListViewModel, ViewModel() { @@ -205,7 +203,6 @@ class ConversationListViewModelImpl @AssistedInject constructor( ).map { it.map { conversationDetails -> conversationDetails.toConversationItem( - wireSessionImageLoader = wireSessionImageLoader, userTypeMapper = userTypeMapper, searchQuery = searchQuery, ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/BadgeEventType.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/BadgeEventType.kt index 722266a63ad..a1bbc3bad0a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/BadgeEventType.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/BadgeEventType.kt @@ -18,15 +18,38 @@ package com.wire.android.ui.home.conversationslist.model +import kotlinx.serialization.Serializable + +@Serializable sealed class BadgeEventType { + + @Serializable data class UnreadMessage(val unreadMessageCount: Int) : BadgeEventType() - object UnreadMention : BadgeEventType() - object UnreadReply : BadgeEventType() - object MissedCall : BadgeEventType() - object Knock : BadgeEventType() - object ReceivedConnectionRequest : BadgeEventType() - object SentConnectRequest : BadgeEventType() - object Blocked : BadgeEventType() - object Deleted : BadgeEventType() - object None : BadgeEventType() + + @Serializable + data object UnreadMention : BadgeEventType() + + @Serializable + data object UnreadReply : BadgeEventType() + + @Serializable + data object MissedCall : BadgeEventType() + + @Serializable + data object Knock : BadgeEventType() + + @Serializable + data object ReceivedConnectionRequest : BadgeEventType() + + @Serializable + data object SentConnectRequest : BadgeEventType() + + @Serializable + data object Blocked : BadgeEventType() + + @Serializable + data object Deleted : BadgeEventType() + + @Serializable + data object None : BadgeEventType() } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationItem.kt index 467805827d1..8ba2d340eb8 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationItem.kt @@ -29,7 +29,9 @@ import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.OtherUser import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.type.isTeammate +import kotlinx.serialization.Serializable +@Serializable sealed class ConversationItem : ConversationFolderItem { abstract val conversationId: ConversationId abstract val mutedStatus: MutedConversationStatus @@ -45,6 +47,7 @@ sealed class ConversationItem : ConversationFolderItem { val isTeamConversation get() = teamId != null + @Serializable data class GroupConversation( val groupName: String, val hasOnGoingCall: Boolean = false, @@ -64,6 +67,7 @@ sealed class ConversationItem : ConversationFolderItem { override val searchQuery: String = "", ) : ConversationItem() + @Serializable data class PrivateConversation( val userAvatarData: UserAvatarData, val conversationInfo: ConversationInfo, @@ -82,6 +86,7 @@ sealed class ConversationItem : ConversationFolderItem { override val searchQuery: String = "", ) : ConversationItem() + @Serializable data class ConnectionConversation( val userAvatarData: UserAvatarData, val conversationInfo: ConversationInfo, @@ -100,6 +105,7 @@ sealed class ConversationItem : ConversationFolderItem { } } +@Serializable data class ConversationInfo( val name: String, val membership: Membership = Membership.None, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModel.kt index 9d10a6888d4..8198a2a4201 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModel.kt @@ -34,7 +34,6 @@ import com.wire.android.ui.navArgs import com.wire.android.util.FileManager import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.startFileShareIntent -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase @@ -57,7 +56,6 @@ import javax.inject.Inject @HiltViewModel class MediaGalleryViewModel @Inject constructor( savedStateHandle: SavedStateHandle, - wireSessionImageLoader: WireSessionImageLoader, private val getConversationDetails: ObserveConversationDetailsUseCase, private val dispatchers: DispatcherProvider, private val getImageData: GetMessageAssetUseCase, @@ -67,7 +65,6 @@ class MediaGalleryViewModel @Inject constructor( private val mediaGalleryNavArgs: MediaGalleryNavArgs = savedStateHandle.navArgs() val imageAsset: ImageAsset.PrivateAsset = ImageAsset.PrivateAsset( - wireSessionImageLoader, mediaGalleryNavArgs.conversationId, mediaGalleryNavArgs.messageId, mediaGalleryNavArgs.isSelfAsset, diff --git a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModel.kt index efda17b8c03..de0cfd71bfa 100644 --- a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModel.kt @@ -45,7 +45,6 @@ import com.wire.android.ui.home.messagecomposer.SelfDeletionDuration import com.wire.android.util.EMPTY import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.parcelableArrayList -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.message.SelfDeletionTimer import com.wire.kalium.logic.data.message.SelfDeletionTimer.Companion.SELF_DELETION_LOG_TAG @@ -80,7 +79,6 @@ class ImportMediaAuthenticatedViewModel @Inject constructor( private val handleUriAsset: HandleUriAssetUseCase, private val persistNewSelfDeletionTimerUseCase: PersistNewSelfDeletionTimerUseCase, private val observeSelfDeletionSettingsForConversation: ObserveSelfDeletionTimerSettingsForConversationUseCase, - private val wireSessionImageLoader: WireSessionImageLoader, val dispatchers: DispatcherProvider, ) : ViewModel() { val searchQueryTextState: TextFieldState = TextFieldState() @@ -124,7 +122,7 @@ class ImportMediaAuthenticatedViewModel @Inject constructor( getSelf().collect { selfUser -> withContext(dispatchers.main()) { avatarAsset = selfUser.previewPicture?.let { - ImageAsset.UserAvatarAsset(wireSessionImageLoader, it) + ImageAsset.UserAvatarAsset(it) } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt index f11a08c7030..a909883ab9d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt @@ -47,7 +47,6 @@ import com.wire.android.ui.userprofile.other.OtherUserProfileInfoMessageType.Rem import com.wire.android.ui.userprofile.other.OtherUserProfileInfoMessageType.UnblockingUserOperationError import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.ui.UIText -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.MutedConversationStatus import com.wire.kalium.logic.data.id.ConversationId @@ -96,7 +95,6 @@ class OtherUserProfileScreenViewModel @Inject constructor( private val observeOneToOneConversation: GetOneToOneConversationUseCase, private val observeUserInfo: ObserveUserInfoUseCase, private val userTypeMapper: UserTypeMapper, - private val wireSessionImageLoader: WireSessionImageLoader, private val observeConversationRoleForUser: ObserveConversationRoleForUserUseCase, private val removeMemberFromConversation: RemoveMemberFromConversationUseCase, private val updateMemberRole: UpdateConversationMemberRoleUseCase, @@ -384,7 +382,7 @@ class OtherUserProfileScreenViewModel @Inject constructor( ) { val otherUser = userResult.otherUser val userAvatarAsset = otherUser.completePicture - ?.let { pic -> ImageAsset.UserAvatarAsset(wireSessionImageLoader, pic) } + ?.let { pic -> ImageAsset.UserAvatarAsset(pic) } state = state.copy( isDataLoading = false, diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt index 09b9d4ef760..37008d07364 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt @@ -42,7 +42,6 @@ import com.wire.android.notification.WireNotificationManager import com.wire.android.ui.legalhold.banner.LegalHoldUIState import com.wire.android.ui.userprofile.self.dialog.StatusDialogData import com.wire.android.util.dispatchers.DispatcherProvider -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.configuration.server.ServerConfig import com.wire.kalium.logic.data.call.Call import com.wire.kalium.logic.data.id.QualifiedIdMapper @@ -94,7 +93,6 @@ class SelfUserProfileViewModel @Inject constructor( private val logout: LogoutUseCase, private val observeLegalHoldStatusForSelfUser: ObserveLegalHoldStateForSelfUserUseCase, private val dispatchers: DispatcherProvider, - private val wireSessionImageLoader: WireSessionImageLoader, private val authServerConfigProvider: AuthServerConfigProvider, private val selfServerLinks: SelfServerConfigUseCase, private val otherAccountMapper: OtherAccountMapper, @@ -211,7 +209,7 @@ class SelfUserProfileViewModel @Inject constructor( showLoadingAvatar(true) try { userProfileState = userProfileState.copy( - avatarAsset = UserAvatarAsset(wireSessionImageLoader, avatarAssetId) + avatarAsset = UserAvatarAsset(avatarAssetId) ) // Update avatar asset id on user data store // TODO: obtain the asset id through a useCase once we also store assets ids diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModel.kt index 3fd3f129163..53a5f518132 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModel.kt @@ -28,7 +28,6 @@ import com.wire.android.ui.home.conversations.details.participants.usecase.Obser import com.wire.android.ui.navArgs import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.ui.UIText -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.StorageFailure import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.QualifiedID @@ -62,7 +61,6 @@ class ServiceDetailsViewModel @Inject constructor( private val getServiceById: GetServiceByIdUseCase, private val observeIsServiceMember: ObserveIsServiceMemberUseCase, private val observeConversationRoleForUser: ObserveConversationRoleForUserUseCase, - private val wireSessionImageLoader: WireSessionImageLoader, private val removeMemberFromConversation: RemoveMemberFromConversationUseCase, private val addServiceToConversation: AddServiceToConversationUseCase, private val serviceDetailsMapper: ServiceDetailsMapper, @@ -137,7 +135,7 @@ class ServiceDetailsViewModel @Inject constructor( getServiceById(serviceId = serviceId).also { service -> if (service != null) { val serviceAvatarAsset = service.completeAssetId?.let { asset -> - ImageAsset.UserAvatarAsset(wireSessionImageLoader, asset) + ImageAsset.UserAvatarAsset(asset) } serviceDetailsState = serviceDetailsState.copy( diff --git a/app/src/main/kotlin/com/wire/android/util/ui/LocalizedStringResource.kt b/app/src/main/kotlin/com/wire/android/util/ui/LocalizedStringResource.kt index b9a326ce9e6..7f8f3ebf9dc 100644 --- a/app/src/main/kotlin/com/wire/android/util/ui/LocalizedStringResource.kt +++ b/app/src/main/kotlin/com/wire/android/util/ui/LocalizedStringResource.kt @@ -17,37 +17,16 @@ */ package com.wire.android.util.ui -import android.content.Context import androidx.annotation.PluralsRes import androidx.annotation.StringRes +import kotlinx.serialization.Serializable +@Serializable sealed interface LocalizedStringResource { - fun getString(context: Context): String - data class StringResource(@StringRes val id: Int) : LocalizedStringResource { - override fun getString(context: Context): String = context.getString(id) - } + @Serializable + data class String(@StringRes val id: Int) : LocalizedStringResource - data class PluralResource(@PluralsRes val id: Int, val quantity: Int, val formatArgs: Array) : LocalizedStringResource { - override fun getString(context: Context): String = context.resources.getQuantityString(id, quantity, formatArgs) - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as PluralResource - - if (id != other.id) return false - if (quantity != other.quantity) return false - if (!formatArgs.contentEquals(other.formatArgs)) return false - - return true - } - - override fun hashCode(): Int { - var result = id - result = 31 * result + quantity - result = 31 * result + formatArgs.contentHashCode() - return result - } - } + @Serializable + data class Plural(@PluralsRes val id: Int, val quantity: Int) : LocalizedStringResource } diff --git a/app/src/main/kotlin/com/wire/android/util/ui/UIText.kt b/app/src/main/kotlin/com/wire/android/util/ui/UIText.kt index af3fd64d2ba..14bdb4faea3 100644 --- a/app/src/main/kotlin/com/wire/android/util/ui/UIText.kt +++ b/app/src/main/kotlin/com/wire/android/util/ui/UIText.kt @@ -26,22 +26,29 @@ import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import com.wire.android.appLogger import com.wire.kalium.logic.data.message.mention.MessageMention +import com.wire.kalium.util.serialization.AnyPrimitiveValueSerializer +import kotlinx.serialization.Serializable +@Serializable sealed class UIText { + + @Serializable data class DynamicString( val value: String, val mentions: List = listOf() ) : UIText() + @Serializable class StringResource( @StringRes val resId: Int, - vararg val formatArgs: Any + vararg val formatArgs: @Serializable(with = AnyPrimitiveValueSerializer::class) Any ) : UIText() + @Serializable class PluralResource( @PluralsRes val resId: Int, val count: Int, - vararg val formatArgs: Any + vararg val formatArgs: @Serializable(with = AnyPrimitiveValueSerializer::class) Any ) : UIText() @Suppress("SpreadOperator") diff --git a/app/src/test/kotlin/com/wire/android/mapper/MessageMapperTest.kt b/app/src/test/kotlin/com/wire/android/mapper/MessageMapperTest.kt index d10a96f29d2..3cde050371e 100644 --- a/app/src/test/kotlin/com/wire/android/mapper/MessageMapperTest.kt +++ b/app/src/test/kotlin/com/wire/android/mapper/MessageMapperTest.kt @@ -32,7 +32,6 @@ import com.wire.android.ui.home.conversations.model.UIMessageContent.TextMessage import com.wire.android.ui.home.conversationslist.model.Membership import com.wire.android.util.time.ISOFormatter import com.wire.android.util.ui.UIText -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.android.util.uiMessageDateTime import com.wire.kalium.logic.data.message.Message import com.wire.kalium.logic.data.message.MessageContent @@ -220,11 +219,8 @@ class MessageMapperTest { @MockK lateinit var isoFormatter: ISOFormatter - @MockK - private lateinit var wireSessionImageLoader: WireSessionImageLoader - private val messageMapper by lazy { - MessageMapper(userTypeMapper, messageContentMapper, isoFormatter, wireSessionImageLoader) + MessageMapper(userTypeMapper, messageContentMapper, isoFormatter) } init { diff --git a/app/src/test/kotlin/com/wire/android/mapper/OtherAccountMapperTest.kt b/app/src/test/kotlin/com/wire/android/mapper/OtherAccountMapperTest.kt index f192a529008..2f619df0c9b 100644 --- a/app/src/test/kotlin/com/wire/android/mapper/OtherAccountMapperTest.kt +++ b/app/src/test/kotlin/com/wire/android/mapper/OtherAccountMapperTest.kt @@ -20,11 +20,9 @@ package com.wire.android.mapper import com.wire.android.ui.home.conversations.avatar import com.wire.android.ui.userprofile.self.model.OtherAccount -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.team.Team import com.wire.kalium.logic.data.user.SelfUser import io.mockk.MockKAnnotations -import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test @@ -45,27 +43,23 @@ class OtherAccountMapperTest { // Then results.forEachIndexed { index, result -> val (selfUser, team) = data[index] - assert(compareResult(arrangement.wireSessionImageLoader, selfUser, team, result)) + assert(compareResult(selfUser, team, result)) } } private fun compareResult( - wireSessionImageLoader: WireSessionImageLoader, selfUser: SelfUser, team: Team?, otherAccount: OtherAccount ): Boolean = selfUser.id == otherAccount.id && selfUser.name == otherAccount.fullName - && selfUser.avatar(wireSessionImageLoader, selfUser.connectionStatus) == otherAccount.avatarData + && selfUser.avatar(selfUser.connectionStatus) == otherAccount.avatarData && team?.name == otherAccount.teamName private class Arrangement { - @MockK - lateinit var wireSessionImageLoader: WireSessionImageLoader - - private val mapper: OtherAccountMapper by lazy { OtherAccountMapper(wireSessionImageLoader) } + private val mapper: OtherAccountMapper by lazy { OtherAccountMapper() } init { MockKAnnotations.init(this, relaxUnitFun = true) diff --git a/app/src/test/kotlin/com/wire/android/mapper/RegularMessageContentMapperTest.kt b/app/src/test/kotlin/com/wire/android/mapper/RegularMessageContentMapperTest.kt index 3158c9fc66b..8d4f4996230 100644 --- a/app/src/test/kotlin/com/wire/android/mapper/RegularMessageContentMapperTest.kt +++ b/app/src/test/kotlin/com/wire/android/mapper/RegularMessageContentMapperTest.kt @@ -28,7 +28,6 @@ import com.wire.android.ui.home.conversations.model.UIMessageContent import com.wire.android.ui.home.conversations.model.UIMessageContent.AssetMessage import com.wire.android.util.time.ISOFormatter import com.wire.android.util.ui.UIText -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.message.AssetContent import com.wire.kalium.logic.data.message.AssetContent.AssetMetadata @@ -220,11 +219,8 @@ class RegularMessageContentMapperTest { @MockK lateinit var messageResourceProvider: MessageResourceProvider - @MockK - private lateinit var wireSessionImageLoader: WireSessionImageLoader - private val messageContentMapper by lazy { - RegularMessageMapper(messageResourceProvider, wireSessionImageLoader, ISOFormatter()) + RegularMessageMapper(messageResourceProvider, ISOFormatter()) } init { diff --git a/app/src/test/kotlin/com/wire/android/mapper/UICallParticipantMapperTest.kt b/app/src/test/kotlin/com/wire/android/mapper/UICallParticipantMapperTest.kt index 0c34b0124ed..d3c23da2a63 100644 --- a/app/src/test/kotlin/com/wire/android/mapper/UICallParticipantMapperTest.kt +++ b/app/src/test/kotlin/com/wire/android/mapper/UICallParticipantMapperTest.kt @@ -21,7 +21,6 @@ package com.wire.android.mapper import com.wire.kalium.logic.data.call.Participant import com.wire.kalium.logic.data.id.QualifiedID import io.mockk.MockKAnnotations -import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test @@ -54,7 +53,7 @@ class UICallParticipantMapperTest { private class Arrangement { - private val mapper: UICallParticipantMapper = UICallParticipantMapper(mockk(), UserTypeMapper()) + private val mapper: UICallParticipantMapper = UICallParticipantMapper(UserTypeMapper()) init { MockKAnnotations.init(this, relaxUnitFun = true) diff --git a/app/src/test/kotlin/com/wire/android/mapper/UIParticipantMapperTest.kt b/app/src/test/kotlin/com/wire/android/mapper/UIParticipantMapperTest.kt index a6f600441f0..de63bc438ba 100644 --- a/app/src/test/kotlin/com/wire/android/mapper/UIParticipantMapperTest.kt +++ b/app/src/test/kotlin/com/wire/android/mapper/UIParticipantMapperTest.kt @@ -23,7 +23,6 @@ import com.wire.android.ui.home.conversations.details.participants.model.UIParti import com.wire.android.ui.home.conversations.handle import com.wire.android.ui.home.conversations.name import com.wire.android.ui.home.conversations.userId -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.conversation.Conversation.Member import com.wire.kalium.logic.data.conversation.MemberDetails import com.wire.kalium.logic.data.id.TeamId @@ -36,7 +35,6 @@ import com.wire.kalium.logic.data.user.UserAvailabilityStatus import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.type.UserType import io.mockk.MockKAnnotations -import io.mockk.impl.annotations.MockK import kotlinx.coroutines.test.runTest import org.amshove.kluent.internal.assertEquals import org.junit.jupiter.api.Test @@ -58,7 +56,7 @@ class UIParticipantMapperTest { val results = data.map { mapper.toUIParticipant(it.user) } // Then results.forEachIndexed { index, result -> - assert(compareResult(arrangement.wireSessionImageLoader, data[index], result, arrangement.userTypeMapper)) + assert(compareResult(data[index], result, arrangement.userTypeMapper)) } } @@ -96,7 +94,6 @@ class UIParticipantMapperTest { } private fun compareResult( - wireSessionImageLoader: WireSessionImageLoader, memberDetails: MemberDetails, uiParticipant: UIParticipant, userTypeMapper: UserTypeMapper @@ -105,20 +102,17 @@ class UIParticipantMapperTest { return (memberDetails.userId == uiParticipant.id && memberDetails.name == uiParticipant.name && memberDetails.handle == uiParticipant.handle - && memberDetails.user.avatar(wireSessionImageLoader, connectionState) == uiParticipant.avatarData + && memberDetails.user.avatar(connectionState) == uiParticipant.avatarData && userTypeMapper.toMembership(memberDetails.user.userType) == uiParticipant.membership && memberDetails.user is SelfUser == uiParticipant.isSelf) } private class Arrangement { - @MockK - lateinit var wireSessionImageLoader: WireSessionImageLoader - val userTypeMapper: UserTypeMapper = UserTypeMapper() private val mapper: UIParticipantMapper by lazy { - UIParticipantMapper(userTypeMapper, wireSessionImageLoader) + UIParticipantMapper(userTypeMapper) } init { diff --git a/app/src/test/kotlin/com/wire/android/model/ImageAssetTest.kt b/app/src/test/kotlin/com/wire/android/model/ImageAssetTest.kt index 61bac0831af..eb8d3110c61 100644 --- a/app/src/test/kotlin/com/wire/android/model/ImageAssetTest.kt +++ b/app/src/test/kotlin/com/wire/android/model/ImageAssetTest.kt @@ -19,12 +19,10 @@ package com.wire.android.model import android.net.Uri -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.UserAssetId import io.mockk.MockKAnnotations import io.mockk.every -import io.mockk.impl.annotations.MockK import io.mockk.mockk import io.mockk.mockkStatic import okio.Path @@ -36,9 +34,6 @@ import org.junit.jupiter.api.Test class ImageAssetTest { - @MockK - private lateinit var imageLoader: WireSessionImageLoader - @BeforeEach fun setup() { MockKAnnotations.init(this, relaxUnitFun = true) @@ -47,15 +42,13 @@ class ImageAssetTest { every { Uri.parse(any()) } returns mockUri } - private fun createUserAvatarAsset(userAssetId: UserAssetId) = ImageAsset.UserAvatarAsset( - imageLoader, userAssetId - ) + private fun createUserAvatarAsset(userAssetId: UserAssetId) = ImageAsset.UserAvatarAsset(userAssetId) private fun createPrivateAsset( conversationId: ConversationId, messageId: String, isSelfAsset: Boolean - ) = ImageAsset.PrivateAsset(imageLoader, conversationId, messageId, isSelfAsset) + ) = ImageAsset.PrivateAsset(conversationId, messageId, isSelfAsset) private fun createLocalAsset( dataPath: Path, diff --git a/app/src/test/kotlin/com/wire/android/navigation/NavigationUtilsTest.kt b/app/src/test/kotlin/com/wire/android/navigation/NavigationUtilsTest.kt index ae5667d5c43..97f32e2b119 100644 --- a/app/src/test/kotlin/com/wire/android/navigation/NavigationUtilsTest.kt +++ b/app/src/test/kotlin/com/wire/android/navigation/NavigationUtilsTest.kt @@ -22,7 +22,6 @@ import com.wire.android.model.ImageAsset import com.wire.android.model.parseIntoPrivateImageAsset import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.id.QualifiedIdMapperImpl -import io.mockk.mockk import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows @@ -44,7 +43,7 @@ internal class NavigationUtilsTest { "$mockConversationIdValue@$mockConversationIdDomain:$mockMessageId:$mockIsSelfAsset:$mockIsEphemeral" // When - val privateImgAsset = correctImagePrivateAssetString.parseIntoPrivateImageAsset(mockk(), qualifiedIdMapper) + val privateImgAsset = correctImagePrivateAssetString.parseIntoPrivateImageAsset(qualifiedIdMapper) // Then assertEquals(privateImgAsset.conversationId.value, mockConversationIdValue) @@ -62,7 +61,7 @@ internal class NavigationUtilsTest { val mockWrongImagePrivateAssetString = "wrong-private-asset@image" // When, Then - assertThrows { mockWrongImagePrivateAssetString.parseIntoPrivateImageAsset(mockk(), qualifiedIdMapper) } + assertThrows { mockWrongImagePrivateAssetString.parseIntoPrivateImageAsset(qualifiedIdMapper) } } @Test @@ -87,11 +86,13 @@ internal class NavigationUtilsTest { val mockQualifiedIdDomain = "mocked.domain" val mockMessageId = "mocked-message-id" val actualPrivateAssetImage = ImageAsset.PrivateAsset( - mockk(), - QualifiedID( + conversationId = QualifiedID( value = mockQualifiedIdValue, domain = mockQualifiedIdDomain - ), mockMessageId, true, true + ), + messageId = mockMessageId, + isSelfAsset = true, + isEphemeral = true ) val expectedPrivateAssetImage = "$mockQualifiedIdValue@$mockQualifiedIdDomain:$mockMessageId:true:true" diff --git a/app/src/test/kotlin/com/wire/android/ui/calling/SharedCallingViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/calling/SharedCallingViewModelTest.kt index 6eaceb90506..18e62a81877 100644 --- a/app/src/test/kotlin/com/wire/android/ui/calling/SharedCallingViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/calling/SharedCallingViewModelTest.kt @@ -25,7 +25,6 @@ import com.wire.android.config.TestDispatcherProvider import com.wire.android.mapper.UICallParticipantMapper import com.wire.android.mapper.UserTypeMapper import com.wire.android.media.CallRinger -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.call.VideoState import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase @@ -100,9 +99,6 @@ class SharedCallingViewModelTest { @MockK private lateinit var view: View - @MockK - private lateinit var wireSessionImageLoader: WireSessionImageLoader - @MockK private lateinit var userTypeMapper: UserTypeMapper @@ -110,10 +106,7 @@ class SharedCallingViewModelTest { private lateinit var onCompleted: () -> Unit private val uiCallParticipantMapper: UICallParticipantMapper by lazy { - UICallParticipantMapper( - wireSessionImageLoader, - userTypeMapper - ) + UICallParticipantMapper(userTypeMapper) } private lateinit var sharedCallingViewModel: SharedCallingViewModel @@ -141,7 +134,6 @@ class SharedCallingViewModelTest { observeSpeaker = observeSpeaker, callRinger = callRinger, uiCallParticipantMapper = uiCallParticipantMapper, - wireSessionImageLoader = wireSessionImageLoader, userTypeMapper = userTypeMapper, dispatchers = TestDispatcherProvider() ) diff --git a/app/src/test/kotlin/com/wire/android/ui/connection/ConnectionActionButtonViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/connection/ConnectionActionButtonViewModelTest.kt index 58f1e3be4c8..034026ff597 100644 --- a/app/src/test/kotlin/com/wire/android/ui/connection/ConnectionActionButtonViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/connection/ConnectionActionButtonViewModelTest.kt @@ -30,7 +30,6 @@ import com.wire.android.framework.TestConversation import com.wire.android.framework.TestUser import com.wire.android.ui.userprofile.other.OtherUserProfileScreenViewModelTest import com.wire.android.util.ui.UIText -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.feature.connection.AcceptConnectionRequestUseCase @@ -343,9 +342,6 @@ internal class ConnectionActionButtonHiltArrangement { @MockK lateinit var ignoreConnectionRequest: IgnoreConnectionRequestUseCase - @MockK - lateinit var wireSessionImageLoader: WireSessionImageLoader - @MockK lateinit var unblockUser: UnblockUserUseCase diff --git a/app/src/test/kotlin/com/wire/android/ui/home/HomeViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/HomeViewModelTest.kt index dcb849f3407..666c721f35b 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/HomeViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/HomeViewModelTest.kt @@ -22,7 +22,6 @@ import com.wire.android.config.CoroutineTestExtension import com.wire.android.datastore.GlobalDataStore import com.wire.android.framework.TestUser import com.wire.android.migration.userDatabase.ShouldTriggerMigrationForUserUserCase -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.user.SelfUser import com.wire.kalium.logic.data.user.UserAvailabilityStatus import com.wire.kalium.logic.feature.client.NeedsToRegisterClientUseCase @@ -98,8 +97,6 @@ class HomeViewModelTest { @MockK lateinit var observeLegalHoldStatusForSelfUser: ObserveLegalHoldStateForSelfUserUseCase @MockK - lateinit var wireSessionImageLoader: WireSessionImageLoader - @MockK lateinit var shouldTriggerMigrationForUser: ShouldTriggerMigrationForUserUserCase private val viewModel by lazy { @@ -109,7 +106,6 @@ class HomeViewModelTest { getSelf = getSelf, needsToRegisterClient = needsToRegisterClient, observeLegalHoldStatusForSelfUser = observeLegalHoldStatusForSelfUser, - wireSessionImageLoader = wireSessionImageLoader, shouldTriggerMigrationForUser = shouldTriggerMigrationForUser ) } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/participants/usecase/ObserveParticipantsForConversationUseCaseTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/participants/usecase/ObserveParticipantsForConversationUseCaseTest.kt index dc0e39f3f6c..4f4aba479ec 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/participants/usecase/ObserveParticipantsForConversationUseCaseTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/participants/usecase/ObserveParticipantsForConversationUseCaseTest.kt @@ -23,7 +23,6 @@ import com.wire.android.config.TestDispatcherProvider import com.wire.android.mapper.UIParticipantMapper import com.wire.android.mapper.UserTypeMapper import com.wire.android.mapper.testOtherUser -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.conversation.Conversation.Member import com.wire.kalium.logic.data.conversation.MemberDetails import com.wire.kalium.logic.data.id.ConversationId @@ -220,9 +219,7 @@ internal class ObserveParticipantsForConversationUseCaseArrangement { @MockK lateinit var getMembersE2EICertificateStatuses: GetMembersE2EICertificateStatusesUseCase - @MockK - private lateinit var wireSessionImageLoader: WireSessionImageLoader - private val uIParticipantMapper by lazy { UIParticipantMapper(UserTypeMapper(), wireSessionImageLoader) } + private val uIParticipantMapper by lazy { UIParticipantMapper(UserTypeMapper()) } private val conversationMembersChannel = Channel>(capacity = Channel.UNLIMITED) private val useCase by lazy { ObserveParticipantsForConversationUseCase( diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelArrangement.kt index 931b5f6280d..778549c42ad 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelArrangement.kt @@ -23,7 +23,6 @@ import com.wire.android.config.mockUri import com.wire.android.framework.TestUser import com.wire.android.ui.home.conversations.ConversationNavArgs import com.wire.android.ui.navArgs -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.StorageFailure import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.id.ConversationId @@ -59,9 +58,6 @@ class ConversationInfoViewModelArrangement { @MockK lateinit var fetchConversationMLSVerificationStatus: FetchConversationMLSVerificationStatusUseCase - @MockK - private lateinit var wireSessionImageLoader: WireSessionImageLoader - @MockK(relaxed = true) lateinit var onNotFound: () -> Unit @@ -71,7 +67,6 @@ class ConversationInfoViewModelArrangement { savedStateHandle, observeConversationDetails, fetchConversationMLSVerificationStatus, - wireSessionImageLoader, selfUserId = TestUser.SELF_USER_ID, ) } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCaseTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCaseTest.kt index 2d7dd96a6ea..8afc5b6d8ff 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCaseTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCaseTest.kt @@ -24,7 +24,6 @@ import com.wire.android.config.TestDispatcherProvider import com.wire.android.framework.TestConversationDetails import com.wire.android.mapper.UserTypeMapper import com.wire.android.ui.home.conversationslist.model.Membership -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents import com.wire.kalium.logic.data.conversation.ConversationQueryConfig import com.wire.kalium.logic.feature.conversation.GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase @@ -85,9 +84,6 @@ class GetConversationsFromSearchUseCaseTest { @MockK lateinit var useCase: GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase - @MockK - lateinit var wireSessionImageLoader: WireSessionImageLoader - @MockK lateinit var userTypeMapper: UserTypeMapper @@ -110,6 +106,6 @@ class GetConversationsFromSearchUseCaseTest { } returns flowOf(PagingData.from(conversations)) } - fun arrange() = this to GetConversationsFromSearchUseCase(useCase, wireSessionImageLoader, userTypeMapper, dispatcherProvider) + fun arrange() = this to GetConversationsFromSearchUseCase(useCase, userTypeMapper, dispatcherProvider) } } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt index 2aa948a4d29..1388bbcaf70 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt @@ -29,7 +29,6 @@ import com.wire.android.mapper.UserTypeMapper import com.wire.android.ui.common.dialogs.BlockUserDialogState import com.wire.android.ui.home.conversations.usecase.GetConversationsFromSearchUseCase import com.wire.android.ui.home.conversationslist.model.ConversationsSource -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents import com.wire.kalium.logic.data.conversation.MutedConversationStatus import com.wire.kalium.logic.data.id.ConversationId @@ -205,9 +204,6 @@ class ConversationListViewModelTest { private lateinit var observeConversationListDetailsWithEventsUseCase: ObserveConversationListDetailsWithEventsUseCase - @MockK - private lateinit var wireSessionImageLoader: WireSessionImageLoader - init { MockKAnnotations.init(this, relaxUnitFun = true) coEvery { @@ -258,7 +254,6 @@ class ConversationListViewModelTest { updateConversationArchivedStatus = updateConversationArchivedStatus, observeConversationListDetailsWithEvents = observeConversationListDetailsWithEventsUseCase, userTypeMapper = UserTypeMapper(), - wireSessionImageLoader = wireSessionImageLoader ) } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModelTest.kt index d41787b15ce..80d101cae10 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModelTest.kt @@ -29,7 +29,6 @@ import com.wire.android.ui.home.conversations.delete.DeleteMessageDialogActiveSt import com.wire.android.ui.home.conversations.delete.DeleteMessageDialogsState import com.wire.android.ui.navArgs import com.wire.android.util.FileManager -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.ConversationDetails @@ -192,9 +191,6 @@ class MediaGalleryViewModelTest { @MockK private lateinit var savedStateHandle: SavedStateHandle - @MockK - private lateinit var wireSessionImageLoader: WireSessionImageLoader - @MockK lateinit var getConversationDetails: ObserveConversationDetailsUseCase @@ -271,7 +267,6 @@ class MediaGalleryViewModelTest { fun arrange() = this to MediaGalleryViewModel( savedStateHandle, - wireSessionImageLoader, getConversationDetails, TestDispatcherProvider(), getImageData, diff --git a/app/src/test/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModelTest.kt index f2623f18c9e..e305a258bc6 100644 --- a/app/src/test/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModelTest.kt @@ -28,7 +28,6 @@ import com.wire.android.framework.TestConversationItem import com.wire.android.framework.TestUser import com.wire.android.ui.home.conversations.usecase.GetConversationsFromSearchUseCase import com.wire.android.ui.home.conversations.usecase.HandleUriAssetUseCase -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveSelfDeletionTimerSettingsForConversationUseCase import com.wire.kalium.logic.feature.selfDeletingMessages.PersistNewSelfDeletionTimerUseCase import com.wire.kalium.logic.feature.user.GetSelfUserUseCase @@ -87,9 +86,6 @@ class ImportMediaAuthenticatedViewModelTest { @MockK lateinit var observeSelfDeletionSettingsForConversation: ObserveSelfDeletionTimerSettingsForConversationUseCase - @MockK - lateinit var wireSessionImageLoader: WireSessionImageLoader - init { MockKAnnotations.init(this, relaxUnitFun = true) coEvery { @@ -109,7 +105,6 @@ class ImportMediaAuthenticatedViewModelTest { handleUriAsset = handleUriAssetUseCase, persistNewSelfDeletionTimerUseCase = persistNewSelfDeletionTimerUseCase, observeSelfDeletionSettingsForConversation = observeSelfDeletionSettingsForConversation, - wireSessionImageLoader = wireSessionImageLoader, dispatchers = dispatcherProvider, ) } diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileViewModelArrangement.kt index e550f0188dc..f0a5443b530 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileViewModelArrangement.kt @@ -28,7 +28,6 @@ import com.wire.android.ui.home.conversationslist.model.Membership import com.wire.android.ui.navArgs import com.wire.android.ui.userprofile.other.OtherUserProfileScreenViewModelTest.Companion.CONVERSATION_ID import com.wire.android.ui.userprofile.other.OtherUserProfileScreenViewModelTest.Companion.USER_ID -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.feature.client.FetchUsersClientsFromRemoteUseCase import com.wire.kalium.logic.feature.client.ObserveClientsByUserIdUseCase @@ -44,8 +43,8 @@ import com.wire.kalium.logic.feature.conversation.UpdateConversationArchivedStat import com.wire.kalium.logic.feature.conversation.UpdateConversationMemberRoleResult import com.wire.kalium.logic.feature.conversation.UpdateConversationMemberRoleUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationMutedStatusUseCase -import com.wire.kalium.logic.feature.e2ei.usecase.IsOtherUserE2EIVerifiedUseCase import com.wire.kalium.logic.feature.e2ei.usecase.GetUserE2eiCertificatesUseCase +import com.wire.kalium.logic.feature.e2ei.usecase.IsOtherUserE2EIVerifiedUseCase import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import com.wire.kalium.logic.feature.user.GetUserInfoResult import com.wire.kalium.logic.feature.user.ObserveUserInfoUseCase @@ -66,9 +65,6 @@ internal class OtherUserProfileViewModelArrangement { @MockK lateinit var observeUserInfo: ObserveUserInfoUseCase - @MockK - lateinit var wireSessionImageLoader: WireSessionImageLoader - @MockK lateinit var observeConversationRoleForUserUseCase: ObserveConversationRoleForUserUseCase @@ -123,7 +119,6 @@ internal class OtherUserProfileViewModelArrangement { getOneToOneConversation, observeUserInfo, userTypeMapper, - wireSessionImageLoader, observeConversationRoleForUserUseCase, removeMemberFromConversationUseCase, updateConversationMemberRoleUseCase, diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelArrangement.kt index e9478646aa1..0ebea4db93a 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelArrangement.kt @@ -30,7 +30,6 @@ import com.wire.android.mapper.OtherAccountMapper import com.wire.android.notification.NotificationChannelsManager import com.wire.android.notification.WireNotificationManager import com.wire.android.util.dispatchers.DispatcherProvider -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.feature.auth.LogoutUseCase import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase @@ -67,8 +66,6 @@ class SelfUserProfileViewModelArrangement { @MockK lateinit var dispatchers: DispatcherProvider @MockK - lateinit var wireSessionImageLoader: WireSessionImageLoader - @MockK lateinit var authServerConfigProvider: AuthServerConfigProvider @MockK lateinit var selfServerLinks: SelfServerConfigUseCase @@ -105,7 +102,6 @@ class SelfUserProfileViewModelArrangement { logout = logout, observeLegalHoldStatusForSelfUser = observeLegalHoldStatusForSelfUser, dispatchers = TestDispatcherProvider(), - wireSessionImageLoader = wireSessionImageLoader, authServerConfigProvider = authServerConfigProvider, selfServerLinks = selfServerLinks, otherAccountMapper = otherAccountMapper, diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModelTest.kt index d5a5026c0d7..44165d97274 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModelTest.kt @@ -28,7 +28,6 @@ import com.wire.android.ui.home.conversations.details.participants.usecase.Conve import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveConversationRoleForUserUseCase import com.wire.android.ui.navArgs import com.wire.android.ui.userprofile.other.OtherUserProfileScreenViewModelTest -import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.StorageFailure import com.wire.kalium.logic.data.conversation.Conversation @@ -295,9 +294,6 @@ class ServiceDetailsViewModelTest { @MockK lateinit var observeConversationRoleForUser: ObserveConversationRoleForUserUseCase - @MockK - lateinit var wireSessionImageLoader: WireSessionImageLoader - @MockK lateinit var removeMemberFromConversation: RemoveMemberFromConversationUseCase @@ -316,7 +312,6 @@ class ServiceDetailsViewModelTest { getServiceById, observeIsServiceMember, observeConversationRoleForUser, - wireSessionImageLoader, removeMemberFromConversation, addServiceToConversation, serviceDetailsMapper, diff --git a/app/src/test/kotlin/com/wire/android/util/ui/AssetImageFetcherTest.kt b/app/src/test/kotlin/com/wire/android/util/ui/AssetImageFetcherTest.kt index bc9b49db4c4..c30e6346bce 100644 --- a/app/src/test/kotlin/com/wire/android/util/ui/AssetImageFetcherTest.kt +++ b/app/src/test/kotlin/com/wire/android/util/ui/AssetImageFetcherTest.kt @@ -53,7 +53,7 @@ internal class AssetImageFetcherTest { val someUserAssetId = AssetId("value", "domain") val someDummyData = "some-dummy-data".toByteArray() val someDummyName = "some-dummy-name" - val data = ImageAsset.UserAvatarAsset(mockk(), someUserAssetId) + val data = ImageAsset.UserAvatarAsset(someUserAssetId) val avatarPath = fakeKaliumFileSystem.selfUserAvatarPath() val (arrangement, assetImageFetcher) = Arrangement() .withSuccessfulImageData(data, avatarPath, someDummyData.size.toLong(), someDummyName) @@ -74,7 +74,7 @@ internal class AssetImageFetcherTest { val someUserAssetId = AssetId("value", "domain") val someDummyData = "some-dummy-data".toByteArray() val someDummyName = "some-dummy-name" - val data = ImageAsset.UserAvatarAsset(mockk(), someUserAssetId) + val data = ImageAsset.UserAvatarAsset(someUserAssetId) val avatarPath = fakeKaliumFileSystem.selfUserAvatarPath() val (arrangement, assetImageFetcher) = Arrangement() .withSuccessfulImageData(data, avatarPath, someDummyData.size.toLong(), someDummyName, 1) @@ -97,7 +97,7 @@ internal class AssetImageFetcherTest { val someMessageId = "some-message-id" val someDummyData = "some-dummy-data".toByteArray() val someDummyName = "some-dummy-name" - val data = ImageAsset.PrivateAsset(mockk(), someConversationId, someMessageId, true) + val data = ImageAsset.PrivateAsset(someConversationId, someMessageId, true) val avatarPath = fakeKaliumFileSystem.selfUserAvatarPath() val (arrangement, assetImageFetcher) = Arrangement() .withSuccessfulImageData(data, avatarPath, 1, someDummyName) @@ -117,7 +117,7 @@ internal class AssetImageFetcherTest { fun givenAUserAvatarAssetData_WhenCallingFetchUnsuccessfully_ThenFetchResultIsNotReturned() = runTest { // Given val someUserAssetId = AssetId("value", "domain") - val data = ImageAsset.UserAvatarAsset(mockk(), someUserAssetId) + val data = ImageAsset.UserAvatarAsset(someUserAssetId) val (arrangement, assetImageFetcher) = Arrangement().withErrorResponse(data).arrange() // When @@ -132,7 +132,7 @@ internal class AssetImageFetcherTest { // Given val someConversationId = ConversationId("some-value", "some-domain") val someMessageId = "some-message-id" - val data = ImageAsset.PrivateAsset(mockk(), someConversationId, someMessageId, true) + val data = ImageAsset.PrivateAsset(someConversationId, someMessageId, true) val (arrangement, assetImageFetcher) = Arrangement().withErrorResponse(data).arrange() // When @@ -147,7 +147,7 @@ internal class AssetImageFetcherTest { runTest { // Given val someUserAssetId = AssetId("value", "domain") - val data = ImageAsset.UserAvatarAsset(mockk(), someUserAssetId) + val data = ImageAsset.UserAvatarAsset(someUserAssetId) val (arrangement, assetImageFetcher) = Arrangement() .withErrorResponse( data = data, @@ -169,7 +169,7 @@ internal class AssetImageFetcherTest { runTest { // Given val someUserAssetId = AssetId("value", "domain") - val data = ImageAsset.UserAvatarAsset(mockk(), someUserAssetId) + val data = ImageAsset.UserAvatarAsset(someUserAssetId) val (arrangement, assetImageFetcher) = Arrangement() .withErrorResponse( data = data, @@ -191,7 +191,7 @@ internal class AssetImageFetcherTest { runTest { // Given val someUserAssetId = AssetId("value", "domain") - val data = ImageAsset.UserAvatarAsset(mockk(), someUserAssetId) + val data = ImageAsset.UserAvatarAsset(someUserAssetId) val (arrangement, assetImageFetcher) = Arrangement() .withErrorResponse( data = data, @@ -214,7 +214,7 @@ internal class AssetImageFetcherTest { // Given val someConversationId = ConversationId("some-value", "some-domain") val someMessageId = "some-message-id" - val data = ImageAsset.PrivateAsset(mockk(), someConversationId, someMessageId, true) + val data = ImageAsset.PrivateAsset(someConversationId, someMessageId, true) val (arrangement, assetImageFetcher) = Arrangement() .withErrorResponse( data = data, @@ -237,7 +237,7 @@ internal class AssetImageFetcherTest { // Given val someConversationId = ConversationId("some-value", "some-domain") val someMessageId = "some-message-id" - val data = ImageAsset.PrivateAsset(mockk(), someConversationId, someMessageId, true) + val data = ImageAsset.PrivateAsset(someConversationId, someMessageId, true) val (arrangement, assetImageFetcher) = Arrangement() .withErrorResponse( data = data, @@ -260,7 +260,7 @@ internal class AssetImageFetcherTest { // Given val someConversationId = ConversationId("some-value", "some-domain") val someMessageId = "some-message-id" - val data = ImageAsset.PrivateAsset(mockk(), someConversationId, someMessageId, true) + val data = ImageAsset.PrivateAsset(someConversationId, someMessageId, true) val (arrangement, assetImageFetcher) = Arrangement() .withErrorResponse( data = data, diff --git a/core/ui-common/build.gradle.kts b/core/ui-common/build.gradle.kts index ac41ac5e461..5f6cb899b8e 100644 --- a/core/ui-common/build.gradle.kts +++ b/core/ui-common/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id(libs.plugins.wire.android.library.get().pluginId) id(libs.plugins.wire.kover.get().pluginId) + alias(libs.plugins.kotlin.serialization) } android { @@ -12,6 +13,8 @@ dependencies { implementation(libs.androidx.core) implementation(libs.androidx.appcompat) implementation(libs.material) + implementation(libs.ktx.serialization) + implementation(libs.bundlizer.core) val composeBom = platform(libs.compose.bom) implementation(composeBom) diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetState.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetState.kt index a43ddb14322..ffcdacf1cce 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetState.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetState.kt @@ -17,6 +17,7 @@ */ package com.wire.android.ui.common.bottomsheet +import android.os.Bundle import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.SheetState import androidx.compose.material3.SheetValue @@ -31,12 +32,16 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.SoftwareKeyboardController import androidx.compose.ui.unit.Density +import dev.ahmedmourad.bundlizer.Bundlizer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.serializer +import kotlinx.serialization.serializerOrNull @OptIn(ExperimentalMaterial3Api::class) -open class WireModalSheetState internal constructor( +open class WireModalSheetState( density: Density, private val scope: CoroutineScope, private val keyboardController: SoftwareKeyboardController? = null, @@ -84,33 +89,43 @@ open class WireModalSheetState internal constructor( companion object { const val DELAY_TO_SHOW_BOTTOM_SHEET_WHEN_KEYBOARD_IS_OPEN = 300L - @Suppress("UNCHECKED_CAST") - fun saver( + @OptIn(InternalSerializationApi::class) + inline fun saver( density: Density, softwareKeyboardController: SoftwareKeyboardController?, - onDismissAction: () -> Unit, + noinline onDismissAction: () -> Unit, scope: CoroutineScope ): Saver, *> = Saver( save = { - val isExpanded = it.currentValue is WireSheetValue.Expanded - val (isValueOfTypeUnit, value) = (it.currentValue as? WireSheetValue.Expanded)?.let { - val isValueOfTypeUnit = it.value is Unit // Unit cannot be saved into Bundle, need to handle it separately - val value = if (isValueOfTypeUnit) null else it.value - isValueOfTypeUnit to value - } ?: (false to null) - listOf(isExpanded, isValueOfTypeUnit, value) + when (it.currentValue) { + is WireSheetValue.Hidden -> listOf(false) // hidden + is WireSheetValue.Expanded -> { + val value = (it.currentValue as WireSheetValue.Expanded).value + when { + value is Unit -> // expanded and with Unit value + listOf(true, SavedType.Unit) + + canBeSaved(value) -> // expanded and non-Unit value that can be saved normally + listOf(true, SavedType.Regular, value) + + T::class.serializerOrNull() != null -> // expanded and with non-Unit value that can be serialized + listOf(true, SavedType.SerializedBundle, Bundlizer.bundle(T::class.serializer(), value)) + + else -> listOf(false) // hidden because value cannot be saved + } + } + } }, restore = { savedValue -> val isExpanded = savedValue[0] as Boolean val sheetValue = when (isExpanded) { - true -> { - val isValueOfTypeUnit = savedValue[1] as Boolean - if (isValueOfTypeUnit) { - WireSheetValue.Expanded(Unit as T) - } else { - val value = savedValue[2] as T - WireSheetValue.Expanded(value) - } + true -> when (savedValue[1] as SavedType) { + SavedType.Unit -> WireSheetValue.Expanded(Unit as T) + + SavedType.Regular -> WireSheetValue.Expanded(savedValue[2] as T) + + SavedType.SerializedBundle -> + WireSheetValue.Expanded(Bundlizer.unbundle(T::class.serializer(), savedValue[2] as Bundle)) } false -> WireSheetValue.Hidden @@ -121,6 +136,8 @@ open class WireModalSheetState internal constructor( } } +enum class SavedType { Unit, Regular, SerializedBundle } + @OptIn(ExperimentalMaterial3Api::class) sealed class WireSheetValue(val originalValue: SheetValue) { data object Hidden : WireSheetValue(SheetValue.Hidden) @@ -135,9 +152,9 @@ sealed class WireSheetValue(val originalValue: SheetValue) { * @param onDismissAction The action to be executed when the sheet is dismissed. */ @Composable -fun rememberWireModalSheetState( +inline fun rememberWireModalSheetState( initialValue: WireSheetValue = WireSheetValue.Hidden, - onDismissAction: () -> Unit = {} + noinline onDismissAction: () -> Unit = {} ): WireModalSheetState { val softwareKeyboardController = LocalSoftwareKeyboardController.current val density = LocalDensity.current diff --git a/kalium b/kalium index a307ae3911f..76f6cd1213e 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit a307ae3911f6c62188d7b66cf55b15c9823990f0 +Subproject commit 76f6cd1213ee9d81ef756ff25dadb006c1567861 From 7720cdddcbb3db33e808c85047d76c6b4f263971 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Thu, 28 Nov 2024 14:02:21 +0100 Subject: [PATCH 11/15] chore: update kalium --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 76f6cd1213e..b60e6797ed9 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 76f6cd1213ee9d81ef756ff25dadb006c1567861 +Subproject commit b60e6797ed9ff794a3ebb402277bc513d1ba1f9d From aed7ea8212ee8ce54100db248a0dd44b4631b856 Mon Sep 17 00:00:00 2001 From: Yamil Medina Date: Fri, 29 Nov 2024 06:13:04 -0300 Subject: [PATCH 12/15] feat: support Proteus federation if MLS not supported by backend (WPB-14456) (#3668) --- .../android/di/accountScoped/SearchModule.kt | 6 +++ .../search/SearchUserViewModel.kt | 20 ++-------- .../search/SearchUserViewModelTest.kt | 40 ++++++------------- 3 files changed, 21 insertions(+), 45 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/SearchModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/SearchModule.kt index 02f80574ae6..e5bd7ef34a8 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/SearchModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/SearchModule.kt @@ -22,6 +22,7 @@ import com.wire.android.di.KaliumCoreLogic import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.search.FederatedSearchParser +import com.wire.kalium.logic.feature.search.IsFederationSearchAllowedUseCase import com.wire.kalium.logic.feature.search.SearchByHandleUseCase import com.wire.kalium.logic.feature.search.SearchScope import com.wire.kalium.logic.feature.search.SearchUsersUseCase @@ -53,4 +54,9 @@ class SearchModule { @ViewModelScoped @Provides fun provideFederatedSearchParser(searchScope: SearchScope): FederatedSearchParser = searchScope.federatedSearchParser + + @ViewModelScoped + @Provides + fun provideIsFederationSearchAllowedUseCase(searchScope: SearchScope): IsFederationSearchAllowedUseCase = + searchScope.isFederationSearchAllowedUseCase } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModel.kt index d40a68298d4..e7a87b34957 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModel.kt @@ -27,16 +27,13 @@ import com.wire.android.mapper.ContactMapper import com.wire.android.ui.home.newconversation.model.Contact import com.wire.android.ui.navArgs import com.wire.android.util.EMPTY -import com.wire.kalium.logic.data.conversation.Conversation -import com.wire.kalium.logic.data.user.SupportedProtocol import com.wire.kalium.logic.feature.auth.ValidateUserHandleResult import com.wire.kalium.logic.feature.auth.ValidateUserHandleUseCase -import com.wire.kalium.logic.feature.conversation.GetConversationProtocolInfoUseCase import com.wire.kalium.logic.feature.search.FederatedSearchParser +import com.wire.kalium.logic.feature.search.IsFederationSearchAllowedUseCase import com.wire.kalium.logic.feature.search.SearchByHandleUseCase import com.wire.kalium.logic.feature.search.SearchUserResult import com.wire.kalium.logic.feature.search.SearchUsersUseCase -import com.wire.kalium.logic.feature.user.GetDefaultProtocolUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableSet @@ -59,8 +56,7 @@ class SearchUserViewModel @Inject constructor( private val contactMapper: ContactMapper, private val federatedSearchParser: FederatedSearchParser, private val validateUserHandle: ValidateUserHandleUseCase, - private val getDefaultProtocol: GetDefaultProtocolUseCase, - private val getConversationProtocolInfo: GetConversationProtocolInfoUseCase, + private val isFederationSearchAllowed: IsFederationSearchAllowedUseCase, savedStateHandle: SavedStateHandle ) : ViewModel() { @@ -78,17 +74,7 @@ class SearchUserViewModel @Inject constructor( init { viewModelScope.launch { - val isProteusTeam = getDefaultProtocol() == SupportedProtocol.PROTEUS - - val isOtherDomainAllowed: Boolean = addMembersSearchNavArgs?.conversationId?.let { conversationId -> - when (val result = getConversationProtocolInfo(conversationId)) { - is GetConversationProtocolInfoUseCase.Result.Failure -> !isProteusTeam - - is GetConversationProtocolInfoUseCase.Result.Success -> - !isProteusTeam && result.protocolInfo !is Conversation.ProtocolInfo.Proteus - } - } ?: !isProteusTeam - + val isOtherDomainAllowed = isFederationSearchAllowed(addMembersSearchNavArgs?.conversationId) state = state.copy(isOtherDomainAllowed = isOtherDomainAllowed) } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModelTest.kt index f3bf3afed96..b3e2b1d1d4e 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModelTest.kt @@ -33,17 +33,15 @@ import com.wire.kalium.logic.data.id.GroupID import com.wire.kalium.logic.data.mls.CipherSuite import com.wire.kalium.logic.data.publicuser.model.UserSearchDetails import com.wire.kalium.logic.data.user.ConnectionState -import com.wire.kalium.logic.data.user.SupportedProtocol import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.type.UserType import com.wire.kalium.logic.feature.auth.ValidateUserHandleResult import com.wire.kalium.logic.feature.auth.ValidateUserHandleUseCase -import com.wire.kalium.logic.feature.conversation.GetConversationProtocolInfoUseCase import com.wire.kalium.logic.feature.search.FederatedSearchParser +import com.wire.kalium.logic.feature.search.IsFederationSearchAllowedUseCase import com.wire.kalium.logic.feature.search.SearchByHandleUseCase import com.wire.kalium.logic.feature.search.SearchUserResult import com.wire.kalium.logic.feature.search.SearchUsersUseCase -import com.wire.kalium.logic.feature.user.GetDefaultProtocolUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify @@ -138,10 +136,9 @@ class SearchUserViewModelTest { fun `given Proteus conversation and MLS team, when calling the searchUseCase, then otherDomain is not allowed`() = runTest { val conversationId = ConversationId("id", "domain") - val (arrangement, viewModel) = Arrangement() + val (_, viewModel) = Arrangement() .withAddMembersSearchNavArgs(AddMembersSearchNavArgs(conversationId, true)) - .withConversationProtocolInfo(GetConversationProtocolInfoUseCase.Result.Success(Conversation.ProtocolInfo.Proteus)) - .withDefaultProtocol(SupportedProtocol.MLS) + .withIsFederationSearchAllowedResult(false) .withIsValidHandleResult(ValidateUserHandleResult.Valid("")) .withFederatedSearchParserResult( FederatedSearchParser.Result( @@ -164,10 +161,9 @@ class SearchUserViewModelTest { fun `given MLS conversation and Proteus team, when calling the searchUseCase, then otherDomain is not allowed`() = runTest { val conversationId = ConversationId("id", "domain") - val (arrangement, viewModel) = Arrangement() + val (_, viewModel) = Arrangement() .withAddMembersSearchNavArgs(AddMembersSearchNavArgs(conversationId, true)) - .withConversationProtocolInfo(GetConversationProtocolInfoUseCase.Result.Success(mlsProtocol)) - .withDefaultProtocol(SupportedProtocol.PROTEUS) + .withIsFederationSearchAllowedResult(false) .withIsValidHandleResult(ValidateUserHandleResult.Valid("")) .withFederatedSearchParserResult( FederatedSearchParser.Result( @@ -190,10 +186,9 @@ class SearchUserViewModelTest { fun `given MLS conversation and MLS team, when calling the searchUseCase, then otherDomain is allowed`() = runTest { val conversationId = ConversationId("id", "domain") - val (arrangement, viewModel) = Arrangement() + val (_, viewModel) = Arrangement() .withAddMembersSearchNavArgs(AddMembersSearchNavArgs(conversationId, true)) - .withConversationProtocolInfo(GetConversationProtocolInfoUseCase.Result.Success(mlsProtocol)) - .withDefaultProtocol(SupportedProtocol.MLS) + .withIsFederationSearchAllowedResult(true) .withIsValidHandleResult(ValidateUserHandleResult.Valid("")) .withFederatedSearchParserResult( FederatedSearchParser.Result( @@ -360,10 +355,7 @@ class SearchUserViewModelTest { lateinit var searchByHandleUseCase: SearchByHandleUseCase @MockK - lateinit var getDefaultProtocolUseCase: GetDefaultProtocolUseCase - - @MockK - lateinit var getConversationProtocolInfo: GetConversationProtocolInfoUseCase + lateinit var isFederationSearchAllowedUseCase: IsFederationSearchAllowedUseCase init { MockKAnnotations.init(this, relaxUnitFun = true) @@ -371,10 +363,7 @@ class SearchUserViewModelTest { val user = args.get(0) as UserSearchDetails fromSearchUserResult(user) } - every { getDefaultProtocolUseCase() } returns SupportedProtocol.PROTEUS - coEvery { - getConversationProtocolInfo(any()) - } returns GetConversationProtocolInfoUseCase.Result.Success(Conversation.ProtocolInfo.Proteus) + withIsFederationSearchAllowedResult(false) } fun fromSearchUserResult(user: UserSearchDetails): Contact { @@ -427,12 +416,8 @@ class SearchUserViewModelTest { coEvery { searchByHandleUseCase(any(), any(), any()) } returns result } - suspend fun withConversationProtocolInfo(result: GetConversationProtocolInfoUseCase.Result) = apply { - coEvery { getConversationProtocolInfo(any()) } returns result - } - - fun withDefaultProtocol(protocol: SupportedProtocol) = apply { - every { getDefaultProtocolUseCase() } returns protocol + fun withIsFederationSearchAllowedResult(isAllowed: Boolean = true) = apply { + coEvery { isFederationSearchAllowedUseCase(any()) } returns isAllowed } private lateinit var searchUserViewModel: SearchUserViewModel @@ -444,8 +429,7 @@ class SearchUserViewModelTest { contactMapper = contactMapper, federatedSearchParser = federatedSearchParser, validateUserHandle = validateUserHandle, - getConversationProtocolInfo = getConversationProtocolInfo, - getDefaultProtocol = getDefaultProtocolUseCase, + isFederationSearchAllowed = isFederationSearchAllowedUseCase, savedStateHandle = savedStateHandle ) }.run { From 1e91d4681dfe4a6f5e033ddab517a6dd0c8df725 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Mon, 2 Dec 2024 14:43:10 +0100 Subject: [PATCH 13/15] chore: enable cc proteus [WPB-14658] (#3682) --- default.json | 1 + 1 file changed, 1 insertion(+) diff --git a/default.json b/default.json index 6463e8ff2d5..509de4d03fd 100644 --- a/default.json +++ b/default.json @@ -8,6 +8,7 @@ "development_api_enabled": false, "mls_support_enabled": false, "analytics_enabled": true, + "encrypt_proteus_storage": true, "analytics_app_key": "4483f7a58ae3e70b3780319c4ccb5c88a037be49", "analytics_server_url": "https://countly.wire.com/" }, From 368d4f51c41ee1d76b97e6ff4b893c055d905903 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Mon, 2 Dec 2024 14:43:45 +0100 Subject: [PATCH 14/15] chore: enable paginated conversation list [WPB-14657] (#3681) --- default.json | 1 + 1 file changed, 1 insertion(+) diff --git a/default.json b/default.json index 509de4d03fd..75d210ced82 100644 --- a/default.json +++ b/default.json @@ -8,6 +8,7 @@ "development_api_enabled": false, "mls_support_enabled": false, "analytics_enabled": true, + "paginated_conversation_list_enabled": true, "encrypt_proteus_storage": true, "analytics_app_key": "4483f7a58ae3e70b3780319c4ccb5c88a037be49", "analytics_server_url": "https://countly.wire.com/" From ec33458cf67f0eb6adf9f1b662825d7dd0c42fc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= <30429749+saleniuk@users.noreply.github.com> Date: Mon, 2 Dec 2024 14:45:36 +0100 Subject: [PATCH 15/15] fix: crash when saving bottom sheet state for message with reply [WPB-14433] (#3689) --- .../com/wire/android/model/ImageAsset.kt | 1 + .../ui/home/conversations/model/UIMessage.kt | 5 +- .../conversations/model/UIQuotedMessage.kt | 15 ++- .../kotlin/com/wire/android/util/ui/UIText.kt | 6 +- core/ui-common/build.gradle.kts | 7 +- .../common/bottomsheet/WireModalSheetState.kt | 24 ++-- .../util/AnyPrimitiveAsStringSerializer.kt | 62 +++++++++++ .../wire/android/ui/common/ExampleUnitTest.kt | 5 +- .../bottomsheet/WireModalSheetStateTest.kt | 105 ++++++++++++++++++ 9 files changed, 208 insertions(+), 22 deletions(-) create mode 100644 core/ui-common/src/main/kotlin/com/wire/android/util/AnyPrimitiveAsStringSerializer.kt create mode 100644 core/ui-common/src/test/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetStateTest.kt diff --git a/app/src/main/kotlin/com/wire/android/model/ImageAsset.kt b/app/src/main/kotlin/com/wire/android/model/ImageAsset.kt index b39c78d5095..8735b109d27 100644 --- a/app/src/main/kotlin/com/wire/android/model/ImageAsset.kt +++ b/app/src/main/kotlin/com/wire/android/model/ImageAsset.kt @@ -58,6 +58,7 @@ sealed class ImageAsset { val idKey: String ) : ImageAsset() + @Serializable sealed class Remote : ImageAsset() { /** 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 76a124fc994..587f7b6fe55 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 @@ -215,8 +215,10 @@ sealed interface MessageFlowStatus { } } + @Serializable data object Delivered : MessageFlowStatus + @Serializable data class Read(val count: Long) : MessageFlowStatus } @@ -285,7 +287,8 @@ sealed interface UIMessageContent { @Serializable data object IncompleteAssetMessage : UIMessageContent - interface PartialDeliverable { + @Serializable + sealed interface PartialDeliverable { val deliveryStatus: DeliveryStatusContent } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIQuotedMessage.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIQuotedMessage.kt index 04b90d310ab..8193ee55afc 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIQuotedMessage.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIQuotedMessage.kt @@ -39,25 +39,34 @@ sealed class UIQuotedMessage { val quotedContent: Content ) : UIQuotedMessage() { + @Serializable sealed interface Content + @Serializable data class Text(val value: String) : Content + @Serializable data class GenericAsset( val assetName: String?, val assetMimeType: String ) : Content + @Serializable data class DisplayableImage( val displayable: ImageAsset.PrivateAsset ) : Content + @Serializable data class Location(val locationName: String) : Content - object AudioMessage : Content + @Serializable + data object AudioMessage : Content - object Deleted : Content - object Invalid : Content + @Serializable + data object Deleted : Content + + @Serializable + data object Invalid : Content } } diff --git a/app/src/main/kotlin/com/wire/android/util/ui/UIText.kt b/app/src/main/kotlin/com/wire/android/util/ui/UIText.kt index 14bdb4faea3..1734807af18 100644 --- a/app/src/main/kotlin/com/wire/android/util/ui/UIText.kt +++ b/app/src/main/kotlin/com/wire/android/util/ui/UIText.kt @@ -25,8 +25,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import com.wire.android.appLogger +import com.wire.android.util.AnyPrimitiveAsStringSerializer import com.wire.kalium.logic.data.message.mention.MessageMention -import com.wire.kalium.util.serialization.AnyPrimitiveValueSerializer import kotlinx.serialization.Serializable @Serializable @@ -41,14 +41,14 @@ sealed class UIText { @Serializable class StringResource( @StringRes val resId: Int, - vararg val formatArgs: @Serializable(with = AnyPrimitiveValueSerializer::class) Any + vararg val formatArgs: @Serializable(with = AnyPrimitiveAsStringSerializer::class) Any ) : UIText() @Serializable class PluralResource( @PluralsRes val resId: Int, val count: Int, - vararg val formatArgs: @Serializable(with = AnyPrimitiveValueSerializer::class) Any + vararg val formatArgs: @Serializable(with = AnyPrimitiveAsStringSerializer::class) Any ) : UIText() @Suppress("SpreadOperator") diff --git a/core/ui-common/build.gradle.kts b/core/ui-common/build.gradle.kts index 5f6cb899b8e..664adf293e8 100644 --- a/core/ui-common/build.gradle.kts +++ b/core/ui-common/build.gradle.kts @@ -2,6 +2,7 @@ plugins { id(libs.plugins.wire.android.library.get().pluginId) id(libs.plugins.wire.kover.get().pluginId) alias(libs.plugins.kotlin.serialization) + id(BuildPlugins.junit5) } android { @@ -35,7 +36,11 @@ dependencies { implementation(libs.coil.gif) implementation(libs.coil.compose) - testImplementation(libs.junit4) + testImplementation(libs.junit5.core) + testImplementation(libs.junit5.params) + testImplementation(libs.mockk.core) + testImplementation(libs.kluent.core) + testRuntimeOnly(libs.junit5.engine) androidTestImplementation(libs.androidx.test.extJunit) androidTestImplementation(libs.androidx.espresso.core) } diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetState.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetState.kt index ffcdacf1cce..624601e3807 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetState.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetState.kt @@ -24,9 +24,9 @@ import androidx.compose.material3.SheetValue import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.Saver -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalSoftwareKeyboardController @@ -89,13 +89,14 @@ open class WireModalSheetState( companion object { const val DELAY_TO_SHOW_BOTTOM_SHEET_WHEN_KEYBOARD_IS_OPEN = 300L + @Suppress("TooGenericExceptionCaught") @OptIn(InternalSerializationApi::class) inline fun saver( density: Density, softwareKeyboardController: SoftwareKeyboardController?, noinline onDismissAction: () -> Unit, scope: CoroutineScope - ): Saver, *> = Saver( + ): Saver, List> = Saver( save = { when (it.currentValue) { is WireSheetValue.Hidden -> listOf(false) // hidden @@ -109,7 +110,13 @@ open class WireModalSheetState( listOf(true, SavedType.Regular, value) T::class.serializerOrNull() != null -> // expanded and with non-Unit value that can be serialized - listOf(true, SavedType.SerializedBundle, Bundlizer.bundle(T::class.serializer(), value)) + try { + val serializedBundleValue = Bundlizer.bundle(T::class.serializer(), value) + listOf(true, SavedType.SerializedBundle, serializedBundleValue) + } catch (e: Exception) { + e.printStackTrace() + listOf(false) // hidden because value cannot be serialized properly + } else -> listOf(false) // hidden because value cannot be saved } @@ -159,14 +166,9 @@ inline fun rememberWireModalSheetState( val softwareKeyboardController = LocalSoftwareKeyboardController.current val density = LocalDensity.current val scope = rememberCoroutineScope() - return rememberSaveable( - saver = WireModalSheetState.saver( - density = density, - softwareKeyboardController = softwareKeyboardController, - onDismissAction = onDismissAction, - scope = scope - ) - ) { + // TODO: we can use rememberSaveable instead of remember to save the state but first we need to make sure that we don't store too much, + // especially for conversations and messages to not keep such data unencrypted anywhere + return remember { WireModalSheetState( density = density, scope = scope, diff --git a/core/ui-common/src/main/kotlin/com/wire/android/util/AnyPrimitiveAsStringSerializer.kt b/core/ui-common/src/main/kotlin/com/wire/android/util/AnyPrimitiveAsStringSerializer.kt new file mode 100644 index 00000000000..60275a95b59 --- /dev/null +++ b/core/ui-common/src/main/kotlin/com/wire/android/util/AnyPrimitiveAsStringSerializer.kt @@ -0,0 +1,62 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.util + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +object AnyPrimitiveAsStringSerializer : KSerializer { + override val descriptor: SerialDescriptor = AnyPrimitiveSurrogate.serializer().descriptor + override fun serialize(encoder: Encoder, value: Any) = + encoder.encodeSerializableValue(AnyPrimitiveSurrogate.serializer(), AnyPrimitiveSurrogate(value)) + override fun deserialize(decoder: Decoder): Any = + decoder.decodeSerializableValue(AnyPrimitiveSurrogate.serializer()).value +} + +@Serializable +@SerialName("AnyPrimitive") +private data class AnyPrimitiveSurrogate(private val kind: AnyPrimitiveKind, private val stringValue: String) { + constructor(value: Any) : this( + kind = when (value) { + is String -> AnyPrimitiveKind.STRING + is Int -> AnyPrimitiveKind.INT + is Long -> AnyPrimitiveKind.LONG + is Float -> AnyPrimitiveKind.FLOAT + is Double -> AnyPrimitiveKind.DOUBLE + is Boolean -> AnyPrimitiveKind.BOOLEAN + else -> throw IllegalArgumentException("Unsupported type: ${value::class}") + }, + stringValue = value.toString() + ) + + val value: Any + get() = when (kind) { + AnyPrimitiveKind.STRING -> stringValue + AnyPrimitiveKind.INT -> stringValue.toInt() + AnyPrimitiveKind.LONG -> stringValue.toLong() + AnyPrimitiveKind.FLOAT -> stringValue.toFloat() + AnyPrimitiveKind.DOUBLE -> stringValue.toDouble() + AnyPrimitiveKind.BOOLEAN -> stringValue.toBoolean() + } +} + +private enum class AnyPrimitiveKind { STRING, INT, LONG, FLOAT, DOUBLE, BOOLEAN } diff --git a/core/ui-common/src/test/kotlin/com/wire/android/ui/common/ExampleUnitTest.kt b/core/ui-common/src/test/kotlin/com/wire/android/ui/common/ExampleUnitTest.kt index b0896ca1a45..09521343967 100644 --- a/core/ui-common/src/test/kotlin/com/wire/android/ui/common/ExampleUnitTest.kt +++ b/core/ui-common/src/test/kotlin/com/wire/android/ui/common/ExampleUnitTest.kt @@ -17,9 +17,8 @@ */ package com.wire.android.ui.common -import org.junit.Test - -import org.junit.Assert.* +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test /** * Example local unit test, which will execute on the development machine (host). diff --git a/core/ui-common/src/test/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetStateTest.kt b/core/ui-common/src/test/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetStateTest.kt new file mode 100644 index 00000000000..8b2cd8de657 --- /dev/null +++ b/core/ui-common/src/test/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetStateTest.kt @@ -0,0 +1,105 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.common.bottomsheet + +import androidx.compose.runtime.saveable.SaverScope +import com.wire.android.util.AnyPrimitiveAsStringSerializer +import io.mockk.mockk +import kotlinx.serialization.Serializable +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertInstanceOf + +class WireModalSheetStateTest { + + @Suppress("LongParameterList") + @Serializable + class SerializableTestModel( + val boolean: Boolean, + val int: Int, + val long: Long, + val float: Float, + val double: Double, + val char: Char, + val string: String, + val nullable: String?, + val list: List, + vararg val any: @Serializable(with = AnyPrimitiveAsStringSerializer::class) Any + ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is SerializableTestModel) return false + + if (boolean != other.boolean) return false + if (int != other.int) return false + if (long != other.long) return false + if (float != other.float) return false + if (double != other.double) return false + if (char != other.char) return false + if (string != other.string) return false + if (nullable != other.nullable) return false + if (list != other.list) return false + if (!any.contentEquals(other.any)) return false + + return true + } + + override fun hashCode(): Int { + var result = boolean.hashCode() + result = 31 * result + int + result = 31 * result + long.hashCode() + result = 31 * result + float.hashCode() + result = 31 * result + double.hashCode() + result = 31 * result + char.hashCode() + result = 31 * result + string.hashCode() + result = 31 * result + (nullable?.hashCode() ?: 0) + result = 31 * result + list.hashCode() + result = 31 * result + any.contentHashCode() + return result + } + } + + @Test + fun givenSerializableModel_whenSavingState_thenStateIsSavedAndRestoredProperly() { + // given + val model = SerializableTestModel( + boolean = true, + int = 1, + long = 2L, + float = 3.0f, + double = 4.0, + char = 'c', + string = "string", + nullable = null, + list = listOf("a", "b", "c"), + any = arrayOf(1, 2L, 3.0, 4f, true, false, 'c', "string") + ) + val sheetValue = WireSheetValue.Expanded(model) + with(WireModalSheetState.saver(mockk(), mockk(), mockk(), mockk())) { + // when + val saved = SaverScope { true }.save(WireModalSheetState(mockk(), mockk(), mockk(), mockk(), sheetValue)) + // then + assertInstanceOf>(saved).let { + val restored = restore(it) + assertInstanceOf>(restored?.currentValue).let { + assertEquals(sheetValue.value, it.value) + } + } + } + } +}