Merge pull request #3069 from element-hq/feature/bma/avatarCluster_x2

Avatar cluster for DM
This commit is contained in:
Benoit Marty 2024-06-24 10:53:50 +02:00 committed by GitHub
commit 010a90f9ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
98 changed files with 416 additions and 206 deletions

View file

@ -45,6 +45,7 @@ import io.element.android.libraries.matrix.api.room.powerlevels.canInvite
import io.element.android.libraries.matrix.api.room.powerlevels.canSendState
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
import io.element.android.libraries.matrix.ui.room.canCall
import io.element.android.libraries.matrix.ui.room.getCurrentRoomMember
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
import io.element.android.libraries.matrix.ui.room.isOwnUserAdmin
import io.element.android.services.analytics.api.AnalyticsService
@ -98,8 +99,9 @@ class RoomDetailsPresenter @Inject constructor(
val canEditTopic by getCanSendState(membersState, StateEventType.ROOM_TOPIC)
val canJoinCall by room.canCall(updateKey = syncUpdateTimestamp)
val dmMember by room.getDirectRoomMember(membersState)
val currentMember by room.getCurrentRoomMember(membersState)
val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember)
val roomType by getRoomType(dmMember)
val roomType by getRoomType(dmMember, currentMember)
val topicState = remember(canEditTopic, roomTopic, roomType) {
val topic = roomTopic
@ -165,10 +167,16 @@ class RoomDetailsPresenter @Inject constructor(
}
@Composable
private fun getRoomType(dmMember: RoomMember?): State<RoomDetailsType> = remember(dmMember) {
private fun getRoomType(
dmMember: RoomMember?,
currentMember: RoomMember?,
): State<RoomDetailsType> = remember(dmMember, currentMember) {
derivedStateOf {
if (dmMember != null) {
RoomDetailsType.Dm(dmMember)
if (dmMember != null && currentMember != null) {
RoomDetailsType.Dm(
me = currentMember,
otherMember = dmMember,
)
} else {
RoomDetailsType.Room
}

View file

@ -52,7 +52,10 @@ data class RoomDetailsState(
@Immutable
sealed interface RoomDetailsType {
data object Room : RoomDetailsType
data class Dm(val roomMember: RoomMember) : RoomDetailsType
data class Dm(
val me: RoomMember,
val otherMember: RoomMember,
) : RoomDetailsType
}
@Immutable

View file

@ -19,6 +19,7 @@ package io.element.android.features.roomdetails.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.features.leaveroom.api.aLeaveRoomState
import io.element.android.features.roomdetails.impl.members.aRoomMember
import io.element.android.features.userprofile.shared.UserProfileState
import io.element.android.features.userprofile.shared.aUserProfileState
import io.element.android.libraries.matrix.api.core.RoomAlias
@ -141,6 +142,10 @@ fun aDmRoomDetailsState(
roomName: String = "Daniel",
) = aRoomDetailsState(
roomName = roomName,
roomType = RoomDetailsType.Dm(aDmRoomMember(isIgnored = isDmMemberIgnored)),
isPublic = false,
roomType = RoomDetailsType.Dm(
aRoomMember(),
aDmRoomMember(isIgnored = isDmMemberIgnored),
),
roomMemberDetailsState = aUserProfileState()
)

View file

@ -20,6 +20,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
@ -48,7 +49,6 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.leaveroom.api.LeaveRoomView
import io.element.android.features.roomdetails.impl.components.RoomBadge
import io.element.android.features.userprofile.shared.UserProfileHeaderSection
import io.element.android.features.userprofile.shared.blockuser.BlockUserDialogs
import io.element.android.features.userprofile.shared.blockuser.BlockUserSection
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
@ -56,6 +56,7 @@ import io.element.android.libraries.designsystem.components.ClickableLinkText
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.CompositeAvatar
import io.element.android.libraries.designsystem.components.avatar.DmAvatars
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.button.MainActionButton
import io.element.android.libraries.designsystem.components.list.ListItemContent
@ -78,6 +79,7 @@ import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.getBestName
import io.element.android.libraries.matrix.api.user.MatrixUser
@ -128,39 +130,35 @@ fun RoomDetailsView(
roomId = state.roomId,
roomName = state.roomName,
roomAlias = state.roomAlias,
isEncrypted = state.isEncrypted,
isPublic = state.isPublic,
heroes = state.heroes,
openAvatarPreview = { avatarUrl ->
openAvatarPreview(state.roomName, avatarUrl)
},
)
MainActionsSection(
state = state,
onShareRoom = onShareRoom,
onInvitePeople = invitePeople,
onCall = onJoinCallClick,
)
}
is RoomDetailsType.Dm -> {
val member = state.roomType.roomMember
UserProfileHeaderSection(
avatarUrl = state.roomAvatarUrl ?: member.avatarUrl,
userId = member.userId,
userName = state.roomName,
openAvatarPreview = { avatarUrl ->
openAvatarPreview(member.getBestName(), avatarUrl)
DmHeaderSection(
me = state.roomType.me,
otherMember = state.roomType.otherMember,
roomName = state.roomName,
openAvatarPreview = { name, avatarUrl ->
openAvatarPreview(name, avatarUrl)
},
)
MainActionsSection(
state = state,
onShareRoom = onShareRoom,
onInvitePeople = invitePeople,
onCall = onJoinCallClick,
)
}
}
BadgeList(
isEncrypted = state.isEncrypted,
isPublic = state.isPublic,
modifier = Modifier.align(Alignment.CenterHorizontally),
)
Spacer(Modifier.height(32.dp))
MainActionsSection(
state = state,
onShareRoom = onShareRoom,
onInvitePeople = invitePeople,
onCall = onJoinCallClick,
)
Spacer(Modifier.height(12.dp))
if (state.roomTopic !is RoomTopicState.Hidden) {
@ -326,8 +324,6 @@ private fun RoomHeaderSection(
roomId: RoomId,
roomName: String,
roomAlias: RoomAlias?,
isEncrypted: Boolean,
isPublic: Boolean,
heroes: ImmutableList<MatrixUser>,
openAvatarPreview: (url: String) -> Unit,
) {
@ -346,23 +342,56 @@ private fun RoomHeaderSection(
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
.testTag(TestTags.roomDetailAvatar)
)
Spacer(modifier = Modifier.height(24.dp))
TitleAndSubtitle(title = roomName, subtitle = roomAlias?.value)
}
}
@Composable
private fun DmHeaderSection(
me: RoomMember,
otherMember: RoomMember,
roomName: String,
openAvatarPreview: (name: String, url: String) -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
DmAvatars(
userAvatarData = me.getAvatarData(size = AvatarSize.DmCluster),
otherUserAvatarData = otherMember.getAvatarData(size = AvatarSize.DmCluster),
openAvatarPreview = { url -> openAvatarPreview(me.getBestName(), url) },
openOtherAvatarPreview = { url -> openAvatarPreview(roomName, url) },
)
TitleAndSubtitle(
title = roomName,
subtitle = otherMember.userId.value,
)
}
}
@Composable
private fun ColumnScope.TitleAndSubtitle(
title: String,
subtitle: String?,
) {
Spacer(modifier = Modifier.height(24.dp))
Text(
text = title,
style = ElementTheme.typography.fontHeadingLgBold,
textAlign = TextAlign.Center,
)
if (subtitle != null) {
Spacer(modifier = Modifier.height(6.dp))
Text(
text = roomName,
style = ElementTheme.typography.fontHeadingLgBold,
text = subtitle,
style = ElementTheme.typography.fontBodyLgRegular,
color = MaterialTheme.colorScheme.secondary,
textAlign = TextAlign.Center,
)
if (roomAlias != null) {
Spacer(modifier = Modifier.height(6.dp))
Text(
text = roomAlias.value,
style = ElementTheme.typography.fontBodyLgRegular,
color = MaterialTheme.colorScheme.secondary,
textAlign = TextAlign.Center,
)
}
BadgeList(isEncrypted = isEncrypted, isPublic = isPublic)
Spacer(Modifier.height(32.dp))
}
}
@ -370,11 +399,12 @@ private fun RoomHeaderSection(
private fun BadgeList(
isEncrypted: Boolean,
isPublic: Boolean,
modifier: Modifier = Modifier,
) {
if (isEncrypted || isPublic) {
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.padding(horizontal = 16.dp),
modifier = modifier
.padding(start = 16.dp, end = 16.dp, top = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
if (isEncrypted) {

View file

@ -169,8 +169,12 @@ class RoomDetailsPresenterTest {
val presenter = createRoomDetailsPresenter(room)
presenter.test {
val initialState = awaitItem()
assertThat(initialState.roomType).isEqualTo(RoomDetailsType.Dm(otherRoomMember))
assertThat(initialState.roomType).isEqualTo(
RoomDetailsType.Dm(
me = myRoomMember,
otherMember = otherRoomMember,
)
)
cancelAndIgnoreRemainingEvents()
}
}

View file

@ -23,6 +23,7 @@ import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.roomdetails.impl.members.aRoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.ui.strings.CommonStrings
@ -177,7 +178,11 @@ class RoomDetailsViewTest {
fun `click on avatar test on DM`() {
val eventsRecorder = EventsRecorder<RoomDetailsEvent>(expectEvents = false)
val state = aRoomDetailsState(
roomType = RoomDetailsType.Dm(aDmRoomMember(avatarUrl = "an_avatar_url")),
roomType = RoomDetailsType.Dm(
aRoomMember(),
aDmRoomMember(avatarUrl = "an_avatar_url"),
),
roomName = "Daniel",
eventSink = eventsRecorder,
)
val callback = EnsureCalledOnceWithTwoParams("Daniel", "an_avatar_url")

View file

@ -17,14 +17,11 @@
package io.element.android.features.userprofile.shared
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@ -36,6 +33,8 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.testtags.TestTags
@ -55,15 +54,12 @@ fun UserProfileHeaderSection(
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(modifier = Modifier.size(70.dp)) {
Avatar(
avatarData = AvatarData(userId.value, userName, avatarUrl, AvatarSize.UserHeader),
modifier = Modifier
Avatar(
avatarData = AvatarData(userId.value, userName, avatarUrl, AvatarSize.UserHeader),
modifier = Modifier
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
.fillMaxSize()
.testTag(TestTags.memberDetailAvatar)
)
}
)
Spacer(modifier = Modifier.height(24.dp))
if (userName != null) {
Text(
@ -86,3 +82,14 @@ fun UserProfileHeaderSection(
Spacer(Modifier.height(40.dp))
}
}
@PreviewsDayNight
@Composable
internal fun UserProfileHeaderSectionPreview() = ElementPreview {
UserProfileHeaderSection(
avatarUrl = null,
userId = UserId("@alice:example.com"),
userName = "Alice",
openAvatarPreview = {},
)
}