Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: handle temporary guest users details (WPB-10454) #3322

Merged
merged 29 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6010a0f
feat: add progress avatar
yamilmedina Aug 9, 2024
9e9d547
feat: add progress avatar border adjustments
yamilmedina Aug 9, 2024
7416465
feat: add progress avatar border adjustments mappigs
yamilmedina Aug 9, 2024
de68629
feat: add progress avatar border adjustments mappigs
yamilmedina Aug 9, 2024
f579855
feat: add progress avatar border adjustments mappigs cleanup
yamilmedina Aug 9, 2024
3946e9e
feat: design adjustment for guest users
yamilmedina Aug 12, 2024
98200c8
feat: design adjustment for guest users
yamilmedina Aug 12, 2024
9a50f95
feat: design adjustment for guest users
yamilmedina Aug 12, 2024
bd03231
feat: adjust units
yamilmedina Aug 13, 2024
09566c0
feat: add style for convo item
yamilmedina Aug 13, 2024
0e60060
fix: detekt
yamilmedina Aug 13, 2024
c0b7fc3
fix: detekt
yamilmedina Aug 13, 2024
8e74801
feat: tests
yamilmedina Aug 13, 2024
07848d8
feat: tests
yamilmedina Aug 13, 2024
1047532
feat: ui tests
yamilmedina Aug 13, 2024
e34e975
fix: testname
yamilmedina Aug 13, 2024
d34c3ca
Merge branch 'develop' into feat/guest-users-details
yamilmedina Aug 13, 2024
e4762cf
chore: merge develop and adr adj
yamilmedina Aug 13, 2024
4cd14be
chore: merge develop and adr adj
yamilmedina Aug 13, 2024
04b12a0
feat: adjust designs
yamilmedina Aug 13, 2024
1652662
feat: adjust linter
yamilmedina Aug 13, 2024
9472a2e
Merge branch 'develop' into feat/guest-users-details
yamilmedina Aug 14, 2024
a118c54
Merge branch 'develop' into feat/guest-users-details
yamilmedina Aug 14, 2024
6351aca
feat: adjust linter
yamilmedina Aug 14, 2024
02f1d3a
feat: adjust linter
yamilmedina Aug 14, 2024
7848ae6
feat: adjust temporary units time left
yamilmedina Aug 14, 2024
2a0af60
Merge branch 'develop' into feat/guest-users-details
yamilmedina Aug 14, 2024
5951266
feat: adjust temporary units time left, docs
yamilmedina Aug 14, 2024
475e876
feat: adjust temporary units time left, docs
yamilmedina Aug 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading