Improve display name disambiguation rendering #2722
This commit is contained in:
parent
0d665388b8
commit
e26fe58fd3
14 changed files with 310 additions and 84 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,6 @@ internal fun TimelineItemEventRowTimestampPreview(
|
||||||
body = str,
|
body = str,
|
||||||
),
|
),
|
||||||
reactionsState = aTimelineItemReactions(count = 0),
|
reactionsState = aTimelineItemReactions(count = 0),
|
||||||
senderDisambiguatedDisplayName = "A sender",
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 ->
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue