Improve display name disambiguation rendering #2722

This commit is contained in:
Benoit Marty 2024-04-22 15:47:49 +02:00 committed by Benoit Marty
parent 0d665388b8
commit e26fe58fd3
14 changed files with 310 additions and 84 deletions

View file

@ -48,8 +48,11 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
), ),
anActionListState().copy( anActionListState().copy(
target = ActionListState.Target.Success( target = ActionListState.Target.Success(
event = aTimelineItemEvent(content = aTimelineItemImageContent()).copy( event = aTimelineItemEvent(
reactionsState = reactionsState content = aTimelineItemImageContent(),
displayNameAmbiguous = true,
).copy(
reactionsState = reactionsState,
), ),
displayEmojiReactions = true, displayEmojiReactions = true,
actions = aTimelineItemActionList(), actions = aTimelineItemActionList(),
@ -142,6 +145,7 @@ fun aTimelineItemActionList(): ImmutableList<TimelineItemAction> {
TimelineItemAction.ViewSource, TimelineItemAction.ViewSource,
) )
} }
fun aTimelineItemPollActionList(): ImmutableList<TimelineItemAction> { fun aTimelineItemPollActionList(): ImmutableList<TimelineItemAction> {
return persistentListOf( return persistentListOf(
TimelineItemAction.EndPoll, TimelineItemAction.EndPoll,

View file

@ -55,6 +55,8 @@ import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme 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.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.sender.SenderName
import io.element.android.features.messages.impl.sender.SenderNameMode
import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
@ -143,8 +145,8 @@ fun ActionListView(
onEmojiReactionClicked = ::onEmojiReactionClicked, onEmojiReactionClicked = ::onEmojiReactionClicked,
onCustomReactionClicked = ::onCustomReactionClicked, onCustomReactionClicked = ::onCustomReactionClicked,
modifier = Modifier modifier = Modifier
.navigationBarsPadding() .navigationBarsPadding()
.imePadding() .imePadding()
) )
} }
} }
@ -175,8 +177,8 @@ private fun SheetContent(
MessageSummary( MessageSummary(
event = target.event, event = target.event,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
) )
Spacer(modifier = Modifier.height(14.dp)) Spacer(modifier = Modifier.height(14.dp))
HorizontalDivider() HorizontalDivider()
@ -268,15 +270,11 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
icon() icon()
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Row { SenderName(
if (event.senderDisambiguatedDisplayName != null) { senderId = event.senderId,
Text( senderProfile = event.senderProfile,
text = event.senderDisambiguatedDisplayName, senderNameMode = SenderNameMode.ActionList,
style = ElementTheme.typography.fontBodySmMedium, )
color = MaterialTheme.colorScheme.primary
)
}
}
content() content()
} }
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
@ -324,13 +322,13 @@ private fun EmojiReactionsRow(
contentDescription = stringResource(id = CommonStrings.a11y_react_with_other_emojis), contentDescription = stringResource(id = CommonStrings.a11y_react_with_other_emojis),
tint = MaterialTheme.colorScheme.secondary, tint = MaterialTheme.colorScheme.secondary,
modifier = Modifier modifier = Modifier
.size(24.dp) .size(24.dp)
.clickable( .clickable(
enabled = true, enabled = true,
onClick = onCustomReactionClicked, onClick = onCustomReactionClicked,
indication = rememberRipple(bounded = false, radius = emojiRippleRadius), indication = rememberRipple(bounded = false, radius = emojiRippleRadius),
interactionSource = remember { MutableInteractionSource() } interactionSource = remember { MutableInteractionSource() }
) )
) )
} }
} }
@ -354,11 +352,11 @@ private fun EmojiButton(
} }
Box( Box(
modifier = Modifier modifier = Modifier
.size(48.dp) .size(48.dp)
.background(backgroundColor, CircleShape) .background(backgroundColor, CircleShape)
.clearAndSetSemantics { .clearAndSetSemantics {
contentDescription = description contentDescription = description
}, },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(

View file

@ -0,0 +1,134 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.sender
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
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.clipToBounds
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
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.matrix.api.timeline.item.event.ProfileTimelineDetails
// https://www.figma.com/file/Ni6Ii8YKtmXCKYNE90cC67/Timeline-(new)?type=design&node-id=917-80169&mode=design&t=A0CJCBbMqR8NOwUQ-0
@Composable
fun SenderName(
senderId: UserId,
senderProfile: ProfileTimelineDetails,
senderNameMode: SenderNameMode,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
when (senderProfile) {
is ProfileTimelineDetails.Error,
ProfileTimelineDetails.Pending,
ProfileTimelineDetails.Unavailable -> {
MainText(text = senderId.value, mode = senderNameMode)
}
is ProfileTimelineDetails.Ready -> {
val displayName = senderProfile.displayName
if (displayName.isNullOrEmpty()) {
MainText(text = senderId.value, mode = senderNameMode)
} else {
MainText(text = displayName, mode = senderNameMode)
if (senderProfile.displayNameAmbiguous) {
SecondaryText(text = senderId.value, mode = senderNameMode)
}
}
}
}
}
}
@Composable
private fun RowScope.MainText(
text: String,
mode: SenderNameMode,
) {
val style = when (mode) {
is SenderNameMode.Timeline -> ElementTheme.typography.fontBodyMdMedium
SenderNameMode.ActionList,
SenderNameMode.Reply -> ElementTheme.typography.fontBodySmMedium
}
val modifier = when (mode) {
is SenderNameMode.Timeline -> Modifier.alignByBaseline()
SenderNameMode.ActionList,
SenderNameMode.Reply -> Modifier
}
val color = when (mode) {
is SenderNameMode.Timeline -> mode.mainColor
SenderNameMode.ActionList,
SenderNameMode.Reply -> MaterialTheme.colorScheme.primary
}
Text(
modifier = modifier.clipToBounds(),
text = text,
style = style,
color = color,
overflow = TextOverflow.Ellipsis,
)
}
@Composable
private fun RowScope.SecondaryText(
text: String,
mode: SenderNameMode,
) {
val style = when (mode) {
is SenderNameMode.Timeline -> ElementTheme.typography.fontBodySmRegular
SenderNameMode.ActionList,
SenderNameMode.Reply -> ElementTheme.typography.fontBodyXsRegular
}
val modifier = when (mode) {
is SenderNameMode.Timeline -> Modifier.alignByBaseline()
SenderNameMode.ActionList,
SenderNameMode.Reply -> Modifier
}
Text(
modifier = modifier.clipToBounds(),
text = text,
style = style,
color = MaterialTheme.colorScheme.secondary,
overflow = TextOverflow.Ellipsis,
)
}
@PreviewsDayNight
@Composable
internal fun SenderNamePreview(
@PreviewParameter(SenderNameDataProvider::class) senderNameData: SenderNameData,
) = ElementPreview {
SenderName(
senderId = senderNameData.userId,
senderProfile = senderNameData.profileTimelineDetails,
senderNameMode = senderNameData.senderNameMode,
)
}

View file

@ -0,0 +1,57 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.sender
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.messages.impl.timeline.components.aProfileTimelineDetailsReady
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
data class SenderNameData(
val userId: UserId,
val profileTimelineDetails: ProfileTimelineDetails,
val senderNameMode: SenderNameMode,
)
open class SenderNameDataProvider : PreviewParameterProvider<SenderNameData> {
override val values: Sequence<SenderNameData>
get() = sequenceOf(
SenderNameMode.Timeline(mainColor = Color.Red),
SenderNameMode.Reply,
SenderNameMode.ActionList,
)
.flatMap { senderNameMode ->
sequenceOf(
aSenderNameData(senderNameMode = senderNameMode),
aSenderNameData(displayNameAmbiguous = true, senderNameMode = senderNameMode),
SenderNameData(UserId("@alice:${senderNameMode.javaClass.name.lowercase()}"), ProfileTimelineDetails.Unavailable, senderNameMode = senderNameMode),
)
}
}
fun aSenderNameData(
displayNameAmbiguous: Boolean = false,
senderNameMode: SenderNameMode,
) = SenderNameData(
userId = UserId("@alice:${senderNameMode.javaClass.name.lowercase()}"),
profileTimelineDetails = aProfileTimelineDetailsReady(
displayName = "Alice ${senderNameMode.javaClass.name}",
displayNameAmbiguous = displayNameAmbiguous,
),
senderNameMode = senderNameMode,
)

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.sender
import androidx.compose.ui.graphics.Color
sealed interface SenderNameMode {
data class Timeline(val mainColor: Color) : SenderNameMode
data object Reply : SenderNameMode
data object ActionList : SenderNameMode
}

View file

@ -16,6 +16,7 @@
package io.element.android.features.messages.impl.timeline package io.element.android.features.messages.impl.timeline
import io.element.android.features.messages.impl.timeline.components.aProfileTimelineDetailsReady
import io.element.android.features.messages.impl.timeline.components.receipt.aReadReceiptData import io.element.android.features.messages.impl.timeline.components.receipt.aReadReceiptData
import io.element.android.features.messages.impl.timeline.model.InReplyToDetails import io.element.android.features.messages.impl.timeline.model.InReplyToDetails
import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.NewEventState
@ -130,7 +131,8 @@ internal fun aTimelineItemEvent(
transactionId: TransactionId? = null, transactionId: TransactionId? = null,
isMine: Boolean = false, isMine: Boolean = false,
isEditable: Boolean = false, isEditable: Boolean = false,
senderDisambiguatedDisplayName: String = "Sender", senderDisplayName: String = "Sender",
displayNameAmbiguous: Boolean = false,
content: TimelineItemEventContent = aTimelineItemTextContent(), content: TimelineItemEventContent = aTimelineItemTextContent(),
groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None, groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
sendState: LocalEventSendState? = null, sendState: LocalEventSendState? = null,
@ -152,7 +154,10 @@ internal fun aTimelineItemEvent(
sentTime = "12:34", sentTime = "12:34",
isMine = isMine, isMine = isMine,
isEditable = isEditable, isEditable = isEditable,
senderDisambiguatedDisplayName = senderDisambiguatedDisplayName, senderProfile = aProfileTimelineDetailsReady(
displayName = senderDisplayName,
displayNameAmbiguous = displayNameAmbiguous,
),
groupPosition = groupPosition, groupPosition = groupPosition,
localSendState = sendState, localSendState = sendState,
inReplyTo = inReplyTo, inReplyTo = inReplyTo,

View file

@ -69,6 +69,8 @@ import androidx.constraintlayout.compose.ConstrainScope
import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.ConstraintLayout
import io.element.android.compound.theme.ElementTheme 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.messages.impl.sender.SenderName
import io.element.android.features.messages.impl.sender.SenderNameMode
import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelineEvents
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
@ -106,6 +108,7 @@ import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail
import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.TestTags
@ -292,7 +295,8 @@ private fun TimelineItemEventRowContent(
val avatarStrokeSize = 3.dp val avatarStrokeSize = 3.dp
if (event.showSenderInformation && !timelineRoomInfo.isDm) { if (event.showSenderInformation && !timelineRoomInfo.isDm) {
MessageSenderInformation( MessageSenderInformation(
event.safeSenderName, event.senderId,
event.senderProfile,
event.senderAvatar, event.senderAvatar,
avatarStrokeSize, avatarStrokeSize,
Modifier Modifier
@ -372,7 +376,8 @@ private fun TimelineItemEventRowContent(
@Composable @Composable
private fun MessageSenderInformation( private fun MessageSenderInformation(
sender: String, senderId: UserId,
senderProfile: ProfileTimelineDetails,
senderAvatar: AvatarData, senderAvatar: AvatarData,
avatarStrokeSize: Dp, avatarStrokeSize: Dp,
modifier: Modifier = Modifier modifier: Modifier = Modifier
@ -399,13 +404,10 @@ private fun MessageSenderInformation(
Row { Row {
Avatar(senderAvatar) Avatar(senderAvatar)
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
Text( SenderName(
modifier = Modifier.clipToBounds(), senderId = senderId,
text = sender, senderProfile = senderProfile,
maxLines = 1, senderNameMode = SenderNameMode.Timeline(avatarColors.foreground),
overflow = TextOverflow.Ellipsis,
color = avatarColors.foreground,
style = ElementTheme.typography.fontBodyMdMedium,
) )
} }
} }
@ -562,10 +564,10 @@ private fun MessageEventBubbleContent(
} }
} }
val inReplyTo = @Composable { inReplyTo: InReplyToDetails -> val inReplyTo = @Composable { inReplyTo: InReplyToDetails ->
val senderDisambiguatedDisplayName = inReplyTo.senderProfile.getDisambiguatedDisplayName(inReplyTo.senderId)
val topPadding = if (showThreadDecoration) 0.dp else 8.dp val topPadding = if (showThreadDecoration) 0.dp else 8.dp
ReplyToContent( ReplyToContent(
senderDisambiguatedDisplayName = senderDisambiguatedDisplayName, senderId = inReplyTo.senderId,
senderProfile = inReplyTo.senderProfile,
metadata = inReplyTo.metadata(), metadata = inReplyTo.metadata(),
modifier = Modifier modifier = Modifier
.padding(top = topPadding, start = 8.dp, end = 8.dp) .padding(top = topPadding, start = 8.dp, end = 8.dp)
@ -610,7 +612,8 @@ private fun MessageEventBubbleContent(
@Composable @Composable
private fun ReplyToContent( private fun ReplyToContent(
senderDisambiguatedDisplayName: String, senderId: UserId,
senderProfile: ProfileTimelineDetails,
metadata: InReplyToMetadata?, metadata: InReplyToMetadata?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -634,18 +637,15 @@ private fun ReplyToContent(
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
} }
val a11InReplyToText = stringResource(CommonStrings.common_in_reply_to, senderDisambiguatedDisplayName) val a11InReplyToText = stringResource(CommonStrings.common_in_reply_to, senderProfile.getDisambiguatedDisplayName(senderId))
Column(verticalArrangement = Arrangement.SpaceBetween) { Column(verticalArrangement = Arrangement.SpaceBetween) {
Text( SenderName(
senderId = senderId,
senderProfile = senderProfile,
senderNameMode = SenderNameMode.Reply,
modifier = Modifier.semantics { modifier = Modifier.semantics {
contentDescription = a11InReplyToText contentDescription = a11InReplyToText
}, },
text = senderDisambiguatedDisplayName,
style = ElementTheme.typography.fontBodySmMedium,
textAlign = TextAlign.Start,
color = ElementTheme.materialColors.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
) )
ReplyToContentText(metadata) ReplyToContentText(metadata)
} }

View file

@ -28,7 +28,10 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy
internal fun TimelineItemEventRowDisambiguatedPreview( internal fun TimelineItemEventRowDisambiguatedPreview(
@PreviewParameter(InReplyToDetailsDisambiguatedProvider::class) inReplyToDetails: InReplyToDetails, @PreviewParameter(InReplyToDetailsDisambiguatedProvider::class) inReplyToDetails: InReplyToDetails,
) = ElementPreview { ) = ElementPreview {
TimelineItemEventRowWithReplyContentToPreview(inReplyToDetails) TimelineItemEventRowWithReplyContentToPreview(
inReplyToDetails = inReplyToDetails,
displayNameAmbiguous = true,
)
} }
class InReplyToDetailsDisambiguatedProvider : InReplyToDetailsProvider() { class InReplyToDetailsDisambiguatedProvider : InReplyToDetailsProvider() {

View file

@ -27,7 +27,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight
internal fun TimelineItemEventRowLongSenderNamePreview() = ElementPreviewLight { internal fun TimelineItemEventRowLongSenderNamePreview() = ElementPreviewLight {
ATimelineItemEventRow( ATimelineItemEventRow(
event = aTimelineItemEvent( event = aTimelineItemEvent(
senderDisambiguatedDisplayName = "a long sender display name to test single line and ellipsis at the end of the line", senderDisplayName = "a long sender display name to test single line and ellipsis at the end of the line",
), ),
) )
} }

View file

@ -43,7 +43,6 @@ internal fun TimelineItemEventRowTimestampPreview(
body = str, body = str,
), ),
reactionsState = aTimelineItemReactions(count = 0), reactionsState = aTimelineItemReactions(count = 0),
senderDisambiguatedDisplayName = "A sender",
), ),
) )
} }

View file

@ -59,7 +59,10 @@ internal fun TimelineItemEventRowWithReplyPreview(
} }
@Composable @Composable
internal fun TimelineItemEventRowWithReplyContentToPreview(inReplyToDetails: InReplyToDetails) { internal fun TimelineItemEventRowWithReplyContentToPreview(
inReplyToDetails: InReplyToDetails,
displayNameAmbiguous: Boolean = false,
) {
Column { Column {
sequenceOf(false, true).forEach { sequenceOf(false, true).forEach {
ATimelineItemEventRow( ATimelineItemEventRow(
@ -70,6 +73,7 @@ internal fun TimelineItemEventRowWithReplyContentToPreview(inReplyToDetails: InR
body = "A reply." body = "A reply."
), ),
inReplyTo = inReplyToDetails, inReplyTo = inReplyToDetails,
displayNameAmbiguous = displayNameAmbiguous,
groupPosition = TimelineItemGroupPosition.First, groupPosition = TimelineItemGroupPosition.First,
), ),
) )
@ -81,6 +85,7 @@ internal fun TimelineItemEventRowWithReplyContentToPreview(inReplyToDetails: InR
aspectRatio = 2.5f aspectRatio = 2.5f
), ),
inReplyTo = inReplyToDetails, inReplyTo = inReplyToDetails,
displayNameAmbiguous = displayNameAmbiguous,
isThreaded = true, isThreaded = true,
groupPosition = TimelineItemGroupPosition.Last, groupPosition = TimelineItemGroupPosition.Last,
), ),
@ -169,11 +174,19 @@ open class InReplyToDetailsProvider : PreviewParameterProvider<InReplyToDetails>
eventId = EventId("\$event"), eventId = EventId("\$event"),
eventContent = eventContent, eventContent = eventContent,
senderId = UserId("@Sender:domain"), senderId = UserId("@Sender:domain"),
senderProfile = ProfileTimelineDetails.Ready( senderProfile = aProfileTimelineDetailsReady(
displayName = "Sender",
displayNameAmbiguous = displayNameAmbiguous, displayNameAmbiguous = displayNameAmbiguous,
avatarUrl = null,
), ),
textContent = (eventContent as? MessageContent)?.body.orEmpty(), textContent = (eventContent as? MessageContent)?.body.orEmpty(),
) )
} }
internal fun aProfileTimelineDetailsReady(
displayName: String? = "Sender",
displayNameAmbiguous: Boolean = false,
avatarUrl: String? = null,
) = ProfileTimelineDetails.Ready(
displayName = displayName,
displayNameAmbiguous = displayNameAmbiguous,
avatarUrl = avatarUrl,
)

View file

@ -33,7 +33,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import java.text.DateFormat import java.text.DateFormat
@ -55,15 +55,14 @@ class TimelineItemEventFactory @Inject constructor(
val currentSender = currentTimelineItem.event.sender val currentSender = currentTimelineItem.event.sender
val groupPosition = val groupPosition =
computeGroupPosition(currentTimelineItem, timelineItems, index) computeGroupPosition(currentTimelineItem, timelineItems, index)
val (senderDisambiguatedDisplayName, senderAvatarUrl) = currentTimelineItem.getSenderInfo() val senderProfile = currentTimelineItem.event.senderProfile
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT) val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
val sentTime = timeFormatter.format(Date(currentTimelineItem.event.timestamp)) val sentTime = timeFormatter.format(Date(currentTimelineItem.event.timestamp))
val senderAvatarData = AvatarData( val senderAvatarData = AvatarData(
id = currentSender.value, id = currentSender.value,
name = senderDisambiguatedDisplayName ?: currentSender.value, name = senderProfile.getDisambiguatedDisplayName(currentSender),
url = senderAvatarUrl, url = senderProfile.getAvatarUrl(),
size = AvatarSize.TimelineSender size = AvatarSize.TimelineSender
) )
currentTimelineItem.event currentTimelineItem.event
@ -72,7 +71,7 @@ class TimelineItemEventFactory @Inject constructor(
eventId = currentTimelineItem.eventId, eventId = currentTimelineItem.eventId,
transactionId = currentTimelineItem.transactionId, transactionId = currentTimelineItem.transactionId,
senderId = currentSender, senderId = currentSender,
senderDisambiguatedDisplayName = senderDisambiguatedDisplayName, senderProfile = senderProfile,
senderAvatar = senderAvatarData, senderAvatar = senderAvatarData,
content = contentFactory.create(currentTimelineItem.event), content = contentFactory.create(currentTimelineItem.event),
isMine = currentTimelineItem.event.isOwn, isMine = currentTimelineItem.event.isOwn,
@ -99,26 +98,6 @@ class TimelineItemEventFactory @Inject constructor(
) )
} }
private fun MatrixTimelineItem.Event.getSenderInfo(): Pair<String?, String?> {
val senderDisambiguatedDisplayName: String?
val senderAvatarUrl: String?
when (val senderProfile = event.senderProfile) {
ProfileTimelineDetails.Unavailable,
ProfileTimelineDetails.Pending,
is ProfileTimelineDetails.Error -> {
senderDisambiguatedDisplayName = null
senderAvatarUrl = null
}
is ProfileTimelineDetails.Ready -> {
senderDisambiguatedDisplayName = senderProfile.getDisambiguatedDisplayName(event.sender)
senderAvatarUrl = senderProfile.avatarUrl
}
}
return senderDisambiguatedDisplayName to senderAvatarUrl
}
private fun MatrixTimelineItem.Event.computeReactionsState(): TimelineItemReactions { private fun MatrixTimelineItem.Event.computeReactionsState(): TimelineItemReactions {
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT) val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
var aggregatedReactions = event.reactions.map { reaction -> var aggregatedReactions = event.reactions.map { reaction ->

View file

@ -27,7 +27,9 @@ import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@Immutable @Immutable
@ -57,7 +59,7 @@ sealed interface TimelineItem {
val eventId: EventId? = null, val eventId: EventId? = null,
val transactionId: TransactionId? = null, val transactionId: TransactionId? = null,
val senderId: UserId, val senderId: UserId,
val senderDisambiguatedDisplayName: String?, val senderProfile: ProfileTimelineDetails,
val senderAvatar: AvatarData, val senderAvatar: AvatarData,
val content: TimelineItemEventContent, val content: TimelineItemEventContent,
val sentTime: String = "", val sentTime: String = "",
@ -74,7 +76,7 @@ sealed interface TimelineItem {
) : TimelineItem { ) : TimelineItem {
val showSenderInformation = groupPosition.isNew() && !isMine val showSenderInformation = groupPosition.isNew() && !isMine
val safeSenderName: String = senderDisambiguatedDisplayName ?: senderId.value val safeSenderName: String = senderProfile.getDisambiguatedDisplayName(senderId)
val failedToSend: Boolean = localSendState is LocalEventSendState.SendingFailed val failedToSend: Boolean = localSendState is LocalEventSendState.SendingFailed

View file

@ -52,3 +52,10 @@ fun ProfileTimelineDetails.getDisambiguatedDisplayName(userId: UserId): String {
else -> userId.value else -> userId.value
} }
} }
fun ProfileTimelineDetails.getAvatarUrl(): String? {
return when (this) {
is ProfileTimelineDetails.Ready -> avatarUrl
else -> null
}
}