Use DmAvatars in header of DM details.

This commit is contained in:
Benoit Marty 2024-06-21 15:43:32 +02:00
parent a2c8dc42cb
commit e8fe8f51c8
8 changed files with 97 additions and 17 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.powerlevels.canSendState
import io.element.android.libraries.matrix.api.room.roomNotificationSettings 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.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.getDirectRoomMember
import io.element.android.libraries.matrix.ui.room.isOwnUserAdmin import io.element.android.libraries.matrix.ui.room.isOwnUserAdmin
import io.element.android.services.analytics.api.AnalyticsService 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 canEditTopic by getCanSendState(membersState, StateEventType.ROOM_TOPIC)
val canJoinCall by room.canCall(updateKey = syncUpdateTimestamp) val canJoinCall by room.canCall(updateKey = syncUpdateTimestamp)
val dmMember by room.getDirectRoomMember(membersState) val dmMember by room.getDirectRoomMember(membersState)
val currentMember by room.getCurrentRoomMember(membersState)
val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember) val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember)
val roomType by getRoomType(dmMember) val roomType by getRoomType(dmMember, currentMember)
val topicState = remember(canEditTopic, roomTopic, roomType) { val topicState = remember(canEditTopic, roomTopic, roomType) {
val topic = roomTopic val topic = roomTopic
@ -165,10 +167,16 @@ class RoomDetailsPresenter @Inject constructor(
} }
@Composable @Composable
private fun getRoomType(dmMember: RoomMember?): State<RoomDetailsType> = remember(dmMember) { private fun getRoomType(
dmMember: RoomMember?,
currentMember: RoomMember?,
): State<RoomDetailsType> = remember(dmMember, currentMember) {
derivedStateOf { derivedStateOf {
if (dmMember != null) { if (dmMember != null && currentMember != null) {
RoomDetailsType.Dm(dmMember) RoomDetailsType.Dm(
me = currentMember,
roomMember = dmMember,
)
} else { } else {
RoomDetailsType.Room RoomDetailsType.Room
} }

View file

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

View file

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

View file

@ -40,6 +40,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
@ -48,7 +49,6 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.leaveroom.api.LeaveRoomView import io.element.android.features.leaveroom.api.LeaveRoomView
import io.element.android.features.roomdetails.impl.components.RoomBadge 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.BlockUserDialogs
import io.element.android.features.userprofile.shared.blockuser.BlockUserSection import io.element.android.features.userprofile.shared.blockuser.BlockUserSection
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage 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.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize 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.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.BackButton
import io.element.android.libraries.designsystem.components.button.MainActionButton import io.element.android.libraries.designsystem.components.button.MainActionButton
import io.element.android.libraries.designsystem.components.list.ListItemContent 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.designsystem.utils.CommonDrawables
import io.element.android.libraries.matrix.api.core.RoomAlias 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.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.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.getBestName import io.element.android.libraries.matrix.api.room.getBestName
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
@ -137,13 +139,12 @@ fun RoomDetailsView(
) )
} }
is RoomDetailsType.Dm -> { is RoomDetailsType.Dm -> {
val member = state.roomType.roomMember DmHeaderSection(
UserProfileHeaderSection( me = state.roomType.me,
avatarUrl = state.roomAvatarUrl ?: member.avatarUrl, otherMember = state.roomType.roomMember,
userId = member.userId, roomName = state.roomName,
userName = state.roomName, openAvatarPreview = { name, avatarUrl ->
openAvatarPreview = { avatarUrl -> openAvatarPreview(name, avatarUrl)
openAvatarPreview(member.getBestName(), avatarUrl)
}, },
) )
} }
@ -359,6 +360,47 @@ private fun RoomHeaderSection(
} }
} }
@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) },
)
Spacer(modifier = Modifier.height(24.dp))
Text(
modifier = Modifier.clipToBounds(),
text = roomName,
style = ElementTheme.typography.fontHeadingLgBold,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(6.dp))
Text(
text = otherMember.userId.value,
style = ElementTheme.typography.fontBodyLgRegular,
color = MaterialTheme.colorScheme.secondary,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textAlign = TextAlign.Center,
)
Spacer(Modifier.height(40.dp))
}
}
@Composable @Composable
private fun BadgeList( private fun BadgeList(
isEncrypted: Boolean, isEncrypted: Boolean,

View file

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

View file

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

View file

@ -34,6 +34,8 @@ import androidx.compose.ui.tooling.preview.Preview
import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.text.toPx import io.element.android.libraries.designsystem.text.toPx
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
/** /**
* https://www.figma.com/design/A2pAEvTEpJZBiOPUlcMnKi/Settings-%2B-Room-Details-(new)?node-id=1787-56333 * https://www.figma.com/design/A2pAEvTEpJZBiOPUlcMnKi/Settings-%2B-Room-Details-(new)?node-id=1787-56333
@ -90,6 +92,7 @@ fun DmAvatars(
.clickable(enabled = otherUserAvatarData.url != null) { .clickable(enabled = otherUserAvatarData.url != null) {
otherUserAvatarData.url?.let { openOtherAvatarPreview(it) } otherUserAvatarData.url?.let { openOtherAvatarPreview(it) }
} }
.testTag(TestTags.memberDetailAvatar)
) )
} }
} }

View file

@ -63,3 +63,14 @@ fun MatrixRoom.getDirectRoomMember(roomMembersState: MatrixRoomMembersState): St
} }
} }
} }
@Composable
fun MatrixRoom.getCurrentRoomMember(roomMembersState: MatrixRoomMembersState): State<RoomMember?> {
val roomMembers = roomMembersState.roomMembers()
return remember(roomMembersState) {
derivedStateOf {
roomMembers
?.find { it.userId == sessionId }
}
}
}