Skip to content

Commit

Permalink
feat: handle temporary guest users details (WPB-10454) (#3322)
Browse files Browse the repository at this point in the history
  • Loading branch information
yamilmedina authored Aug 14, 2024
1 parent 1808cc8 commit e429419
Show file tree
Hide file tree
Showing 16 changed files with 381 additions and 42 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/deploy-adr-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/**' ]
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
91 changes: 75 additions & 16 deletions app/src/main/kotlin/com/wire/android/ui/common/UserProfileAvatar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ 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
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
Expand All @@ -45,12 +47,21 @@ 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
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

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
Expand All @@ -59,6 +70,7 @@ import kotlin.math.sqrt
* 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
Expand All @@ -70,10 +82,11 @@ 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,
type: UserProfileAvatarType = UserProfileAvatarType.WithIndicators(legalHoldIndicatorVisible = false),
type: UserProfileAvatarType = UserProfileAvatarType.WithIndicators.LegalHold(legalHoldIndicatorVisible = false),
) {
Box(
contentAlignment = Alignment.Center,
Expand All @@ -93,36 +106,38 @@ 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)
size + (max(avatarBorderSize, dimensions().avatarStatusBorderSize) * 2)
}
UserProfileAvatarType.WithoutIndicators -> {

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
}
}
)
.let {
if (type is UserProfileAvatarType.WithIndicators) {
if (type is UserProfileAvatarType.WithIndicators.LegalHold) {
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,
Expand All @@ -136,8 +151,8 @@ fun UserProfileAvatar(
.testTag("User avatar"),
contentScale = ContentScale.Crop
)
if (type is UserProfileAvatarType.WithIndicators) {
val avatarWithLegalHoldRadius = (size.value / 2f) + dimensions().avatarLegalHoldIndicatorBorderSize.value
if (type is UserProfileAvatarType.WithIndicators.LegalHold) {
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)
Expand All @@ -147,15 +162,31 @@ 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) {
CircularProgressIndicator(
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
.size(size)
.clip(CircleShape)
.scale(scaleX = -1f, scaleY = 1f)
.testTag(TEMP_USER_INDICATOR_TEST_TAG)
)
}
}
}

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()
Expand Down Expand Up @@ -217,7 +248,7 @@ fun PreviewUserProfileAvatarWithLegalHold() {
WireTheme {
UserProfileAvatar(
avatarData = UserAvatarData(availabilityStatus = UserAvailabilityStatus.AVAILABLE),
type = UserProfileAvatarType.WithIndicators(legalHoldIndicatorVisible = true)
type = UserProfileAvatarType.WithIndicators.LegalHold(legalHoldIndicatorVisible = true)
)
}
}
Expand All @@ -229,7 +260,7 @@ fun PreviewLargeUserProfileAvatarWithLegalHold() {
UserProfileAvatar(
avatarData = UserAvatarData(availabilityStatus = UserAvailabilityStatus.AVAILABLE),
size = 48.dp,
type = UserProfileAvatarType.WithIndicators(legalHoldIndicatorVisible = true)
type = UserProfileAvatarType.WithIndicators.LegalHold(legalHoldIndicatorVisible = true)
)
}
}
Expand All @@ -246,3 +277,31 @@ fun PreviewUserProfileAvatarWithoutIndicators() {
)
}
}

@PreviewMultipleThemes
@Composable
fun PreviewTempUserCustomIndicators() {
WireTheme {
UserProfileAvatar(
avatarData = UserAvatarData(),
padding = 4.dp,
size = dimensions().avatarDefaultBigSize,
type = UserProfileAvatarType.WithIndicators.TemporaryUser(expiresAt = Clock.System.now().plus(1.hours)),
)
}
}

@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)),
)
}
}
2 changes: 1 addition & 1 deletion app/src/main/kotlin/com/wire/android/ui/home/HomeTopBar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit e429419

Please sign in to comment.