From 6010a0f3e8d9f98e3dda3445e00c859f5ac3375b Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Fri, 9 Aug 2024 16:13:12 +0200 Subject: [PATCH 01/25] feat: add progress avatar --- .../android/ui/common/UserProfileAvatar.kt | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) 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 0cee86784c6..7f75b10896a 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 @@ -18,13 +18,16 @@ package com.wire.android.ui.common +import android.annotation.SuppressLint import androidx.compose.foundation.Image import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -97,6 +100,7 @@ fun UserProfileAvatar( // indicator borders need to be taken into account, the avatar itself will be smaller by the borders widths size + (max(dimensions().avatarStatusBorderSize, dimensions().avatarLegalHoldIndicatorBorderSize) * 2) } + UserProfileAvatarType.WithoutIndicators -> { // indicator borders don't need to be taken into account, the avatar itself will take all available space size @@ -246,3 +250,52 @@ fun PreviewUserProfileAvatarWithoutIndicators() { ) } } + +@PreviewMultipleThemes +@Composable +fun PreviewUserCustomIndicators() { + WireTheme { + ImageWithProgressBorder( + painter = getDefaultAvatar(Membership.Guest), + size = 48.dp, + padding = 0.dp, + progress = 0.85f + ) + } +} + +@SuppressLint("ComposeModifierMissing") +@Composable +fun ImageWithProgressBorder( + painter: Painter, + progress: Float, + size: Dp = MaterialTheme.wireDimensions.avatarDefaultSize, + padding: Dp = MaterialTheme.wireDimensions.avatarClickablePadding, +) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.size(size) + ) { + Image( + painter = painter, + contentDescription = null, + modifier = Modifier + .fillMaxSize() + .clip(CircleShape) + ) + + // Draw a circular progress indicator border + CircularProgressIndicator( + progress = progress, + modifier = Modifier + .size(size) // Adjust the size to match the image size + .border( + width = dimensions().spacing1x, + color = colorsScheme().outline, + shape = CircleShape + ) + .padding(padding) // Padding to adjust the border thickness + .clip(CircleShape) + ) + } +} From 9e9d547421e65df7ba40796c1596f0823d98d982 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Fri, 9 Aug 2024 16:21:03 +0200 Subject: [PATCH 02/25] feat: add progress avatar border adjustments --- .../com/wire/android/ui/common/UserProfileAvatar.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) 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 7f75b10896a..bcacf554ca6 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 @@ -48,6 +48,7 @@ import com.wire.android.R import com.wire.android.model.Clickable import com.wire.android.model.UserAvatarData import com.wire.android.ui.home.conversationslist.model.Membership +import com.wire.android.ui.theme.Accent import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.util.ui.PreviewMultipleThemes @@ -287,13 +288,10 @@ fun ImageWithProgressBorder( // Draw a circular progress indicator border CircularProgressIndicator( progress = progress, + color = colorsScheme().wireAccentColors.getOrDefault(Accent.Blue, Color.Transparent), + strokeWidth = dimensions().spacing2x, modifier = Modifier .size(size) // Adjust the size to match the image size - .border( - width = dimensions().spacing1x, - color = colorsScheme().outline, - shape = CircleShape - ) .padding(padding) // Padding to adjust the border thickness .clip(CircleShape) ) From 74164650bf8c7d25e4a37b3c1f08c989bf738f41 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Fri, 9 Aug 2024 17:28:09 +0200 Subject: [PATCH 03/25] feat: add progress avatar border adjustments mappigs --- .../com/wire/android/mapper/ContactMapper.kt | 1 + .../android/ui/common/UserProfileAvatar.kt | 35 +++++++-- .../ConversationParticipantItem.kt | 78 +++++++++++++++++-- .../search/HighLightSubtTitle.kt | 10 +-- .../ui/userprofile/common/UserProfileInfo.kt | 44 ++++++++++- .../ui/userprofile/common/UsernameMapper.kt | 12 ++- .../other/OtherUserProfileScreen.kt | 1 + .../userprofile/self/SelfUserProfileScreen.kt | 2 +- app/src/main/res/values/strings.xml | 1 + 9 files changed, 161 insertions(+), 23 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 8d16c5e3557..4e11429f009 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/ContactMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/ContactMapper.kt @@ -89,6 +89,7 @@ class ContactMapper /** * Adds the fully qualified handle to the contact label in case of federated users. + * todo: is this the right place for this logic?, or can be moved to usernamemapper / check for guests if needed. */ private fun mapUserHandle(user: UserSearchDetails): String { return when (user.type) { 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 bcacf554ca6..5a38dd15c02 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 @@ -54,7 +54,14 @@ import com.wire.android.ui.theme.wireDimensions import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.UserAvailabilityStatus +import kotlinx.datetime.Clock +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.Instant +import kotlinx.datetime.minus +import kotlinx.datetime.until import kotlin.math.sqrt +import kotlin.time.Duration.Companion.hours +import kotlin.time.DurationUnit /** * @param avatarData data for the avatar @@ -102,10 +109,14 @@ fun UserProfileAvatar( size + (max(dimensions().avatarStatusBorderSize, dimensions().avatarLegalHoldIndicatorBorderSize) * 2) } - UserProfileAvatarType.WithoutIndicators -> { + is UserProfileAvatarType.WithoutIndicators -> { // indicator borders don't need to be taken into account, the avatar itself will take all available space size } + + is UserProfileAvatarType.WithTemporaryUserIndicator -> { + size + } } ) .let { @@ -154,6 +165,17 @@ fun UserProfileAvatar( .align(Alignment.BottomEnd) ) } + if (type is UserProfileAvatarType.WithTemporaryUserIndicator) { + CircularProgressIndicator( + progress = 0.3f, // todo calculate percentage of time left + color = colorsScheme().wireAccentColors.getOrDefault(Accent.Blue, Color.Transparent), + strokeWidth = dimensions().spacing2x, + modifier = Modifier + .size(size) // Adjust the size to match the image size + .padding(padding) // Padding to adjust the border thickness + .clip(CircleShape) + ) + } } } @@ -164,6 +186,9 @@ sealed class UserProfileAvatarType { // this will not take the indicators into account when calculating avatar size so the avatar itself will be exactly as specified size data object WithoutIndicators : UserProfileAvatarType() + + // for temporary users, the avatar will have a temporary border around it with the countdown. + data class WithTemporaryUserIndicator(val expiresAt: Instant) : UserProfileAvatarType() } /** @@ -256,11 +281,11 @@ fun PreviewUserProfileAvatarWithoutIndicators() { @Composable fun PreviewUserCustomIndicators() { WireTheme { - ImageWithProgressBorder( - painter = getDefaultAvatar(Membership.Guest), - size = 48.dp, + UserProfileAvatar( + avatarData = UserAvatarData(), padding = 0.dp, - progress = 0.85f + size = 48.dp, + type = UserProfileAvatarType.WithTemporaryUserIndicator(expiresAt = Clock.System.now().plus(23.hours)), ) } } 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 c0ab441babf..b021613fa1a 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 @@ -54,6 +54,10 @@ import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.android.util.uiReadReceiptDateTime import com.wire.kalium.logic.data.user.SupportedProtocol import com.wire.kalium.logic.data.user.UserId +import kotlinx.datetime.Clock +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.plus +import kotlin.time.DurationUnit @Composable fun ConversationParticipantItem( @@ -116,13 +120,9 @@ fun ConversationParticipantItem( }, subtitle = { HighlightSubtitle( - subTitle = if (uiParticipant.unavailable) { - uiParticipant.id.domain - } else uiParticipant.readReceiptDate?.let { - it.uiReadReceiptDateTime() - } ?: uiParticipant.handle, + subTitle = Username(uiParticipant), searchQuery = searchQuery, - suffix = uiParticipant.readReceiptDate?.let { "" } ?: "@" + prefix = UsernamePrefix(uiParticipant) ) }, actions = { @@ -140,6 +140,24 @@ fun ConversationParticipantItem( ) } +@Composable +private fun UsernamePrefix(uiParticipant: UIParticipant) = when { + uiParticipant.readReceiptDate != null || uiParticipant.expiresAt != null -> "" + else -> "@" +} + +@Composable +private fun Username(uiParticipant: UIParticipant) = when { + uiParticipant.unavailable -> uiParticipant.id.domain + uiParticipant.readReceiptDate != null -> uiParticipant.readReceiptDate.uiReadReceiptDateTime() + uiParticipant.expiresAt != null -> { + val expiresAtString = uiParticipant.expiresAt.minus(Clock.System.now()).toString(DurationUnit.HOURS) + stringResource(R.string.temporary_user_label, expiresAtString) + } + + else -> uiParticipant.handle +} + @PreviewMultipleThemes @Composable fun PreviewGroupConversationParticipantItem() { @@ -162,3 +180,51 @@ fun PreviewGroupConversationParticipantItem() { ) } } + +@PreviewMultipleThemes +@Composable +fun PreviewGroupConversationTemporaryParticipantItem() { + WireTheme { + ConversationParticipantItem( + UIParticipant( + UserId("0", ""), + "name", + "handle", + false, + false, + UserAvatarData(), + Membership.Guest, + isMLSVerified = true, + isProteusVerified = true, + isUnderLegalHold = true, + supportedProtocolList = listOf(SupportedProtocol.PROTEUS, SupportedProtocol.MLS), + expiresAt = Clock.System.now().plus(23, DateTimeUnit.HOUR) + ), + clickable = Clickable(enabled = true) {} + ) + } +} + +@PreviewMultipleThemes +@Composable +fun PreviewGroupConversationReadReceiptItem() { + WireTheme { + ConversationParticipantItem( + UIParticipant( + UserId("0", ""), + "name", + "handle", + false, + false, + UserAvatarData(), + Membership.Guest, + isMLSVerified = true, + isProteusVerified = true, + isUnderLegalHold = true, + supportedProtocolList = listOf(SupportedProtocol.PROTEUS, SupportedProtocol.MLS), + readReceiptDate = Clock.System.now() + ), + clickable = Clickable(enabled = true) {} + ) + } +} 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 21edf700ffd..1909cbda730 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 @@ -34,7 +34,7 @@ import com.wire.android.util.QueryMatchExtractor fun HighlightSubtitle( subTitle: String, searchQuery: String = String.EMPTY, - suffix: String = "@" + prefix: String = "@" ) { if (subTitle.isBlank()) { return @@ -59,7 +59,7 @@ fun HighlightSubtitle( fontStyle = MaterialTheme.wireTypography.subline01.fontStyle ) ) { - append("$suffix$subTitle") + append("$prefix$subTitle") } highlightIndexes @@ -70,8 +70,8 @@ fun HighlightSubtitle( background = MaterialTheme.wireColorScheme.highlight, color = MaterialTheme.wireColorScheme.onHighlight, ), - start = highLightIndex.startIndex + suffix.length, - end = highLightIndex.endIndex + suffix.length + start = highLightIndex.startIndex + prefix.length, + end = highLightIndex.endIndex + prefix.length ) } } @@ -81,7 +81,7 @@ fun HighlightSubtitle( ) } else { Text( - text = "$suffix$subTitle", + text = "$prefix$subTitle", style = MaterialTheme.wireTypography.subline01, color = MaterialTheme.wireColorScheme.secondaryText, maxLines = 1, diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt index d96abf4afcd..beebcc5e690 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt @@ -18,6 +18,7 @@ package com.wire.android.ui.userprofile.common +import android.annotation.SuppressLint import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.Crossfade import androidx.compose.animation.animateContentSize @@ -46,7 +47,6 @@ import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview import androidx.constraintlayout.compose.ConstraintLayout import com.wire.android.R import com.wire.android.model.ClickBlockParams @@ -58,6 +58,8 @@ import com.wire.android.ui.common.MLSVerifiedIcon import com.wire.android.ui.common.ProteusVerifiedIcon import com.wire.android.ui.common.UserBadge import com.wire.android.ui.common.UserProfileAvatar +import com.wire.android.ui.common.UserProfileAvatarType +import com.wire.android.ui.common.UserProfileAvatarType.* import com.wire.android.ui.common.banner.SecurityClassificationBannerForUser import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.progress.WireCircularProgressIndicator @@ -66,11 +68,15 @@ import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireTypography import com.wire.android.util.debug.LocalFeatureVisibilityFlags import com.wire.android.util.ifNotEmpty +import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.android.util.ui.UIText import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.UserId import kotlinx.coroutines.delay +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant import kotlin.time.Duration +import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.milliseconds @Composable @@ -89,6 +95,7 @@ fun UserProfileInfo( delayToShowPlaceholderIfNoAsset: Duration = 200.milliseconds, isProteusVerified: Boolean = false, isMLSVerified: Boolean = false, + expiresAt: Instant? = null ) { Column( horizontalAlignment = CenterHorizontally, @@ -127,6 +134,7 @@ fun UserProfileInfo( }, showPlaceholderIfNoAsset = showPlaceholderIfNoAsset, withCrossfadeAnimation = true, + type = if (expiresAt != null) WithTemporaryUserIndicator(expiresAt) else WithoutIndicators ) } this@Column.AnimatedVisibility(visible = isLoading) { @@ -182,7 +190,7 @@ fun UserProfileInfo( if (isProteusVerified) ProteusVerifiedIcon() } Text( - text = if (membership == Membership.Service) userName else userName.ifNotEmpty { "@$userName" }, + text = Username(userName, membership, expiresAt), overflow = TextOverflow.Ellipsis, style = MaterialTheme.wireTypography.body02, maxLines = 1, @@ -231,6 +239,16 @@ fun UserProfileInfo( } } +@SuppressLint("ComposeNamingLowercase") +@Composable +private fun Username(userName: String, membership: Membership, expiresAt: Instant?): String { + return when { + expiresAt != null -> UIText.StringResource(R.string.temporary_user_label, userName).asString() + membership == Membership.Service -> userName + else -> userName.ifNotEmpty { "@$userName" } + } +} + @Composable private fun ManageMemberButton(modifier: Modifier, onEditClick: () -> Unit) { IconButton( @@ -257,7 +275,7 @@ sealed class EditableState { class IsEditable(val onEditClick: () -> Unit) : EditableState() } -@Preview +@PreviewMultipleThemes @Composable fun PreviewUserProfileInfo() { UserProfileInfo( @@ -274,3 +292,23 @@ fun PreviewUserProfileInfo() { isMLSVerified = true ) } + + +@PreviewMultipleThemes +@Composable +fun PreviewUserProfileInfoTempUser() { + UserProfileInfo( + userId = UserId("value", "domain"), + isLoading = true, + editableState = EditableState.IsEditable {}, + userName = "23h", + avatarAsset = null, + fullName = "fullName", + onUserProfileClick = {}, + teamName = "Wire", + connection = ConnectionState.ACCEPTED, + isProteusVerified = true, + isMLSVerified = true, + expiresAt = Clock.System.now().plus(23.hours) + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UsernameMapper.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UsernameMapper.kt index b46925b1c1f..a16a157fa8b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UsernameMapper.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UsernameMapper.kt @@ -21,6 +21,8 @@ package com.wire.android.ui.userprofile.common import com.wire.android.util.ifNotEmpty import com.wire.kalium.logic.data.user.OtherUser import com.wire.kalium.logic.data.user.type.UserType +import kotlinx.datetime.Clock +import kotlin.time.DurationUnit object UsernameMapper { @@ -29,9 +31,13 @@ object UsernameMapper { * The username is the handle if it exists, otherwise it is the handle@domain for federated users. */ fun fromOtherUser(otherUser: OtherUser): String = with(otherUser) { - val userId = otherUser.id - return when (otherUser.userType) { - UserType.FEDERATED -> handle?.ifNotEmpty { "$handle@${userId.domain}" }.orEmpty() + return when { + userType == UserType.FEDERATED -> handle?.ifNotEmpty { "$handle@${id.domain}" }.orEmpty() + expiresAt != null -> { + val expiresAtString = expiresAt!!.minus(Clock.System.now()).toString(DurationUnit.HOURS) + expiresAtString + } + else -> handle.orEmpty() } } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt index eff96924201..f5df2268980 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt @@ -424,6 +424,7 @@ private fun TopBarCollapsing( connection = targetState.connectionState, isProteusVerified = targetState.isProteusVerified, isMLSVerified = targetState.isMLSVerified, + expiresAt = targetState.expiresAt ) if (state.isUnderLegalHold) { LegalHoldSubjectBanner( 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 7ae6b218993..e34e195ddd8 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 @@ -426,7 +426,7 @@ private fun OtherAccountItem( }, subtitle = { if (account.teamName != null) { - HighlightSubtitle(subTitle = account.teamName, suffix = "") + HighlightSubtitle(subTitle = account.teamName, prefix = "") } }, actions = { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 07cfc629dad..b991f33b52a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -454,6 +454,7 @@ you Add Participants Name not available + %s left Group name not available Conversation Details OPTIONS From de686296a8d5682d2828229f765a804b2801ea74 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Fri, 9 Aug 2024 17:31:28 +0200 Subject: [PATCH 04/25] feat: add progress avatar border adjustments mappigs --- .../android/ui/common/UserProfileAvatar.kt | 43 +------------------ 1 file changed, 2 insertions(+), 41 deletions(-) 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 5a38dd15c02..c51093cc3fa 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 @@ -18,11 +18,9 @@ package com.wire.android.ui.common -import android.annotation.SuppressLint import androidx.compose.foundation.Image import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize @@ -55,13 +53,9 @@ import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.UserAvailabilityStatus import kotlinx.datetime.Clock -import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.Instant -import kotlinx.datetime.minus -import kotlinx.datetime.until import kotlin.math.sqrt import kotlin.time.Duration.Companion.hours -import kotlin.time.DurationUnit /** * @param avatarData data for the avatar @@ -167,7 +161,7 @@ fun UserProfileAvatar( } if (type is UserProfileAvatarType.WithTemporaryUserIndicator) { CircularProgressIndicator( - progress = 0.3f, // todo calculate percentage of time left + progress = type.expiresAt.minus(Clock.System.now()).inWholeHours.toFloat() / 24.000f, color = colorsScheme().wireAccentColors.getOrDefault(Accent.Blue, Color.Transparent), strokeWidth = dimensions().spacing2x, modifier = Modifier @@ -285,40 +279,7 @@ fun PreviewUserCustomIndicators() { avatarData = UserAvatarData(), padding = 0.dp, size = 48.dp, - type = UserProfileAvatarType.WithTemporaryUserIndicator(expiresAt = Clock.System.now().plus(23.hours)), - ) - } -} - -@SuppressLint("ComposeModifierMissing") -@Composable -fun ImageWithProgressBorder( - painter: Painter, - progress: Float, - size: Dp = MaterialTheme.wireDimensions.avatarDefaultSize, - padding: Dp = MaterialTheme.wireDimensions.avatarClickablePadding, -) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.size(size) - ) { - Image( - painter = painter, - contentDescription = null, - modifier = Modifier - .fillMaxSize() - .clip(CircleShape) - ) - - // Draw a circular progress indicator border - CircularProgressIndicator( - progress = progress, - color = colorsScheme().wireAccentColors.getOrDefault(Accent.Blue, Color.Transparent), - strokeWidth = dimensions().spacing2x, - modifier = Modifier - .size(size) // Adjust the size to match the image size - .padding(padding) // Padding to adjust the border thickness - .clip(CircleShape) + type = UserProfileAvatarType.WithTemporaryUserIndicator(expiresAt = Clock.System.now().plus(22.hours)), ) } } From f579855ca4a7222be50b97906591b06159727629 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Fri, 9 Aug 2024 17:34:20 +0200 Subject: [PATCH 05/25] feat: add progress avatar border adjustments mappigs cleanup --- .../kotlin/com/wire/android/ui/common/UserProfileAvatar.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 c51093cc3fa..74d087a6a5e 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 @@ -161,12 +161,12 @@ fun UserProfileAvatar( } if (type is UserProfileAvatarType.WithTemporaryUserIndicator) { CircularProgressIndicator( - progress = type.expiresAt.minus(Clock.System.now()).inWholeHours.toFloat() / 24.000f, + progress = type.expiresAt.minus(Clock.System.now()).inWholeHours.toFloat() / 24f, color = colorsScheme().wireAccentColors.getOrDefault(Accent.Blue, Color.Transparent), strokeWidth = dimensions().spacing2x, modifier = Modifier - .size(size) // Adjust the size to match the image size - .padding(padding) // Padding to adjust the border thickness + .size(size) + .padding(padding) .clip(CircleShape) ) } From 3946e9ec1ff4a3ea66caa4342c1b42352c620f02 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Mon, 12 Aug 2024 11:45:41 +0200 Subject: [PATCH 06/25] feat: design adjustment for guest users --- .../android/ui/common/UserProfileAvatar.kt | 44 +++++++++---------- .../com/wire/android/ui/home/HomeTopBar.kt | 2 +- .../ui/userprofile/common/UserProfileInfo.kt | 2 +- 3 files changed, 23 insertions(+), 25 deletions(-) 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 74d087a6a5e..a03a6df7bce 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 @@ -31,6 +31,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.ColorPainter import androidx.compose.ui.graphics.painter.Painter @@ -78,7 +79,7 @@ fun UserProfileAvatar( clickable: Clickable? = null, showPlaceholderIfNoAsset: Boolean = true, withCrossfadeAnimation: Boolean = false, - type: UserProfileAvatarType = UserProfileAvatarType.WithIndicators(legalHoldIndicatorVisible = false), + type: UserProfileAvatarType = UserProfileAvatarType.WithIndicators.LegalHold(legalHoldIndicatorVisible = false), ) { Box( contentAlignment = Alignment.Center, @@ -98,23 +99,20 @@ fun UserProfileAvatar( modifier = Modifier .size( when (type) { - is UserProfileAvatarType.WithIndicators -> { + is UserProfileAvatarType.WithIndicators.LegalHold -> { // indicator borders need to be taken into account, the avatar itself will be smaller by the borders widths size + (max(dimensions().avatarStatusBorderSize, dimensions().avatarLegalHoldIndicatorBorderSize) * 2) } + is UserProfileAvatarType.WithIndicators.TemporaryUser, is UserProfileAvatarType.WithoutIndicators -> { // indicator borders don't need to be taken into account, the avatar itself will take all available space size } - - is UserProfileAvatarType.WithTemporaryUserIndicator -> { - size - } } ) .let { - if (type is UserProfileAvatarType.WithIndicators) { + if (type is UserProfileAvatarType.WithIndicators.LegalHold) { if (type.legalHoldIndicatorVisible) { it .border( @@ -146,7 +144,7 @@ fun UserProfileAvatar( .testTag("User avatar"), contentScale = ContentScale.Crop ) - if (type is UserProfileAvatarType.WithIndicators) { + if (type is UserProfileAvatarType.WithIndicators.LegalHold) { val avatarWithLegalHoldRadius = (size.value / 2f) + dimensions().avatarLegalHoldIndicatorBorderSize.value val statusRadius = (dimensions().userAvatarStatusSize - dimensions().avatarStatusBorderSize).value / 2f // calculated using the trigonometry so that the status is always in the right place according to the avatar @@ -159,15 +157,15 @@ fun UserProfileAvatar( .align(Alignment.BottomEnd) ) } - if (type is UserProfileAvatarType.WithTemporaryUserIndicator) { + if (type is UserProfileAvatarType.WithIndicators.TemporaryUser) { CircularProgressIndicator( - progress = type.expiresAt.minus(Clock.System.now()).inWholeHours.toFloat() / 24f, + progress = (type.expiresAt.minus(Clock.System.now()).inWholeHours.toFloat() / 24f), color = colorsScheme().wireAccentColors.getOrDefault(Accent.Blue, Color.Transparent), - strokeWidth = dimensions().spacing2x, + strokeWidth = dimensions().spacing4x, modifier = Modifier .size(size) - .padding(padding) .clip(CircleShape) + .scale(scaleX = -1f, scaleY = 1f) ) } } @@ -175,14 +173,14 @@ fun UserProfileAvatar( sealed class UserProfileAvatarType { - // this will take the indicators into account when calculating avatar size so the composable itself will be larger by the borders - data class WithIndicators(val legalHoldIndicatorVisible: Boolean) : UserProfileAvatarType() + sealed class WithIndicators : UserProfileAvatarType() { + // this will take the indicators into account when calculating avatar size so the composable itself will be larger by the borders + data class LegalHold(val legalHoldIndicatorVisible: Boolean) : WithIndicators() + data class TemporaryUser(val expiresAt: Instant) : WithIndicators() + } // this will not take the indicators into account when calculating avatar size so the avatar itself will be exactly as specified size data object WithoutIndicators : UserProfileAvatarType() - - // for temporary users, the avatar will have a temporary border around it with the countdown. - data class WithTemporaryUserIndicator(val expiresAt: Instant) : UserProfileAvatarType() } /** @@ -241,7 +239,7 @@ fun PreviewUserProfileAvatarWithLegalHold() { WireTheme { UserProfileAvatar( avatarData = UserAvatarData(availabilityStatus = UserAvailabilityStatus.AVAILABLE), - type = UserProfileAvatarType.WithIndicators(legalHoldIndicatorVisible = true) + type = UserProfileAvatarType.WithIndicators.LegalHold(legalHoldIndicatorVisible = true) ) } } @@ -253,7 +251,7 @@ fun PreviewLargeUserProfileAvatarWithLegalHold() { UserProfileAvatar( avatarData = UserAvatarData(availabilityStatus = UserAvailabilityStatus.AVAILABLE), size = 48.dp, - type = UserProfileAvatarType.WithIndicators(legalHoldIndicatorVisible = true) + type = UserProfileAvatarType.WithIndicators.LegalHold(legalHoldIndicatorVisible = true) ) } } @@ -273,13 +271,13 @@ fun PreviewUserProfileAvatarWithoutIndicators() { @PreviewMultipleThemes @Composable -fun PreviewUserCustomIndicators() { +fun PreviewTempUserCustomIndicators() { WireTheme { UserProfileAvatar( avatarData = UserAvatarData(), - padding = 0.dp, - size = 48.dp, - type = UserProfileAvatarType.WithTemporaryUserIndicator(expiresAt = Clock.System.now().plus(22.hours)), + padding = 4.dp, + size = dimensions().avatarDefaultBigSize, + type = UserProfileAvatarType.WithIndicators.TemporaryUser(expiresAt = Clock.System.now().plus(22.hours)), ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/HomeTopBar.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeTopBar.kt index 17527ee843e..d14ae363d03 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/HomeTopBar.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/HomeTopBar.kt @@ -51,7 +51,7 @@ fun HomeTopBar( UserProfileAvatar( avatarData = UserAvatarData(avatarAsset, status), clickable = remember { Clickable(enabled = true) { onNavigateToSelfUserProfile() } }, - type = UserProfileAvatarType.WithIndicators(legalHoldIndicatorVisible = withLegalHoldIndicator), + type = UserProfileAvatarType.WithIndicators.LegalHold(legalHoldIndicatorVisible = withLegalHoldIndicator), ) }, elevation = elevation, diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt index beebcc5e690..15c6dfd68ea 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt @@ -134,7 +134,7 @@ fun UserProfileInfo( }, showPlaceholderIfNoAsset = showPlaceholderIfNoAsset, withCrossfadeAnimation = true, - type = if (expiresAt != null) WithTemporaryUserIndicator(expiresAt) else WithoutIndicators + type = if (expiresAt != null) WithIndicators.TemporaryUser(expiresAt) else WithoutIndicators ) } this@Column.AnimatedVisibility(visible = isLoading) { From 98200c81fddc630643e25063b26bc8aff65085c6 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Mon, 12 Aug 2024 14:56:54 +0200 Subject: [PATCH 07/25] feat: design adjustment for guest users --- .../kotlin/com/wire/android/ui/common/UserProfileAvatar.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 a03a6df7bce..cff419c3c85 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 @@ -55,6 +55,7 @@ import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.UserAvailabilityStatus import kotlinx.datetime.Clock import kotlinx.datetime.Instant +import kotlin.math.absoluteValue import kotlin.math.sqrt import kotlin.time.Duration.Companion.hours @@ -159,7 +160,7 @@ fun UserProfileAvatar( } if (type is UserProfileAvatarType.WithIndicators.TemporaryUser) { CircularProgressIndicator( - progress = (type.expiresAt.minus(Clock.System.now()).inWholeHours.toFloat() / 24f), + progress = (type.expiresAt.minus(Clock.System.now()).inWholeHours.toFloat() / 24f).absoluteValue, color = colorsScheme().wireAccentColors.getOrDefault(Accent.Blue, Color.Transparent), strokeWidth = dimensions().spacing4x, modifier = Modifier From 9a50f95acbcea204de62be7d67b7d7615acb6966 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Mon, 12 Aug 2024 16:29:04 +0200 Subject: [PATCH 08/25] feat: design adjustment for guest users --- .../android/ui/common/UserProfileAvatar.kt | 34 ++++++++++++++----- .../ConversationParticipantItem.kt | 8 +++-- 2 files changed, 32 insertions(+), 10 deletions(-) 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 cff419c3c85..9cf1ee10134 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 @@ -66,6 +66,7 @@ import kotlin.time.Duration.Companion.hours * composable will be larger than this specified size by the indicators borders widths, if padding is specified it will also be added to * the final composable size * @param padding padding around the avatar and indicator borders + * @param avatarBorderSize border of the avatar to override as base * @param clickable clickable callback for the avatar * @param showPlaceholderIfNoAsset if true, will show default avatar if asset is null * @param withCrossfadeAnimation if true, will animate the avatar change @@ -77,6 +78,7 @@ fun UserProfileAvatar( modifier: Modifier = Modifier, size: Dp = MaterialTheme.wireDimensions.avatarDefaultSize, padding: Dp = MaterialTheme.wireDimensions.avatarClickablePadding, + avatarBorderSize: Dp = MaterialTheme.wireDimensions.avatarLegalHoldIndicatorBorderSize, clickable: Clickable? = null, showPlaceholderIfNoAsset: Boolean = true, withCrossfadeAnimation: Boolean = false, @@ -102,7 +104,7 @@ fun UserProfileAvatar( when (type) { is UserProfileAvatarType.WithIndicators.LegalHold -> { // indicator borders need to be taken into account, the avatar itself will be smaller by the borders widths - size + (max(dimensions().avatarStatusBorderSize, dimensions().avatarLegalHoldIndicatorBorderSize) * 2) + size + (max(avatarBorderSize, dimensions().avatarStatusBorderSize) * 2) } is UserProfileAvatarType.WithIndicators.TemporaryUser, @@ -117,21 +119,21 @@ fun UserProfileAvatar( if (type.legalHoldIndicatorVisible) { it .border( - width = dimensions().avatarLegalHoldIndicatorBorderSize / 2, + width = avatarBorderSize / 2, shape = CircleShape, color = colorsScheme().error.copy(alpha = 0.3f) ) - .padding(dimensions().avatarLegalHoldIndicatorBorderSize / 2) + .padding(avatarBorderSize / 2) .border( - width = dimensions().avatarLegalHoldIndicatorBorderSize / 2, + width = avatarBorderSize / 2, shape = CircleShape, color = colorsScheme().error.copy(alpha = 1.0f) ) - .padding(dimensions().avatarLegalHoldIndicatorBorderSize / 2) + .padding(avatarBorderSize / 2) } else { it // this is to make the border of the avatar to be the same size as with the legal hold indicator - .padding(dimensions().avatarLegalHoldIndicatorBorderSize - dimensions().spacing1x) + .padding(avatarBorderSize - dimensions().spacing1x) .border( width = dimensions().spacing1x, shape = CircleShape, @@ -146,7 +148,7 @@ fun UserProfileAvatar( contentScale = ContentScale.Crop ) if (type is UserProfileAvatarType.WithIndicators.LegalHold) { - val avatarWithLegalHoldRadius = (size.value / 2f) + dimensions().avatarLegalHoldIndicatorBorderSize.value + val avatarWithLegalHoldRadius = (size.value / 2f) + avatarBorderSize.value val statusRadius = (dimensions().userAvatarStatusSize - dimensions().avatarStatusBorderSize).value / 2f // calculated using the trigonometry so that the status is always in the right place according to the avatar val paddingToAlignWithAvatar = ((sqrt(2f) - 1f) * avatarWithLegalHoldRadius + (1f - sqrt(2f)) * statusRadius) / sqrt(2f) @@ -162,7 +164,7 @@ fun UserProfileAvatar( CircularProgressIndicator( progress = (type.expiresAt.minus(Clock.System.now()).inWholeHours.toFloat() / 24f).absoluteValue, color = colorsScheme().wireAccentColors.getOrDefault(Accent.Blue, Color.Transparent), - strokeWidth = dimensions().spacing4x, + strokeWidth = avatarBorderSize, modifier = Modifier .size(size) .clip(CircleShape) @@ -282,3 +284,19 @@ fun PreviewTempUserCustomIndicators() { ) } } + +@PreviewMultipleThemes +@Composable +fun PreviewTempUserSmallAvatarCustomIndicators() { + WireTheme { + UserProfileAvatar( + avatarData = UserAvatarData(), + modifier = Modifier.padding( + start = dimensions().spacing8x + ), + avatarBorderSize = 2.dp, + type = UserProfileAvatarType.WithIndicators.TemporaryUser(expiresAt = Clock.System.now().plus(10.hours)), + ) + } +} + 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 b021613fa1a..2f69b720406 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 @@ -40,6 +40,8 @@ import com.wire.android.ui.common.ProtocolLabel import com.wire.android.ui.common.RowItemTemplate import com.wire.android.ui.common.UserBadge import com.wire.android.ui.common.UserProfileAvatar +import com.wire.android.ui.common.UserProfileAvatarType.WithIndicators +import com.wire.android.ui.common.UserProfileAvatarType.WithoutIndicators import com.wire.android.ui.common.dimensions import com.wire.android.ui.home.conversations.details.participants.model.UIParticipant import com.wire.android.ui.home.conversations.search.HighlightName @@ -69,10 +71,12 @@ fun ConversationParticipantItem( RowItemTemplate( leadingIcon = { UserProfileAvatar( - uiParticipant.avatarData, + avatarData = uiParticipant.avatarData, modifier = Modifier.padding( start = dimensions().spacing8x - ) + ), + avatarBorderSize = dimensions().spacing2x, + type = uiParticipant.expiresAt?.let { WithIndicators.TemporaryUser(it) } ?: WithoutIndicators ) }, titleStartPadding = dimensions().spacing0x, From bd03231db7328ca97397b05e25ee226000851f0f Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Tue, 13 Aug 2024 08:35:40 +0200 Subject: [PATCH 09/25] feat: adjust units --- .../kotlin/com/wire/android/ui/common/UserProfileAvatar.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 9cf1ee10134..8bec9fba567 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 @@ -162,7 +162,7 @@ fun UserProfileAvatar( } if (type is UserProfileAvatarType.WithIndicators.TemporaryUser) { CircularProgressIndicator( - progress = (type.expiresAt.minus(Clock.System.now()).inWholeHours.toFloat() / 24f).absoluteValue, + progress = (type.expiresAt.minus(Clock.System.now()).inWholeMinutes.toFloat() / MINUTES_IN_DAY.toFloat()).absoluteValue, color = colorsScheme().wireAccentColors.getOrDefault(Accent.Blue, Color.Transparent), strokeWidth = avatarBorderSize, modifier = Modifier @@ -226,6 +226,8 @@ private fun getDefaultAvatarResourceId(membership: Membership): Int = R.drawable.ic_default_user_avatar } +const val MINUTES_IN_DAY = 60 * 24 + @PreviewMultipleThemes @Composable fun PreviewUserProfileAvatar() { @@ -280,7 +282,7 @@ fun PreviewTempUserCustomIndicators() { avatarData = UserAvatarData(), padding = 4.dp, size = dimensions().avatarDefaultBigSize, - type = UserProfileAvatarType.WithIndicators.TemporaryUser(expiresAt = Clock.System.now().plus(22.hours)), + type = UserProfileAvatarType.WithIndicators.TemporaryUser(expiresAt = Clock.System.now().plus(1.hours)), ) } } From 09566c01c88d9535c7bbfb3dbf2261d07a397ec3 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Tue, 13 Aug 2024 08:55:43 +0200 Subject: [PATCH 10/25] feat: add style for convo item --- .../main/kotlin/com/wire/android/mapper/MessageMapper.kt | 1 + .../messages/item/RegularMessageItemLeading.kt | 8 +++++++- .../wire/android/ui/home/conversations/model/UIMessage.kt | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) 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 e6fa20f779e..0277cfe98a6 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/MessageMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/MessageMapper.kt @@ -162,6 +162,7 @@ class MessageMapper @Inject constructor( }, clientId = (message as? Message.Sendable)?.senderClientId, accent = sender?.accentId?.let { Accent.fromAccentId(it) } ?: Accent.Unknown, + guestExpiresAt = sender?.expiresAt ) private fun getMessageStatus(message: Message.Standalone): MessageStatus { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItemLeading.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItemLeading.kt index 3ff6f940928..4dcd94ce3f7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItemLeading.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItemLeading.kt @@ -22,6 +22,10 @@ import androidx.compose.runtime.remember import com.wire.android.model.Clickable import com.wire.android.model.UserAvatarData import com.wire.android.ui.common.UserProfileAvatar +import com.wire.android.ui.common.UserProfileAvatarType +import com.wire.android.ui.common.UserProfileAvatarType.WithIndicators +import com.wire.android.ui.common.UserProfileAvatarType.WithoutIndicators +import com.wire.android.ui.common.dimensions import com.wire.android.ui.home.conversations.model.MessageHeader @Composable @@ -43,7 +47,9 @@ fun RegularMessageItemLeading( // because avatar takes start padding we don't need to add padding to message item UserProfileAvatar( avatarData = userAvatarData, - clickable = if (isContentClickable) null else avatarClickable + clickable = if (isContentClickable) null else avatarClickable, + avatarBorderSize = dimensions().spacing2x, + type = header.guestExpiresAt?.let { WithIndicators.TemporaryUser(it) } ?: WithoutIndicators ) } } 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 d723951b393..13f59d64599 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 @@ -137,6 +137,7 @@ data class MessageHeader( val isSenderUnavailable: Boolean, val clientId: ClientId? = null, val accent: Accent = Accent.Unknown, + val guestExpiresAt: Instant? = null ) @Stable From 0e60060b7a216b267b3a7d8df774605a2679de3c Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Tue, 13 Aug 2024 11:33:27 +0200 Subject: [PATCH 11/25] fix: detekt --- .../kotlin/com/wire/android/ui/common/UserProfileAvatar.kt | 1 - .../conversations/messages/item/RegularMessageItemLeading.kt | 1 - .../wire/android/ui/userprofile/common/UserProfileInfo.kt | 5 ++--- 3 files changed, 2 insertions(+), 5 deletions(-) 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 8bec9fba567..cd72b11e0bc 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 @@ -301,4 +301,3 @@ fun PreviewTempUserSmallAvatarCustomIndicators() { ) } } - diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItemLeading.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItemLeading.kt index 4dcd94ce3f7..98077e53388 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItemLeading.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItemLeading.kt @@ -22,7 +22,6 @@ import androidx.compose.runtime.remember import com.wire.android.model.Clickable import com.wire.android.model.UserAvatarData import com.wire.android.ui.common.UserProfileAvatar -import com.wire.android.ui.common.UserProfileAvatarType import com.wire.android.ui.common.UserProfileAvatarType.WithIndicators import com.wire.android.ui.common.UserProfileAvatarType.WithoutIndicators import com.wire.android.ui.common.dimensions diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt index 15c6dfd68ea..c24017dd228 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt @@ -59,7 +59,6 @@ import com.wire.android.ui.common.ProteusVerifiedIcon import com.wire.android.ui.common.UserBadge import com.wire.android.ui.common.UserProfileAvatar import com.wire.android.ui.common.UserProfileAvatarType -import com.wire.android.ui.common.UserProfileAvatarType.* import com.wire.android.ui.common.banner.SecurityClassificationBannerForUser import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.progress.WireCircularProgressIndicator @@ -134,7 +133,8 @@ fun UserProfileInfo( }, showPlaceholderIfNoAsset = showPlaceholderIfNoAsset, withCrossfadeAnimation = true, - type = if (expiresAt != null) WithIndicators.TemporaryUser(expiresAt) else WithoutIndicators + type = expiresAt?.let { UserProfileAvatarType.WithIndicators.TemporaryUser(expiresAt) } + ?: UserProfileAvatarType.WithoutIndicators ) } this@Column.AnimatedVisibility(visible = isLoading) { @@ -293,7 +293,6 @@ fun PreviewUserProfileInfo() { ) } - @PreviewMultipleThemes @Composable fun PreviewUserProfileInfoTempUser() { From c0b7fc3ebdc7bda60d0f17dffca09a1c8bcce76c Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Tue, 13 Aug 2024 11:44:10 +0200 Subject: [PATCH 12/25] fix: detekt --- app/src/main/kotlin/com/wire/android/mapper/ContactMapper.kt | 1 - 1 file changed, 1 deletion(-) 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 4e11429f009..8d16c5e3557 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/ContactMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/ContactMapper.kt @@ -89,7 +89,6 @@ class ContactMapper /** * Adds the fully qualified handle to the contact label in case of federated users. - * todo: is this the right place for this logic?, or can be moved to usernamemapper / check for guests if needed. */ private fun mapUserHandle(user: UserSearchDetails): String { return when (user.type) { From 8e7480192d61d69742a093970bd95b6d2cba8722 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Tue, 13 Aug 2024 12:16:19 +0200 Subject: [PATCH 13/25] feat: tests --- .../userprofile/common/UsernameMapperTest.kt | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 app/src/test/kotlin/com/wire/android/ui/userprofile/common/UsernameMapperTest.kt diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/common/UsernameMapperTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/common/UsernameMapperTest.kt new file mode 100644 index 00000000000..4f9ce37502e --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/common/UsernameMapperTest.kt @@ -0,0 +1,34 @@ +package com.wire.android.ui.userprofile.common + +import com.wire.android.framework.TestUser +import com.wire.android.ui.userprofile.common.UsernameMapper.fromOtherUser +import com.wire.kalium.logic.data.user.type.UserType +import kotlinx.datetime.Clock +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import kotlin.time.Duration.Companion.hours + +class UsernameMapperTest { + + @Test + fun `given a federated user, should map its handle with domain`() { + val expected = "otherHandle@domain" + val result = fromOtherUser(TestUser.OTHER_USER.copy(userType = UserType.FEDERATED)) + assertEquals(expected, result) + } + + @Test + fun `given a guest temporary user, should map the handle as hours left`() { + val expected = "22h" + val result = fromOtherUser(TestUser.OTHER_USER.copy(userType = UserType.GUEST, expiresAt = Clock.System.now().plus(22.hours))) + assertEquals(expected, result) + } + + @Test + fun `given some user, non federated or guest, should just map the handle`() { + val expected = "otherHandle" + val result = fromOtherUser(TestUser.OTHER_USER.copy(userType = UserType.INTERNAL)) + assertEquals(expected, result) + } + +} From 07848d8eee91eb6d95872e140ecac539d4a1dfc8 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Tue, 13 Aug 2024 17:18:42 +0200 Subject: [PATCH 14/25] feat: tests --- .../ui/common/UserProfileAvatarTest.kt | 96 +++++++++++++++++++ .../android/ui/common/UserProfileAvatar.kt | 8 +- 2 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/wire/android/ui/common/UserProfileAvatarTest.kt diff --git a/app/src/androidTest/kotlin/com/wire/android/ui/common/UserProfileAvatarTest.kt b/app/src/androidTest/kotlin/com/wire/android/ui/common/UserProfileAvatarTest.kt new file mode 100644 index 00000000000..cdf96738f32 --- /dev/null +++ b/app/src/androidTest/kotlin/com/wire/android/ui/common/UserProfileAvatarTest.kt @@ -0,0 +1,96 @@ +/* + * 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 + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import com.wire.android.model.UserAvatarData +import com.wire.android.ui.WireTestTheme +import com.wire.kalium.logic.data.user.UserAvailabilityStatus +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Clock +import org.junit.Rule +import org.junit.Test +import kotlin.time.Duration.Companion.hours + +class UserProfileAvatarTest { + @get:Rule + val composeTestRule by lazy { createComposeRule() } + + @Test + fun givenAStandardUser_ShouldNotShowIndicators() = runTest { + composeTestRule.setContent { + WireTestTheme { + UserProfileAvatar( + size = dimensions().avatarDefaultBigSize, + avatarData = UserAvatarData(), + type = UserProfileAvatarType.WithoutIndicators + ) + } + } + + composeTestRule.onNodeWithTag(LEGAL_HOLD_INDICATOR_TEST_TAG).assertDoesNotExist() + composeTestRule.onNodeWithTag(TEMP_USER_INDICATOR_TEST_TAG).assertDoesNotExist() + } + + @Test + fun givenAUserUnderLegalHold_ShouldShowLegalHoldIndicators() = runTest { + composeTestRule.setContent { + WireTestTheme { + UserProfileAvatar( + size = dimensions().avatarDefaultBigSize, + avatarData = UserAvatarData(availabilityStatus = UserAvailabilityStatus.AVAILABLE), + type = UserProfileAvatarType.WithIndicators.LegalHold(true) + ) + } + } + + composeTestRule.onNodeWithTag(LEGAL_HOLD_INDICATOR_TEST_TAG).assertIsDisplayed() + } + + @Test + fun givenAUserUnderLegalHoldHidden_ShouldNotShowLegalHoldIndicators() = runTest { + composeTestRule.setContent { + WireTestTheme { + UserProfileAvatar( + size = dimensions().avatarDefaultBigSize, + avatarData = UserAvatarData(), + type = UserProfileAvatarType.WithIndicators.LegalHold(false) + ) + } + } + + composeTestRule.onNodeWithTag(LEGAL_HOLD_INDICATOR_TEST_TAG).assertDoesNotExist() + } + + @Test + fun givenATempGuestUser_ShouldShowTempUserIndicators() = runTest { + composeTestRule.setContent { + WireTestTheme { + UserProfileAvatar( + size = dimensions().avatarDefaultBigSize, + avatarData = UserAvatarData(), + type = UserProfileAvatarType.WithIndicators.TemporaryUser(expiresAt = Clock.System.now().plus(24.hours)) + ) + } + } + + composeTestRule.onNodeWithTag(TEMP_USER_INDICATOR_TEST_TAG).assertExists() + } +} 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 cd72b11e0bc..e89ef9e6c63 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 @@ -59,6 +59,10 @@ import kotlin.math.absoluteValue import kotlin.math.sqrt import kotlin.time.Duration.Companion.hours +const val MINUTES_IN_DAY = 60 * 24 +const val LEGAL_HOLD_INDICATOR_TEST_TAG = "legal_hold_indicator" +const val TEMP_USER_INDICATOR_TEST_TAG = "temp_user_indicator" + /** * @param avatarData data for the avatar * @param modifier modifier for the avatar composable @@ -158,6 +162,7 @@ fun UserProfileAvatar( // on designs the status border extends beyond the avatar's perimeter so we need to subtract it's size from the padding .padding(paddingToAlignWithAvatar.dp - dimensions().avatarStatusBorderSize) .align(Alignment.BottomEnd) + .testTag(LEGAL_HOLD_INDICATOR_TEST_TAG) ) } if (type is UserProfileAvatarType.WithIndicators.TemporaryUser) { @@ -169,6 +174,7 @@ fun UserProfileAvatar( .size(size) .clip(CircleShape) .scale(scaleX = -1f, scaleY = 1f) + .testTag(TEMP_USER_INDICATOR_TEST_TAG) ) } } @@ -226,8 +232,6 @@ private fun getDefaultAvatarResourceId(membership: Membership): Int = R.drawable.ic_default_user_avatar } -const val MINUTES_IN_DAY = 60 * 24 - @PreviewMultipleThemes @Composable fun PreviewUserProfileAvatar() { From 1047532b728c39f0d4164c647bd35b75d1874441 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Tue, 13 Aug 2024 17:20:14 +0200 Subject: [PATCH 15/25] feat: ui tests --- .../android/ui/userprofile/common/UsernameMapperTest.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/common/UsernameMapperTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/common/UsernameMapperTest.kt index 4f9ce37502e..f1037ce5a31 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/common/UsernameMapperTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/common/UsernameMapperTest.kt @@ -20,7 +20,12 @@ class UsernameMapperTest { @Test fun `given a guest temporary user, should map the handle as hours left`() { val expected = "22h" - val result = fromOtherUser(TestUser.OTHER_USER.copy(userType = UserType.GUEST, expiresAt = Clock.System.now().plus(22.hours))) + val result = fromOtherUser( + TestUser.OTHER_USER.copy( + userType = UserType.GUEST, + expiresAt = Clock.System.now().plus(22.hours) + ) + ) assertEquals(expected, result) } @@ -30,5 +35,4 @@ class UsernameMapperTest { val result = fromOtherUser(TestUser.OTHER_USER.copy(userType = UserType.INTERNAL)) assertEquals(expected, result) } - } From e34e975edfbd85d58b3377a585ad630be75683c0 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Tue, 13 Aug 2024 17:21:29 +0200 Subject: [PATCH 16/25] fix: testname --- .../common/{UsernameMapperTest.kt => _UsernameMapperTest.kt} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename app/src/test/kotlin/com/wire/android/ui/userprofile/common/{UsernameMapperTest.kt => _UsernameMapperTest.kt} (97%) diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/common/UsernameMapperTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/common/_UsernameMapperTest.kt similarity index 97% rename from app/src/test/kotlin/com/wire/android/ui/userprofile/common/UsernameMapperTest.kt rename to app/src/test/kotlin/com/wire/android/ui/userprofile/common/_UsernameMapperTest.kt index f1037ce5a31..7e14b57429c 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/common/UsernameMapperTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/common/_UsernameMapperTest.kt @@ -8,7 +8,7 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import kotlin.time.Duration.Companion.hours -class UsernameMapperTest { +class _UsernameMapperTest { @Test fun `given a federated user, should map its handle with domain`() { From e4762cf1f2f2c54bd5d06146147cf0de68b35ab5 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Tue, 13 Aug 2024 17:22:54 +0200 Subject: [PATCH 17/25] chore: merge develop and adr adj --- .github/workflows/deploy-adr-docs.yml | 2 +- docs/adr/0003-introducing-junit5-parametrizable-tests.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-adr-docs.yml b/.github/workflows/deploy-adr-docs.yml index 111838e3ff0..aeaa050bf48 100644 --- a/.github/workflows/deploy-adr-docs.yml +++ b/.github/workflows/deploy-adr-docs.yml @@ -2,7 +2,7 @@ name: Deploy ADR Docs on: push: - branches: [ 'develop', 'chore/adr-and-tests' ] + branches: [ 'develop' ] pull_request: types: [ opened, synchronize ] paths: [ 'docs/adr/**' ] diff --git a/docs/adr/0003-introducing-junit5-parametrizable-tests.md b/docs/adr/0003-introducing-junit5-parametrizable-tests.md index 03281515734..b3453e67824 100644 --- a/docs/adr/0003-introducing-junit5-parametrizable-tests.md +++ b/docs/adr/0003-introducing-junit5-parametrizable-tests.md @@ -4,7 +4,7 @@ Date: 2024-08-05 ## Status -Proposed +Accepted ## Context From 4cd14be6d7990d9d35853c0b6bad266492b4f083 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Tue, 13 Aug 2024 17:28:13 +0200 Subject: [PATCH 18/25] chore: merge develop and adr adj --- .../userprofile/common/UsernameMapperTest.kt | 16 ++++++++ .../userprofile/common/_UsernameMapperTest.kt | 38 ------------------- 2 files changed, 16 insertions(+), 38 deletions(-) delete mode 100644 app/src/test/kotlin/com/wire/android/ui/userprofile/common/_UsernameMapperTest.kt diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/common/UsernameMapperTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/common/UsernameMapperTest.kt index 4904afd7a16..cfec8f16d18 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/common/UsernameMapperTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/common/UsernameMapperTest.kt @@ -1,14 +1,30 @@ package com.wire.android.ui.userprofile.common import com.wire.android.framework.TestUser.OTHER_USER +import com.wire.android.ui.userprofile.common.UsernameMapper.fromOtherUser import com.wire.kalium.logic.data.user.OtherUser import com.wire.kalium.logic.data.user.type.UserType +import kotlinx.datetime.Clock import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.EnumSource +import kotlin.time.Duration.Companion.hours class UsernameMapperTest { + @Test + fun `given a guest temporary user, should map the handle as hours left`() { + val expected = "22h" + val result = fromOtherUser( + OTHER_USER.copy( + userType = UserType.GUEST, + expiresAt = Clock.System.now().plus(22.hours) + ) + ) + assertEquals(expected, result) + } + @ParameterizedTest @EnumSource(TestParams::class) fun `should map other user to its username - handle accordingly`(params: TestParams) { diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/common/_UsernameMapperTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/common/_UsernameMapperTest.kt deleted file mode 100644 index 7e14b57429c..00000000000 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/common/_UsernameMapperTest.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.wire.android.ui.userprofile.common - -import com.wire.android.framework.TestUser -import com.wire.android.ui.userprofile.common.UsernameMapper.fromOtherUser -import com.wire.kalium.logic.data.user.type.UserType -import kotlinx.datetime.Clock -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import kotlin.time.Duration.Companion.hours - -class _UsernameMapperTest { - - @Test - fun `given a federated user, should map its handle with domain`() { - val expected = "otherHandle@domain" - val result = fromOtherUser(TestUser.OTHER_USER.copy(userType = UserType.FEDERATED)) - assertEquals(expected, result) - } - - @Test - fun `given a guest temporary user, should map the handle as hours left`() { - val expected = "22h" - val result = fromOtherUser( - TestUser.OTHER_USER.copy( - userType = UserType.GUEST, - expiresAt = Clock.System.now().plus(22.hours) - ) - ) - assertEquals(expected, result) - } - - @Test - fun `given some user, non federated or guest, should just map the handle`() { - val expected = "otherHandle" - val result = fromOtherUser(TestUser.OTHER_USER.copy(userType = UserType.INTERNAL)) - assertEquals(expected, result) - } -} From 04b12a07986a8bbdeeb8acc5390e3be8dcaa2621 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Tue, 13 Aug 2024 17:45:30 +0200 Subject: [PATCH 19/25] feat: adjust designs --- .../wire/android/ui/userprofile/other/OtherUserProfileScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt index f5df2268980..202c6700539 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt @@ -489,7 +489,7 @@ private fun Content( Crossfade(targetState = tabItems to state, label = "OtherUserProfile") { (tabItems, state) -> Column { - if (!state.isDataLoading) { + if (!state.isDataLoading && !state.isTemporaryUser()) { OtherUserConnectionStatusInfo(state.connectionState, state.membership) OtherUserConnectionUnverifiedWarning(state.fullName, state.connectionState) } From 16526627e0a017181c660bcaf22e37550c71bdc0 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Tue, 13 Aug 2024 18:10:10 +0200 Subject: [PATCH 20/25] feat: adjust linter --- .../com/wire/android/ui/userprofile/common/UserProfileInfo.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt index c24017dd228..937acca9b80 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt @@ -78,6 +78,7 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.milliseconds +@SuppressLint("ComposeParameterOrder") @Composable fun UserProfileInfo( userId: UserId?, From 6351aca0158ca1297b710d3bfb8b469bb75b6865 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Wed, 14 Aug 2024 10:46:33 +0200 Subject: [PATCH 21/25] feat: adjust linter --- .../com/wire/android/ui/userprofile/common/UserProfileInfo.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt index 937acca9b80..b597ac08681 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt @@ -240,7 +240,6 @@ fun UserProfileInfo( } } -@SuppressLint("ComposeNamingLowercase") @Composable private fun Username(userName: String, membership: Membership, expiresAt: Instant?): String { return when { From 02f1d3a477294e3140f2dc96117338d7207a361e Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Wed, 14 Aug 2024 11:32:34 +0200 Subject: [PATCH 22/25] feat: adjust linter --- .../com/wire/android/ui/userprofile/common/UserProfileInfo.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt index b597ac08681..f13c30972e3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt @@ -191,7 +191,7 @@ fun UserProfileInfo( if (isProteusVerified) ProteusVerifiedIcon() } Text( - text = Username(userName, membership, expiresAt), + text = processUsername(userName, membership, expiresAt), overflow = TextOverflow.Ellipsis, style = MaterialTheme.wireTypography.body02, maxLines = 1, @@ -241,7 +241,7 @@ fun UserProfileInfo( } @Composable -private fun Username(userName: String, membership: Membership, expiresAt: Instant?): String { +private fun processUsername(userName: String, membership: Membership, expiresAt: Instant?): String { return when { expiresAt != null -> UIText.StringResource(R.string.temporary_user_label, userName).asString() membership == Membership.Service -> userName From 7848ae6cee6934ea3dd36a6265b0d90311d6c62c Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Wed, 14 Aug 2024 12:11:38 +0200 Subject: [PATCH 23/25] feat: adjust temporary units time left --- .../ui/userprofile/common/UserProfileInfo.kt | 29 +++++++++++++++++-- .../ui/userprofile/common/UsernameMapper.kt | 14 +++++++-- .../userprofile/common/UsernameMapperTest.kt | 15 +++++++++- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt index f13c30972e3..2112b366a17 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt @@ -70,13 +70,17 @@ import com.wire.android.util.ifNotEmpty import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.android.util.ui.UIText import com.wire.kalium.logic.data.user.ConnectionState +import com.wire.kalium.logic.data.user.OtherUser +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 kotlinx.coroutines.delay import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlin.time.Duration import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.minutes @SuppressLint("ComposeParameterOrder") @Composable @@ -298,9 +302,28 @@ fun PreviewUserProfileInfo() { fun PreviewUserProfileInfoTempUser() { UserProfileInfo( userId = UserId("value", "domain"), - isLoading = true, + isLoading = false, editableState = EditableState.IsEditable {}, - userName = "23h", + userName = UsernameMapper.fromOtherUser( + OtherUser( + id = UserId("value", "domain"), + name = "fullName", + handle = "", + accentId = 1, + connectionStatus = ConnectionState.ACCEPTED, + userType = UserType.GUEST, + availabilityStatus = UserAvailabilityStatus.AVAILABLE, + completePicture = null, + previewPicture = null, + expiresAt = Clock.System.now().plus(2.minutes), + botService = null, + isProteusVerified = true, + teamId = null, + deleted = false, + defederated = false, + supportedProtocols = null + ) + ), avatarAsset = null, fullName = "fullName", onUserProfileClick = {}, @@ -308,6 +331,6 @@ fun PreviewUserProfileInfoTempUser() { connection = ConnectionState.ACCEPTED, isProteusVerified = true, isMLSVerified = true, - expiresAt = Clock.System.now().plus(23.hours) + expiresAt = Clock.System.now().plus(1.hours) ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UsernameMapper.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UsernameMapper.kt index a16a157fa8b..e458abcf44a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UsernameMapper.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UsernameMapper.kt @@ -22,8 +22,10 @@ import com.wire.android.util.ifNotEmpty import com.wire.kalium.logic.data.user.OtherUser import com.wire.kalium.logic.data.user.type.UserType import kotlinx.datetime.Clock +import kotlin.time.Duration.Companion.minutes import kotlin.time.DurationUnit +@Suppress("MagicNumber") object UsernameMapper { /** @@ -34,8 +36,16 @@ object UsernameMapper { return when { userType == UserType.FEDERATED -> handle?.ifNotEmpty { "$handle@${id.domain}" }.orEmpty() expiresAt != null -> { - val expiresAtString = expiresAt!!.minus(Clock.System.now()).toString(DurationUnit.HOURS) - expiresAtString + val diff = expiresAt!!.minus(Clock.System.now()) + val diffInMinutes = diff.inWholeMinutes + when { + diffInMinutes <= 0 -> 0.minutes.toString(DurationUnit.MINUTES) + diffInMinutes in 1..59 -> diff.toString(DurationUnit.MINUTES) + else -> { + val expiresAtString = diff.toString(DurationUnit.HOURS) + expiresAtString + } + } } else -> handle.orEmpty() diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/common/UsernameMapperTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/common/UsernameMapperTest.kt index cfec8f16d18..2747fe0bbdd 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/common/UsernameMapperTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/common/UsernameMapperTest.kt @@ -10,6 +10,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.EnumSource import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.minutes class UsernameMapperTest { @@ -25,10 +26,22 @@ class UsernameMapperTest { assertEquals(expected, result) } + @Test + fun `given a guest temporary user, and time left is less than 1 hour, should map the handle as minutes left`() { + val expected = "10m" + val result = fromOtherUser( + OTHER_USER.copy( + userType = UserType.GUEST, + expiresAt = Clock.System.now().plus(10.minutes) + ) + ) + assertEquals(expected, result) + } + @ParameterizedTest @EnumSource(TestParams::class) fun `should map other user to its username - handle accordingly`(params: TestParams) { - assertEquals(params.expected, UsernameMapper.fromOtherUser(params.input), "Failed for input: <${params.input}>") + assertEquals(params.expected, fromOtherUser(params.input), "Failed for input: <${params.input}>") } companion object { From 5951266daf557e9c3fe678f57bc7fd9dcfdd5d24 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Wed, 14 Aug 2024 12:16:38 +0200 Subject: [PATCH 24/25] feat: adjust temporary units time left, docs --- .../com/wire/android/ui/userprofile/common/UsernameMapper.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UsernameMapper.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UsernameMapper.kt index e458abcf44a..5227eee5103 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UsernameMapper.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UsernameMapper.kt @@ -31,6 +31,7 @@ object UsernameMapper { /** * Returns the username for the given [OtherUser]. * The username is the handle if it exists, otherwise it is the handle@domain for federated users. + * For temporary users, the username is the time left until the user expires. */ fun fromOtherUser(otherUser: OtherUser): String = with(otherUser) { return when { From 475e876058edaa57c2f51a61e67399a5fdbae95d Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Wed, 14 Aug 2024 12:26:59 +0200 Subject: [PATCH 25/25] feat: adjust temporary units time left, docs --- .../ConversationParticipantItem.kt | 12 ++++----- .../ui/userprofile/common/UsernameMapper.kt | 25 +++++++++---------- 2 files changed, 18 insertions(+), 19 deletions(-) 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 2f69b720406..402c238af3f 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 @@ -51,6 +51,7 @@ import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography +import com.wire.android.ui.userprofile.common.UsernameMapper.fromExpirationToHandle import com.wire.android.util.EMPTY import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.android.util.uiReadReceiptDateTime @@ -59,7 +60,6 @@ import com.wire.kalium.logic.data.user.UserId import kotlinx.datetime.Clock import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.plus -import kotlin.time.DurationUnit @Composable fun ConversationParticipantItem( @@ -124,9 +124,9 @@ fun ConversationParticipantItem( }, subtitle = { HighlightSubtitle( - subTitle = Username(uiParticipant), + subTitle = processUsername(uiParticipant), searchQuery = searchQuery, - prefix = UsernamePrefix(uiParticipant) + prefix = processUsernamePrefix(uiParticipant) ) }, actions = { @@ -145,17 +145,17 @@ fun ConversationParticipantItem( } @Composable -private fun UsernamePrefix(uiParticipant: UIParticipant) = when { +private fun processUsernamePrefix(uiParticipant: UIParticipant) = when { uiParticipant.readReceiptDate != null || uiParticipant.expiresAt != null -> "" else -> "@" } @Composable -private fun Username(uiParticipant: UIParticipant) = when { +private fun processUsername(uiParticipant: UIParticipant) = when { uiParticipant.unavailable -> uiParticipant.id.domain uiParticipant.readReceiptDate != null -> uiParticipant.readReceiptDate.uiReadReceiptDateTime() uiParticipant.expiresAt != null -> { - val expiresAtString = uiParticipant.expiresAt.minus(Clock.System.now()).toString(DurationUnit.HOURS) + val expiresAtString = fromExpirationToHandle(uiParticipant.expiresAt) stringResource(R.string.temporary_user_label, expiresAtString) } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UsernameMapper.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UsernameMapper.kt index 5227eee5103..34d1a6a2cd3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UsernameMapper.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UsernameMapper.kt @@ -22,6 +22,7 @@ import com.wire.android.util.ifNotEmpty import com.wire.kalium.logic.data.user.OtherUser import com.wire.kalium.logic.data.user.type.UserType import kotlinx.datetime.Clock +import kotlinx.datetime.Instant import kotlin.time.Duration.Companion.minutes import kotlin.time.DurationUnit @@ -36,20 +37,18 @@ object UsernameMapper { fun fromOtherUser(otherUser: OtherUser): String = with(otherUser) { return when { userType == UserType.FEDERATED -> handle?.ifNotEmpty { "$handle@${id.domain}" }.orEmpty() - expiresAt != null -> { - val diff = expiresAt!!.minus(Clock.System.now()) - val diffInMinutes = diff.inWholeMinutes - when { - diffInMinutes <= 0 -> 0.minutes.toString(DurationUnit.MINUTES) - diffInMinutes in 1..59 -> diff.toString(DurationUnit.MINUTES) - else -> { - val expiresAtString = diff.toString(DurationUnit.HOURS) - expiresAtString - } - } - } - + expiresAt != null -> fromExpirationToHandle(expiresAt!!) else -> handle.orEmpty() } } + + fun fromExpirationToHandle(expiresAt: Instant): String { + val diff = expiresAt.minus(Clock.System.now()) + val diffInMinutes = diff.inWholeMinutes + return when { + diffInMinutes <= 0 -> 0.minutes.toString(DurationUnit.MINUTES) + diffInMinutes in 1..59 -> diff.toString(DurationUnit.MINUTES) + else -> diff.toString(DurationUnit.HOURS) + } + } }