Improve touch indicators for the user info UI in the timeline (#4482)
* For the user info in the timeline items, display the ripple effects according to the bounds and shape of the user avatar and display name * Fix ripple in other screens too
This commit is contained in:
committed by
GitHub
parent
a82f756c49
commit
38aa3f1e96
@@ -23,8 +23,8 @@ 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.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
@@ -37,6 +37,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||
import androidx.compose.ui.platform.ViewConfiguration
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
@@ -93,6 +94,7 @@ import io.element.android.libraries.matrix.ui.messages.reply.eventId
|
||||
import io.element.android.libraries.matrix.ui.messages.sender.SenderName
|
||||
import io.element.android.libraries.matrix.ui.messages.sender.SenderNameMode
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.wysiwyg.link.Link
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -314,6 +316,7 @@ private fun TimelineItemEventRowContent(
|
||||
event.senderId,
|
||||
event.senderProfile,
|
||||
event.senderAvatar,
|
||||
onUserDataClick,
|
||||
Modifier
|
||||
.constrainAs(sender) {
|
||||
top.linkTo(parent.top)
|
||||
@@ -321,13 +324,7 @@ private fun TimelineItemEventRowContent(
|
||||
start.linkTo(parent.start)
|
||||
}
|
||||
.padding(horizontal = 16.dp)
|
||||
.zIndex(1f)
|
||||
.clickable(onClick = onUserDataClick)
|
||||
// This is redundant when using talkback
|
||||
.clearAndSetSemantics {
|
||||
invisibleToUser()
|
||||
testTag = TestTags.timelineItemSenderInfo.value
|
||||
}
|
||||
.zIndex(1f),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -425,13 +422,31 @@ private fun MessageSenderInformation(
|
||||
senderId: UserId,
|
||||
senderProfile: ProfileTimelineDetails,
|
||||
senderAvatar: AvatarData,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val avatarColors = AvatarColorsProvider.provide(senderAvatar.id)
|
||||
Row(modifier = modifier) {
|
||||
Avatar(senderAvatar)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Row(
|
||||
modifier = modifier
|
||||
// Add external clickable modifier with no indicator so the touch target is larger than just the display name
|
||||
.clickable(onClick = onClick, enabled = true, interactionSource = remember { MutableInteractionSource() }, indication = null)
|
||||
.clearAndSetSemantics {
|
||||
invisibleToUser()
|
||||
}
|
||||
) {
|
||||
Avatar(
|
||||
modifier = Modifier
|
||||
.testTag(TestTags.timelineItemSenderAvatar)
|
||||
.clip(CircleShape)
|
||||
.clickable(onClick = onClick),
|
||||
avatarData = senderAvatar,
|
||||
)
|
||||
SenderName(
|
||||
modifier = Modifier
|
||||
.testTag(TestTags.timelineItemSenderName)
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 4.dp),
|
||||
senderId = senderId,
|
||||
senderProfile = senderProfile,
|
||||
senderNameMode = SenderNameMode.Timeline(avatarColors.foreground),
|
||||
|
||||
@@ -26,6 +26,7 @@ import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
@@ -42,7 +43,6 @@ import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.ui.strings.CommonPlurals
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@@ -60,7 +60,6 @@ fun TimelineItemReadReceiptView(
|
||||
ReadReceiptsAvatars(
|
||||
receipts = state.receipts,
|
||||
modifier = Modifier
|
||||
.testTag(TestTags.messageReadReceipts)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.clickable {
|
||||
onReadReceiptsClick()
|
||||
@@ -135,6 +134,7 @@ private fun ReadReceiptsAvatars(
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clearAndSetSemantics {
|
||||
testTag = TestTags.messageReadReceipts.value
|
||||
contentDescription = receiptDescription
|
||||
},
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp - avatarStrokeSize),
|
||||
|
||||
@@ -104,7 +104,7 @@ class MessagesViewTest {
|
||||
state = state,
|
||||
onRoomDetailsClick = callback,
|
||||
)
|
||||
rule.onNodeWithText(state.roomName.dataOrNull().orEmpty()).performClick()
|
||||
rule.onNodeWithText(state.roomName.dataOrNull().orEmpty(), useUnmergedTree = true).performClick()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,6 +206,7 @@ class MessagesViewTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(qualifiers = "h1024dp")
|
||||
fun `clicking on a read receipt list emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<ReadReceiptBottomSheetEvents>()
|
||||
val state = aMessagesState(
|
||||
@@ -229,7 +230,7 @@ class MessagesViewTest {
|
||||
rule.setMessagesView(
|
||||
state = state,
|
||||
)
|
||||
rule.onNodeWithTag(TestTags.messageReadReceipts.value).performClick()
|
||||
rule.onNodeWithTag(TestTags.messageReadReceipts.value, useUnmergedTree = true).performClick()
|
||||
eventsRecorder.assertSingle(ReadReceiptBottomSheetEvents.EventSelected(timelineItem))
|
||||
}
|
||||
|
||||
@@ -309,7 +310,7 @@ class MessagesViewTest {
|
||||
|
||||
@Test
|
||||
@Config(qualifiers = "h1024dp")
|
||||
fun `clicking on the sender of an Event invoke expected callback`() {
|
||||
fun `clicking on the avatar of the sender of an Event invoke expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<MessagesEvents>(expectEvents = false)
|
||||
val state = aMessagesState(
|
||||
eventSink = eventsRecorder
|
||||
@@ -322,7 +323,26 @@ class MessagesViewTest {
|
||||
state = state,
|
||||
onUserDataClick = callback,
|
||||
)
|
||||
rule.onNodeWithTag(TestTags.timelineItemSenderInfo.value).performClick()
|
||||
rule.onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(qualifiers = "h1024dp")
|
||||
fun `clicking on the display name of the sender of an Event invoke expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<MessagesEvents>(expectEvents = false)
|
||||
val state = aMessagesState(
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
val timelineItem = state.timelineState.timelineItems.first()
|
||||
ensureCalledOnceWithParam(
|
||||
param = (timelineItem as TimelineItem.Event).senderId
|
||||
) { callback ->
|
||||
rule.setMessagesView(
|
||||
state = state,
|
||||
onUserDataClick = callback,
|
||||
)
|
||||
rule.onNodeWithTag(TestTags.timelineItemSenderName.value, useUnmergedTree = true).performClick()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,9 +13,11 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
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.clipToBounds
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -58,6 +60,7 @@ fun UserProfileHeaderSection(
|
||||
Avatar(
|
||||
avatarData = AvatarData(userId.value, userName, avatarUrl, AvatarSize.UserHeader),
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
|
||||
.testTag(TestTags.memberDetailAvatar)
|
||||
)
|
||||
|
||||
@@ -34,8 +34,8 @@ fun InviteSenderView(
|
||||
modifier = modifier,
|
||||
) {
|
||||
Box(modifier = Modifier.padding(vertical = 2.dp)) {
|
||||
Avatar(avatarData = inviteSender.avatarData)
|
||||
}
|
||||
Avatar(avatarData = inviteSender.avatarData)
|
||||
}
|
||||
Text(
|
||||
text = inviteSender.annotatedString(),
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
|
||||
@@ -105,7 +105,8 @@ object TestTags {
|
||||
/**
|
||||
* Timeline item.
|
||||
*/
|
||||
val timelineItemSenderInfo = TestTag("timeline_item-sender_info")
|
||||
val timelineItemSenderAvatar = TestTag("timeline_item-sender_avatar")
|
||||
val timelineItemSenderName = TestTag("timeline_item-sender_name")
|
||||
|
||||
/**
|
||||
* Search field.
|
||||
|
||||
Reference in New Issue
Block a user