Merge pull request #2739 from element-hq/feature/bma/displayNameInStateEvent

Display name in state event and improve display name disambiguation rendering
This commit is contained in:
Benoit Marty 2024-04-23 14:12:33 +02:00 committed by GitHub
commit 5fc95f56ab
407 changed files with 602 additions and 234 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
@ -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.senderDisplayName != null) { senderId = event.senderId,
Text( senderProfile = event.senderProfile,
text = event.senderDisplayName, 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))

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,66 @@
/*
* 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(
senderNameMode = senderNameMode,
displayNameAmbiguous = true,
),
SenderNameData(
senderNameMode = senderNameMode,
userId = UserId("@alice:${senderNameMode.javaClass.simpleName.lowercase()}"),
profileTimelineDetails = ProfileTimelineDetails.Unavailable,
),
)
}
}
private fun aSenderNameData(
senderNameMode: SenderNameMode,
displayNameAmbiguous: Boolean = false,
) = SenderNameData(
userId = UserId("@alice:${senderNameMode.javaClass.simpleName.lowercase()}"),
profileTimelineDetails = aProfileTimelineDetailsReady(
displayName = "Alice ${senderNameMode.javaClass.simpleName}",
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
@ -131,6 +132,7 @@ internal fun aTimelineItemEvent(
isMine: Boolean = false, isMine: Boolean = false,
isEditable: Boolean = false, isEditable: Boolean = false,
senderDisplayName: 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,
senderDisplayName = senderDisplayName, 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,8 @@ 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.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
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -291,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
@ -371,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
@ -398,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,
) )
} }
} }
@ -561,10 +564,10 @@ private fun MessageEventBubbleContent(
} }
} }
val inReplyTo = @Composable { inReplyTo: InReplyToDetails -> val inReplyTo = @Composable { inReplyTo: InReplyToDetails ->
val senderName = inReplyTo.senderDisplayName ?: inReplyTo.senderId.value
val topPadding = if (showThreadDecoration) 0.dp else 8.dp val topPadding = if (showThreadDecoration) 0.dp else 8.dp
ReplyToContent( ReplyToContent(
senderName = senderName, 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)
@ -609,7 +612,8 @@ private fun MessageEventBubbleContent(
@Composable @Composable
private fun ReplyToContent( private fun ReplyToContent(
senderName: String, senderId: UserId,
senderProfile: ProfileTimelineDetails,
metadata: InReplyToMetadata?, metadata: InReplyToMetadata?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -633,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, senderName) 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 = senderName,
style = ElementTheme.typography.fontBodySmMedium,
textAlign = TextAlign.Start,
color = ElementTheme.materialColors.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
) )
ReplyToContentText(metadata) ReplyToContentText(metadata)
} }

View file

@ -0,0 +1,50 @@
/*
* 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.timeline.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.messages.impl.timeline.model.InReplyToDetails
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
@PreviewsDayNight
@Composable
internal fun TimelineItemEventRowDisambiguatedPreview(
@PreviewParameter(InReplyToDetailsDisambiguatedProvider::class) inReplyToDetails: InReplyToDetails,
) = ElementPreview {
TimelineItemEventRowWithReplyContentToPreview(
inReplyToDetails = inReplyToDetails,
displayNameAmbiguous = true,
)
}
class InReplyToDetailsDisambiguatedProvider : InReplyToDetailsProvider() {
override val values: Sequence<InReplyToDetails>
get() = sequenceOf(
aMessageContent(
body = "Message which are being replied.",
type = TextMessageType("Message which are being replied.", null)
),
).map {
aInReplyToDetails(
displayNameAmbiguous = true,
eventContent = it,
)
}
}

View file

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

View file

@ -42,6 +42,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.MessageConten
import io.element.android.libraries.matrix.api.timeline.item.event.MessageType import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessageType import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
@ -58,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(
@ -69,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,
), ),
) )
@ -80,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,
), ),
@ -150,7 +156,7 @@ open class InReplyToDetailsProvider : PreviewParameterProvider<InReplyToDetails>
) )
} }
private fun aMessageContent( protected fun aMessageContent(
body: String, body: String,
type: MessageType, type: MessageType,
) = MessageContent( ) = MessageContent(
@ -163,12 +169,24 @@ open class InReplyToDetailsProvider : PreviewParameterProvider<InReplyToDetails>
protected fun aInReplyToDetails( protected fun aInReplyToDetails(
eventContent: EventContent, eventContent: EventContent,
displayNameAmbiguous: Boolean = false,
) = InReplyToDetails( ) = InReplyToDetails(
eventId = EventId("\$event"), eventId = EventId("\$event"),
eventContent = eventContent, eventContent = eventContent,
senderId = UserId("@Sender:domain"), senderId = UserId("@Sender:domain"),
senderDisplayName = "Sender", senderProfile = aProfileTimelineDetailsReady(
senderAvatarUrl = null, displayNameAmbiguous = displayNameAmbiguous,
),
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

@ -52,8 +52,12 @@ class TimelineItemContentFactory @Inject constructor(
is FailedToParseMessageLikeContent -> failedToParseMessageFactory.create(itemContent) is FailedToParseMessageLikeContent -> failedToParseMessageFactory.create(itemContent)
is FailedToParseStateContent -> failedToParseStateFactory.create(itemContent) is FailedToParseStateContent -> failedToParseStateFactory.create(itemContent)
is MessageContent -> { is MessageContent -> {
val senderDisplayName = eventTimelineItem.senderProfile.getDisambiguatedDisplayName(eventTimelineItem.sender) val senderDisambiguatedDisplayName = eventTimelineItem.senderProfile.getDisambiguatedDisplayName(eventTimelineItem.sender)
messageFactory.create(itemContent, senderDisplayName, eventTimelineItem.eventId) messageFactory.create(
content = itemContent,
senderDisambiguatedDisplayName = senderDisambiguatedDisplayName,
eventId = eventTimelineItem.eventId,
)
} }
is ProfileChangeContent -> profileChangeFactory.create(eventTimelineItem) is ProfileChangeContent -> profileChangeFactory.create(eventTimelineItem)
is RedactedContent -> redactedMessageFactory.create(itemContent) is RedactedContent -> redactedMessageFactory.create(itemContent)

View file

@ -70,17 +70,21 @@ class TimelineItemContentMessageFactory @Inject constructor(
private val htmlConverterProvider: HtmlConverterProvider, private val htmlConverterProvider: HtmlConverterProvider,
private val permalinkParser: PermalinkParser, private val permalinkParser: PermalinkParser,
) { ) {
suspend fun create(content: MessageContent, senderDisplayName: String, eventId: EventId?): TimelineItemEventContent { suspend fun create(
content: MessageContent,
senderDisambiguatedDisplayName: String,
eventId: EventId?,
): TimelineItemEventContent {
return when (val messageType = content.type) { return when (val messageType = content.type) {
is EmoteMessageType -> { is EmoteMessageType -> {
val emoteBody = "* $senderDisplayName ${messageType.body.trimEnd()}" val emoteBody = "* $senderDisambiguatedDisplayName ${messageType.body.trimEnd()}"
TimelineItemEmoteContent( TimelineItemEmoteContent(
body = emoteBody, body = emoteBody,
htmlDocument = messageType.formatted?.toHtmlDocument( htmlDocument = messageType.formatted?.toHtmlDocument(
permalinkParser = permalinkParser, permalinkParser = permalinkParser,
prefix = "* $senderDisplayName", prefix = "* $senderDisambiguatedDisplayName",
), ),
formattedBody = parseHtml(messageType.formatted, prefix = "* $senderDisplayName") ?: emoteBody.withLinks(), formattedBody = parseHtml(messageType.formatted, prefix = "* $senderDisambiguatedDisplayName") ?: emoteBody.withLinks(),
isEdited = content.isEdited, isEdited = content.isEdited,
) )
} }

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 (senderDisplayName, 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 = senderDisplayName ?: 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,
senderDisplayName = senderDisplayName, 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 senderDisplayName: String?
val senderAvatarUrl: String?
when (val senderProfile = event.senderProfile) {
ProfileTimelineDetails.Unavailable,
ProfileTimelineDetails.Pending,
is ProfileTimelineDetails.Error -> {
senderDisplayName = null
senderAvatarUrl = null
}
is ProfileTimelineDetails.Ready -> {
senderDisplayName = senderProfile.getDisambiguatedDisplayName(event.sender)
senderAvatarUrl = senderProfile.avatarUrl
}
}
return senderDisplayName 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

@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
import io.element.android.libraries.matrix.ui.messages.toPlainText import io.element.android.libraries.matrix.ui.messages.toPlainText
@ -29,8 +30,7 @@ import io.element.android.libraries.matrix.ui.messages.toPlainText
data class InReplyToDetails( data class InReplyToDetails(
val eventId: EventId, val eventId: EventId,
val senderId: UserId, val senderId: UserId,
val senderDisplayName: String?, val senderProfile: ProfileTimelineDetails,
val senderAvatarUrl: String?,
val eventContent: EventContent?, val eventContent: EventContent?,
val textContent: String?, val textContent: String?,
) )
@ -41,8 +41,7 @@ fun InReplyTo.map(
is InReplyTo.Ready -> InReplyToDetails( is InReplyTo.Ready -> InReplyToDetails(
eventId = eventId, eventId = eventId,
senderId = senderId, senderId = senderId,
senderDisplayName = senderDisplayName, senderProfile = senderProfile,
senderAvatarUrl = senderAvatarUrl,
eventContent = content, eventContent = content,
textContent = when (content) { textContent = when (content) {
is MessageContent -> { is MessageContent -> {

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 senderDisplayName: 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 = senderDisplayName ?: senderId.value val safeSenderName: String = senderProfile.getDisambiguatedDisplayName(senderId)
val failedToSend: Boolean = localSendState is LocalEventSendState.SendingFailed val failedToSend: Boolean = localSendState is LocalEventSendState.SendingFailed

View file

@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.fixtures
import io.element.android.features.messages.impl.timeline.aTimelineItemDebugInfo import io.element.android.features.messages.impl.timeline.aTimelineItemDebugInfo
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
import io.element.android.features.messages.impl.timeline.components.aProfileTimelineDetailsReady
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.ReadReceiptData import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem
@ -48,7 +49,7 @@ internal fun aMessageEvent(
id = eventId?.value.orEmpty(), id = eventId?.value.orEmpty(),
eventId = eventId, eventId = eventId,
senderId = A_USER_ID, senderId = A_USER_ID,
senderDisplayName = A_USER_NAME, senderProfile = aProfileTimelineDetailsReady(displayName = A_USER_NAME),
senderAvatar = AvatarData(A_USER_ID.value, A_USER_NAME, size = AvatarSize.TimelineSender), senderAvatar = AvatarData(A_USER_ID.value, A_USER_NAME, size = AvatarSize.TimelineSender),
content = content, content = content,
sentTime = "", sentTime = "",

View file

@ -82,7 +82,7 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory() val sut = createTimelineItemContentMessageFactory()
val result = sut.create( val result = sut.create(
content = createMessageContent(type = OtherMessageType(msgType = "a_type", body = "body")), content = createMessageContent(type = OtherMessageType(msgType = "a_type", body = "body")),
senderDisplayName = "Bob", senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemTextContent( val expected = TimelineItemTextContent(
@ -100,7 +100,7 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory() val sut = createTimelineItemContentMessageFactory()
val result = sut.create( val result = sut.create(
content = createMessageContent(type = LocationMessageType("body", "geo:1,2", "description")), content = createMessageContent(type = LocationMessageType("body", "geo:1,2", "description")),
senderDisplayName = "Bob", senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemLocationContent( val expected = TimelineItemLocationContent(
@ -116,7 +116,7 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory() val sut = createTimelineItemContentMessageFactory()
val result = sut.create( val result = sut.create(
content = createMessageContent(type = LocationMessageType("body", "", null)), content = createMessageContent(type = LocationMessageType("body", "", null)),
senderDisplayName = "Bob", senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemTextContent( val expected = TimelineItemTextContent(
@ -134,7 +134,7 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory() val sut = createTimelineItemContentMessageFactory()
val result = sut.create( val result = sut.create(
content = createMessageContent(type = TextMessageType("body", null)), content = createMessageContent(type = TextMessageType("body", null)),
senderDisplayName = "Bob", senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemTextContent( val expected = TimelineItemTextContent(
@ -152,7 +152,7 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory() val sut = createTimelineItemContentMessageFactory()
val result = sut.create( val result = sut.create(
content = createMessageContent(type = TextMessageType("https://www.example.org", null)), content = createMessageContent(type = TextMessageType("https://www.example.org", null)),
senderDisplayName = "Bob", senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) as TimelineItemTextContent ) as TimelineItemTextContent
val expected = TimelineItemTextContent( val expected = TimelineItemTextContent(
@ -200,7 +200,7 @@ class TimelineItemContentMessageFactoryTest {
formatted = FormattedBody(MessageFormat.HTML, expected.toString()) formatted = FormattedBody(MessageFormat.HTML, expected.toString())
) )
), ),
senderDisplayName = "Bob", senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
assertThat((result as TimelineItemTextContent).formattedBody).isEqualTo(expected) assertThat((result as TimelineItemTextContent).formattedBody).isEqualTo(expected)
@ -218,7 +218,7 @@ class TimelineItemContentMessageFactoryTest {
formatted = FormattedBody(MessageFormat.UNKNOWN, "formatted") formatted = FormattedBody(MessageFormat.UNKNOWN, "formatted")
) )
), ),
senderDisplayName = "Bob", senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
assertThat((result as TimelineItemTextContent).formattedBody).isNull() assertThat((result as TimelineItemTextContent).formattedBody).isNull()
@ -229,7 +229,7 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory() val sut = createTimelineItemContentMessageFactory()
val result = sut.create( val result = sut.create(
content = createMessageContent(type = VideoMessageType("body", null, null, MediaSource("url"), null)), content = createMessageContent(type = VideoMessageType("body", null, null, MediaSource("url"), null)),
senderDisplayName = "Bob", senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemVideoContent( val expected = TimelineItemVideoContent(
@ -277,7 +277,7 @@ class TimelineItemContentMessageFactoryTest {
), ),
) )
), ),
senderDisplayName = "Bob", senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemVideoContent( val expected = TimelineItemVideoContent(
@ -303,7 +303,7 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory() val sut = createTimelineItemContentMessageFactory()
val result = sut.create( val result = sut.create(
content = createMessageContent(type = AudioMessageType("body", MediaSource("url"), null)), content = createMessageContent(type = AudioMessageType("body", MediaSource("url"), null)),
senderDisplayName = "Bob", senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemAudioContent( val expected = TimelineItemAudioContent(
@ -332,7 +332,7 @@ class TimelineItemContentMessageFactoryTest {
) )
) )
), ),
senderDisplayName = "Bob", senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemAudioContent( val expected = TimelineItemAudioContent(
@ -351,7 +351,7 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory() val sut = createTimelineItemContentMessageFactory()
val result = sut.create( val result = sut.create(
content = createMessageContent(type = VoiceMessageType("body", MediaSource("url"), null, null)), content = createMessageContent(type = VoiceMessageType("body", MediaSource("url"), null, null)),
senderDisplayName = "Bob", senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemVoiceContent( val expected = TimelineItemVoiceContent(
@ -384,7 +384,7 @@ class TimelineItemContentMessageFactoryTest {
), ),
) )
), ),
senderDisplayName = "Bob", senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemVoiceContent( val expected = TimelineItemVoiceContent(
@ -409,7 +409,7 @@ class TimelineItemContentMessageFactoryTest {
) )
val result = sut.create( val result = sut.create(
content = createMessageContent(type = VoiceMessageType("body", MediaSource("url"), null, null)), content = createMessageContent(type = VoiceMessageType("body", MediaSource("url"), null, null)),
senderDisplayName = "Bob", senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemAudioContent( val expected = TimelineItemAudioContent(
@ -428,7 +428,7 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory() val sut = createTimelineItemContentMessageFactory()
val result = sut.create( val result = sut.create(
content = createMessageContent(type = ImageMessageType("body", null, null, MediaSource("url"), null)), content = createMessageContent(type = ImageMessageType("body", null, null, MediaSource("url"), null)),
senderDisplayName = "Bob", senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemImageContent( val expected = TimelineItemImageContent(
@ -499,7 +499,7 @@ class TimelineItemContentMessageFactoryTest {
) )
) )
), ),
senderDisplayName = "Bob", senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemImageContent( val expected = TimelineItemImageContent(
@ -524,7 +524,7 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory() val sut = createTimelineItemContentMessageFactory()
val result = sut.create( val result = sut.create(
content = createMessageContent(type = FileMessageType("body", MediaSource("url"), null)), content = createMessageContent(type = FileMessageType("body", MediaSource("url"), null)),
senderDisplayName = "Bob", senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemFileContent( val expected = TimelineItemFileContent(
@ -559,7 +559,7 @@ class TimelineItemContentMessageFactoryTest {
) )
) )
), ),
senderDisplayName = "Bob", senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemFileContent( val expected = TimelineItemFileContent(
@ -578,7 +578,7 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory() val sut = createTimelineItemContentMessageFactory()
val result = sut.create( val result = sut.create(
content = createMessageContent(type = NoticeMessageType("body", null)), content = createMessageContent(type = NoticeMessageType("body", null)),
senderDisplayName = "Bob", senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemNoticeContent( val expected = TimelineItemNoticeContent(
@ -601,7 +601,7 @@ class TimelineItemContentMessageFactoryTest {
formatted = FormattedBody(MessageFormat.HTML, "formatted") formatted = FormattedBody(MessageFormat.HTML, "formatted")
) )
), ),
senderDisplayName = "Bob", senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
assertThat((result as TimelineItemNoticeContent).formattedBody).isEqualTo("formatted") assertThat((result as TimelineItemNoticeContent).formattedBody).isEqualTo("formatted")
@ -612,7 +612,7 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory() val sut = createTimelineItemContentMessageFactory()
val result = sut.create( val result = sut.create(
content = createMessageContent(type = EmoteMessageType("body", null)), content = createMessageContent(type = EmoteMessageType("body", null)),
senderDisplayName = "Bob", senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemEmoteContent( val expected = TimelineItemEmoteContent(
@ -635,7 +635,7 @@ class TimelineItemContentMessageFactoryTest {
formatted = FormattedBody(MessageFormat.HTML, "formatted") formatted = FormattedBody(MessageFormat.HTML, "formatted")
) )
), ),
senderDisplayName = "Bob", senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
assertThat((result as TimelineItemEmoteContent).formattedBody).isEqualTo(SpannableString("* Bob formatted")) assertThat((result as TimelineItemEmoteContent).formattedBody).isEqualTo(SpannableString("* Bob formatted"))

View file

@ -20,6 +20,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.fixtures.aMessageEvent import io.element.android.features.messages.impl.fixtures.aMessageEvent
import io.element.android.features.messages.impl.timeline.aTimelineItemDebugInfo import io.element.android.features.messages.impl.timeline.aTimelineItemDebugInfo
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
import io.element.android.features.messages.impl.timeline.components.aProfileTimelineDetailsReady
import io.element.android.features.messages.impl.timeline.model.ReadReceiptData import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
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.TimelineItemReadReceipts import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts
@ -39,7 +40,7 @@ class TimelineItemGrouperTest {
id = "0", id = "0",
senderId = A_USER_ID, senderId = A_USER_ID,
senderAvatar = anAvatarData(), senderAvatar = anAvatarData(),
senderDisplayName = "", senderProfile = aProfileTimelineDetailsReady(displayName = ""),
content = TimelineItemStateEventContent(body = "a state event"), content = TimelineItemStateEventContent(body = "a state event"),
reactionsState = aTimelineItemReactions(count = 0), reactionsState = aTimelineItemReactions(count = 0),
readReceiptState = TimelineItemReadReceipts(emptyList<ReadReceiptData>().toImmutableList()), readReceiptState = TimelineItemReadReceipts(emptyList<ReadReceiptData>().toImmutableList()),

View file

@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy
import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.test.timeline.aProfileTimelineDetails
import org.junit.Test import org.junit.Test
class InReplyToDetailTest { class InReplyToDetailTest {
@ -54,8 +55,7 @@ class InReplyToDetailTest {
val inReplyTo = InReplyTo.Ready( val inReplyTo = InReplyTo.Ready(
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
senderId = A_USER_ID, senderId = A_USER_ID,
senderDisplayName = "senderDisplayName", senderProfile = aProfileTimelineDetails(),
senderAvatarUrl = "senderAvatarUrl",
content = RoomMembershipContent( content = RoomMembershipContent(
userId = A_USER_ID, userId = A_USER_ID,
change = MembershipChange.INVITED, change = MembershipChange.INVITED,
@ -73,8 +73,7 @@ class InReplyToDetailTest {
val inReplyTo = InReplyTo.Ready( val inReplyTo = InReplyTo.Ready(
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
senderId = A_USER_ID, senderId = A_USER_ID,
senderDisplayName = "senderDisplayName", senderProfile = aProfileTimelineDetails(),
senderAvatarUrl = "senderAvatarUrl",
content = MessageContent( content = MessageContent(
body = "**Hello!**", body = "**Hello!**",
inReplyTo = null, inReplyTo = null,
@ -101,8 +100,7 @@ class InReplyToDetailTest {
val inReplyTo = InReplyTo.Ready( val inReplyTo = InReplyTo.Ready(
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
senderId = A_USER_ID, senderId = A_USER_ID,
senderDisplayName = "senderDisplayName", senderProfile = aProfileTimelineDetails(),
senderAvatarUrl = "senderAvatarUrl",
content = MessageContent( content = MessageContent(
body = "**Hello!**", body = "**Hello!**",
inReplyTo = null, inReplyTo = null,

View file

@ -42,6 +42,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageT
import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.OtherState import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
@ -55,6 +56,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.media.aMediaSource import io.element.android.libraries.matrix.test.media.aMediaSource
import io.element.android.libraries.matrix.test.timeline.aMessageContent import io.element.android.libraries.matrix.test.timeline.aMessageContent
import io.element.android.libraries.matrix.test.timeline.aPollContent import io.element.android.libraries.matrix.test.timeline.aPollContent
import io.element.android.libraries.matrix.test.timeline.aProfileTimelineDetails
import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
@ -430,15 +432,13 @@ class InReplyToMetadataKtTest {
fun anInReplyToDetails( fun anInReplyToDetails(
eventId: EventId = AN_EVENT_ID, eventId: EventId = AN_EVENT_ID,
senderId: UserId = A_USER_ID, senderId: UserId = A_USER_ID,
senderDisplayName: String? = "senderDisplayName", senderProfile: ProfileTimelineDetails = aProfileTimelineDetails(),
senderAvatarUrl: String? = "senderAvatarUrl",
eventContent: EventContent? = aMessageContent(), eventContent: EventContent? = aMessageContent(),
textContent: String? = "textContent", textContent: String? = "textContent",
) = InReplyToDetails( ) = InReplyToDetails(
eventId = eventId, eventId = eventId,
senderId = senderId, senderId = senderId,
senderDisplayName = senderDisplayName, senderProfile = senderProfile,
senderAvatarUrl = senderAvatarUrl,
eventContent = eventContent, eventContent = eventContent,
textContent = textContent, textContent = textContent,
) )

View file

@ -41,7 +41,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessage
import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
@ -52,6 +51,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecry
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
import io.element.android.libraries.matrix.ui.messages.toPlainText import io.element.android.libraries.matrix.ui.messages.toPlainText
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.toolbox.api.strings.StringProvider import io.element.android.services.toolbox.api.strings.StringProvider
@ -72,15 +72,13 @@ class DefaultRoomLastMessageFormatter @Inject constructor(
override fun format(event: EventTimelineItem, isDmRoom: Boolean): CharSequence? { override fun format(event: EventTimelineItem, isDmRoom: Boolean): CharSequence? {
val isOutgoing = event.isOwn val isOutgoing = event.isOwn
// Note: we do not use disambiguated display name here, see val senderDisambiguatedDisplayName = event.senderProfile.getDisambiguatedDisplayName(event.sender)
// https://github.com/element-hq/element-x-ios/issues/1845#issuecomment-1888707428
val senderDisplayName = (event.senderProfile as? ProfileTimelineDetails.Ready)?.displayName ?: event.sender.value
return when (val content = event.content) { return when (val content = event.content) {
is MessageContent -> processMessageContents(content, senderDisplayName, isDmRoom) is MessageContent -> processMessageContents(content, senderDisambiguatedDisplayName, isDmRoom)
RedactedContent -> { RedactedContent -> {
val message = sp.getString(CommonStrings.common_message_removed) val message = sp.getString(CommonStrings.common_message_removed)
if (!isDmRoom) { if (!isDmRoom) {
prefix(message, senderDisplayName) prefix(message, senderDisambiguatedDisplayName)
} else { } else {
message message
} }
@ -91,36 +89,40 @@ class DefaultRoomLastMessageFormatter @Inject constructor(
is UnableToDecryptContent -> { is UnableToDecryptContent -> {
val message = sp.getString(CommonStrings.common_waiting_for_decryption_key) val message = sp.getString(CommonStrings.common_waiting_for_decryption_key)
if (!isDmRoom) { if (!isDmRoom) {
prefix(message, senderDisplayName) prefix(message, senderDisambiguatedDisplayName)
} else { } else {
message message
} }
} }
is RoomMembershipContent -> { is RoomMembershipContent -> {
roomMembershipContentFormatter.format(content, senderDisplayName, isOutgoing) roomMembershipContentFormatter.format(content, senderDisambiguatedDisplayName, isOutgoing)
} }
is ProfileChangeContent -> { is ProfileChangeContent -> {
profileChangeContentFormatter.format(content, event.sender, senderDisplayName, isOutgoing) profileChangeContentFormatter.format(content, event.sender, senderDisambiguatedDisplayName, isOutgoing)
} }
is StateContent -> { is StateContent -> {
stateContentFormatter.format(content, senderDisplayName, isOutgoing, RenderingMode.RoomList) stateContentFormatter.format(content, senderDisambiguatedDisplayName, isOutgoing, RenderingMode.RoomList)
} }
is PollContent -> { is PollContent -> {
val message = sp.getString(CommonStrings.common_poll_summary, content.question) val message = sp.getString(CommonStrings.common_poll_summary, content.question)
prefixIfNeeded(message, senderDisplayName, isDmRoom) prefixIfNeeded(message, senderDisambiguatedDisplayName, isDmRoom)
} }
is FailedToParseMessageLikeContent, is FailedToParseStateContent, is UnknownContent -> { is FailedToParseMessageLikeContent, is FailedToParseStateContent, is UnknownContent -> {
prefixIfNeeded(sp.getString(CommonStrings.common_unsupported_event), senderDisplayName, isDmRoom) prefixIfNeeded(sp.getString(CommonStrings.common_unsupported_event), senderDisambiguatedDisplayName, isDmRoom)
} }
is LegacyCallInviteContent -> sp.getString(CommonStrings.common_call_invite) is LegacyCallInviteContent -> sp.getString(CommonStrings.common_call_invite)
}?.take(MAX_SAFE_LENGTH) }?.take(MAX_SAFE_LENGTH)
} }
private fun processMessageContents(messageContent: MessageContent, senderDisplayName: String, isDmRoom: Boolean): CharSequence? { private fun processMessageContents(
messageContent: MessageContent,
senderDisambiguatedDisplayName: String,
isDmRoom: Boolean,
): CharSequence {
val internalMessage = when (val messageType: MessageType = messageContent.type) { val internalMessage = when (val messageType: MessageType = messageContent.type) {
// Doesn't need a prefix // Doesn't need a prefix
is EmoteMessageType -> { is EmoteMessageType -> {
return "* $senderDisplayName ${messageType.body}" return "* $senderDisambiguatedDisplayName ${messageType.body}"
} }
is TextMessageType -> { is TextMessageType -> {
messageType.toPlainText(permalinkParser) messageType.toPlainText(permalinkParser)
@ -153,19 +155,23 @@ class DefaultRoomLastMessageFormatter @Inject constructor(
messageType.body messageType.body
} }
} }
return prefixIfNeeded(internalMessage, senderDisplayName, isDmRoom) return prefixIfNeeded(internalMessage, senderDisambiguatedDisplayName, isDmRoom)
} }
private fun prefixIfNeeded(message: String, senderDisplayName: String, isDmRoom: Boolean): CharSequence = if (isDmRoom) { private fun prefixIfNeeded(
message: String,
senderDisambiguatedDisplayName: String,
isDmRoom: Boolean,
): CharSequence = if (isDmRoom) {
message message
} else { } else {
prefix(message, senderDisplayName) prefix(message, senderDisambiguatedDisplayName)
} }
private fun prefix(message: String, senderDisplayName: String): AnnotatedString { private fun prefix(message: String, senderDisambiguatedDisplayName: String): AnnotatedString {
return buildAnnotatedString { return buildAnnotatedString {
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append(senderDisplayName) append(senderDisambiguatedDisplayName)
} }
append(": ") append(": ")
append(message) append(message)

View file

@ -49,16 +49,16 @@ class DefaultTimelineEventFormatter @Inject constructor(
) : TimelineEventFormatter { ) : TimelineEventFormatter {
override fun format(event: EventTimelineItem): CharSequence? { override fun format(event: EventTimelineItem): CharSequence? {
val isOutgoing = event.isOwn val isOutgoing = event.isOwn
val senderDisplayName = event.senderProfile.getDisambiguatedDisplayName(event.sender) val senderDisambiguatedDisplayName = event.senderProfile.getDisambiguatedDisplayName(event.sender)
return when (val content = event.content) { return when (val content = event.content) {
is RoomMembershipContent -> { is RoomMembershipContent -> {
roomMembershipContentFormatter.format(content, senderDisplayName, isOutgoing) roomMembershipContentFormatter.format(content, senderDisambiguatedDisplayName, isOutgoing)
} }
is ProfileChangeContent -> { is ProfileChangeContent -> {
profileChangeContentFormatter.format(content, event.sender, senderDisplayName, isOutgoing) profileChangeContentFormatter.format(content, event.sender, senderDisambiguatedDisplayName, isOutgoing)
} }
is StateContent -> { is StateContent -> {
stateContentFormatter.format(content, senderDisplayName, isOutgoing, RenderingMode.Timeline) stateContentFormatter.format(content, senderDisambiguatedDisplayName, isOutgoing, RenderingMode.Timeline)
} }
is LegacyCallInviteContent -> { is LegacyCallInviteContent -> {
sp.getString(CommonStrings.common_call_invite) sp.getString(CommonStrings.common_call_invite)

View file

@ -27,14 +27,19 @@ class ProfileChangeContentFormatter @Inject constructor(
fun format( fun format(
profileChangeContent: ProfileChangeContent, profileChangeContent: ProfileChangeContent,
senderId: UserId, senderId: UserId,
senderDisplayName: String, senderDisambiguatedDisplayName: String,
senderIsYou: Boolean, senderIsYou: Boolean,
): String? = profileChangeContent.run { ): String? = profileChangeContent.run {
val displayNameChanged = displayName != prevDisplayName val displayNameChanged = displayName != prevDisplayName
val avatarChanged = avatarUrl != prevAvatarUrl val avatarChanged = avatarUrl != prevAvatarUrl
return when { return when {
avatarChanged && displayNameChanged -> { avatarChanged && displayNameChanged -> {
val message = format(profileChangeContent.copy(avatarUrl = null, prevAvatarUrl = null), senderId, senderDisplayName, senderIsYou) val message = format(
profileChangeContent = profileChangeContent.copy(avatarUrl = null, prevAvatarUrl = null),
senderId = senderId,
senderDisambiguatedDisplayName = senderDisambiguatedDisplayName,
senderIsYou = senderIsYou,
)
val avatarChangedToo = sp.getString(R.string.state_event_avatar_changed_too) val avatarChangedToo = sp.getString(R.string.state_event_avatar_changed_too)
"$message\n$avatarChangedToo" "$message\n$avatarChangedToo"
} }
@ -63,7 +68,7 @@ class ProfileChangeContentFormatter @Inject constructor(
if (senderIsYou) { if (senderIsYou) {
sp.getString(R.string.state_event_avatar_url_changed_by_you) sp.getString(R.string.state_event_avatar_url_changed_by_you)
} else { } else {
sp.getString(R.string.state_event_avatar_url_changed, senderDisplayName) sp.getString(R.string.state_event_avatar_url_changed, senderDisambiguatedDisplayName)
} }
} }
else -> null else -> null

View file

@ -29,7 +29,7 @@ class RoomMembershipContentFormatter @Inject constructor(
) { ) {
fun format( fun format(
membershipContent: RoomMembershipContent, membershipContent: RoomMembershipContent,
senderDisplayName: String, senderDisambiguatedDisplayName: String,
senderIsYou: Boolean, senderIsYou: Boolean,
): CharSequence? { ): CharSequence? {
val userId = membershipContent.userId val userId = membershipContent.userId
@ -38,34 +38,34 @@ class RoomMembershipContentFormatter @Inject constructor(
MembershipChange.JOINED -> if (memberIsYou) { MembershipChange.JOINED -> if (memberIsYou) {
sp.getString(R.string.state_event_room_join_by_you) sp.getString(R.string.state_event_room_join_by_you)
} else { } else {
sp.getString(R.string.state_event_room_join, userId.value) sp.getString(R.string.state_event_room_join, senderDisambiguatedDisplayName)
} }
MembershipChange.LEFT -> if (memberIsYou) { MembershipChange.LEFT -> if (memberIsYou) {
sp.getString(R.string.state_event_room_leave_by_you) sp.getString(R.string.state_event_room_leave_by_you)
} else { } else {
sp.getString(R.string.state_event_room_leave, userId.value) sp.getString(R.string.state_event_room_leave, senderDisambiguatedDisplayName)
} }
MembershipChange.BANNED, MembershipChange.KICKED_AND_BANNED -> if (senderIsYou) { MembershipChange.BANNED, MembershipChange.KICKED_AND_BANNED -> if (senderIsYou) {
sp.getString(R.string.state_event_room_ban_by_you, userId.value) sp.getString(R.string.state_event_room_ban_by_you, userId.value)
} else { } else {
sp.getString(R.string.state_event_room_ban, senderDisplayName, userId.value) sp.getString(R.string.state_event_room_ban, senderDisambiguatedDisplayName, userId.value)
} }
MembershipChange.UNBANNED -> if (senderIsYou) { MembershipChange.UNBANNED -> if (senderIsYou) {
sp.getString(R.string.state_event_room_unban_by_you, userId.value) sp.getString(R.string.state_event_room_unban_by_you, userId.value)
} else { } else {
sp.getString(R.string.state_event_room_unban, senderDisplayName, userId.value) sp.getString(R.string.state_event_room_unban, senderDisambiguatedDisplayName, userId.value)
} }
MembershipChange.KICKED -> if (senderIsYou) { MembershipChange.KICKED -> if (senderIsYou) {
sp.getString(R.string.state_event_room_remove_by_you, userId.value) sp.getString(R.string.state_event_room_remove_by_you, userId.value)
} else { } else {
sp.getString(R.string.state_event_room_remove, senderDisplayName, userId.value) sp.getString(R.string.state_event_room_remove, senderDisambiguatedDisplayName, userId.value)
} }
MembershipChange.INVITED -> if (senderIsYou) { MembershipChange.INVITED -> if (senderIsYou) {
sp.getString(R.string.state_event_room_invite_by_you, userId.value) sp.getString(R.string.state_event_room_invite_by_you, userId.value)
} else if (memberIsYou) { } else if (memberIsYou) {
sp.getString(R.string.state_event_room_invite_you, senderDisplayName) sp.getString(R.string.state_event_room_invite_you, senderDisambiguatedDisplayName)
} else { } else {
sp.getString(R.string.state_event_room_invite, senderDisplayName, userId.value) sp.getString(R.string.state_event_room_invite, senderDisambiguatedDisplayName, userId.value)
} }
MembershipChange.INVITATION_ACCEPTED -> if (memberIsYou) { MembershipChange.INVITATION_ACCEPTED -> if (memberIsYou) {
sp.getString(R.string.state_event_room_invite_accepted_by_you) sp.getString(R.string.state_event_room_invite_accepted_by_you)
@ -80,34 +80,34 @@ class RoomMembershipContentFormatter @Inject constructor(
MembershipChange.INVITATION_REVOKED -> if (senderIsYou) { MembershipChange.INVITATION_REVOKED -> if (senderIsYou) {
sp.getString(R.string.state_event_room_third_party_revoked_invite_by_you, userId.value) sp.getString(R.string.state_event_room_third_party_revoked_invite_by_you, userId.value)
} else { } else {
sp.getString(R.string.state_event_room_third_party_revoked_invite, senderDisplayName, userId.value) sp.getString(R.string.state_event_room_third_party_revoked_invite, senderDisambiguatedDisplayName, userId.value)
} }
MembershipChange.KNOCKED -> if (memberIsYou) { MembershipChange.KNOCKED -> if (memberIsYou) {
sp.getString(R.string.state_event_room_knock_by_you) sp.getString(R.string.state_event_room_knock_by_you)
} else { } else {
sp.getString(R.string.state_event_room_knock, userId.value) sp.getString(R.string.state_event_room_knock, senderDisambiguatedDisplayName)
} }
MembershipChange.KNOCK_ACCEPTED -> if (senderIsYou) { MembershipChange.KNOCK_ACCEPTED -> if (senderIsYou) {
sp.getString(R.string.state_event_room_knock_accepted_by_you, userId.value) sp.getString(R.string.state_event_room_knock_accepted_by_you, userId.value)
} else { } else {
sp.getString(R.string.state_event_room_knock_accepted, senderDisplayName, userId.value) sp.getString(R.string.state_event_room_knock_accepted, senderDisambiguatedDisplayName, userId.value)
} }
MembershipChange.KNOCK_RETRACTED -> if (memberIsYou) { MembershipChange.KNOCK_RETRACTED -> if (memberIsYou) {
sp.getString(R.string.state_event_room_knock_retracted_by_you) sp.getString(R.string.state_event_room_knock_retracted_by_you)
} else { } else {
sp.getString(R.string.state_event_room_knock_retracted, userId.value) sp.getString(R.string.state_event_room_knock_retracted, senderDisambiguatedDisplayName)
} }
MembershipChange.KNOCK_DENIED -> if (senderIsYou) { MembershipChange.KNOCK_DENIED -> if (senderIsYou) {
sp.getString(R.string.state_event_room_knock_denied_by_you, userId.value) sp.getString(R.string.state_event_room_knock_denied_by_you, userId.value)
} else if (memberIsYou) { } else if (memberIsYou) {
sp.getString(R.string.state_event_room_knock_denied_you, senderDisplayName) sp.getString(R.string.state_event_room_knock_denied_you, senderDisambiguatedDisplayName)
} else { } else {
sp.getString(R.string.state_event_room_knock_denied, senderDisplayName, userId.value) sp.getString(R.string.state_event_room_knock_denied, senderDisambiguatedDisplayName, userId.value)
} }
MembershipChange.NONE -> if (senderIsYou) { MembershipChange.NONE -> if (senderIsYou) {
sp.getString(R.string.state_event_room_none_by_you) sp.getString(R.string.state_event_room_none_by_you)
} else { } else {
sp.getString(R.string.state_event_room_none, senderDisplayName) sp.getString(R.string.state_event_room_none, senderDisambiguatedDisplayName)
} }
MembershipChange.ERROR -> { MembershipChange.ERROR -> {
Timber.v("Filtering timeline item for room membership: $membershipContent") Timber.v("Filtering timeline item for room membership: $membershipContent")

View file

@ -29,7 +29,7 @@ class StateContentFormatter @Inject constructor(
) { ) {
fun format( fun format(
stateContent: StateContent, stateContent: StateContent,
senderDisplayName: String, senderDisambiguatedDisplayName: String,
senderIsYou: Boolean, senderIsYou: Boolean,
renderingMode: RenderingMode, renderingMode: RenderingMode,
): CharSequence? { ): CharSequence? {
@ -39,15 +39,15 @@ class StateContentFormatter @Inject constructor(
when { when {
senderIsYou && hasAvatarUrl -> sp.getString(R.string.state_event_room_avatar_changed_by_you) senderIsYou && hasAvatarUrl -> sp.getString(R.string.state_event_room_avatar_changed_by_you)
senderIsYou && !hasAvatarUrl -> sp.getString(R.string.state_event_room_avatar_removed_by_you) senderIsYou && !hasAvatarUrl -> sp.getString(R.string.state_event_room_avatar_removed_by_you)
!senderIsYou && hasAvatarUrl -> sp.getString(R.string.state_event_room_avatar_changed, senderDisplayName) !senderIsYou && hasAvatarUrl -> sp.getString(R.string.state_event_room_avatar_changed, senderDisambiguatedDisplayName)
else -> sp.getString(R.string.state_event_room_avatar_removed, senderDisplayName) else -> sp.getString(R.string.state_event_room_avatar_removed, senderDisambiguatedDisplayName)
} }
} }
is OtherState.RoomCreate -> { is OtherState.RoomCreate -> {
if (senderIsYou) { if (senderIsYou) {
sp.getString(R.string.state_event_room_created_by_you) sp.getString(R.string.state_event_room_created_by_you)
} else { } else {
sp.getString(R.string.state_event_room_created, senderDisplayName) sp.getString(R.string.state_event_room_created, senderDisambiguatedDisplayName)
} }
} }
is OtherState.RoomEncryption -> sp.getString(CommonStrings.common_encryption_enabled) is OtherState.RoomEncryption -> sp.getString(CommonStrings.common_encryption_enabled)
@ -56,8 +56,8 @@ class StateContentFormatter @Inject constructor(
when { when {
senderIsYou && hasRoomName -> sp.getString(R.string.state_event_room_name_changed_by_you, content.name) senderIsYou && hasRoomName -> sp.getString(R.string.state_event_room_name_changed_by_you, content.name)
senderIsYou && !hasRoomName -> sp.getString(R.string.state_event_room_name_removed_by_you) senderIsYou && !hasRoomName -> sp.getString(R.string.state_event_room_name_removed_by_you)
!senderIsYou && hasRoomName -> sp.getString(R.string.state_event_room_name_changed, senderDisplayName, content.name) !senderIsYou && hasRoomName -> sp.getString(R.string.state_event_room_name_changed, senderDisambiguatedDisplayName, content.name)
else -> sp.getString(R.string.state_event_room_name_removed, senderDisplayName) else -> sp.getString(R.string.state_event_room_name_removed, senderDisambiguatedDisplayName)
} }
} }
is OtherState.RoomThirdPartyInvite -> { is OtherState.RoomThirdPartyInvite -> {
@ -68,7 +68,7 @@ class StateContentFormatter @Inject constructor(
if (senderIsYou) { if (senderIsYou) {
sp.getString(R.string.state_event_room_third_party_invite_by_you, content.displayName) sp.getString(R.string.state_event_room_third_party_invite_by_you, content.displayName)
} else { } else {
sp.getString(R.string.state_event_room_third_party_invite, senderDisplayName, content.displayName) sp.getString(R.string.state_event_room_third_party_invite, senderDisambiguatedDisplayName, content.displayName)
} }
} }
is OtherState.RoomTopic -> { is OtherState.RoomTopic -> {
@ -76,8 +76,8 @@ class StateContentFormatter @Inject constructor(
when { when {
senderIsYou && hasRoomTopic -> sp.getString(R.string.state_event_room_topic_changed_by_you, content.topic) senderIsYou && hasRoomTopic -> sp.getString(R.string.state_event_room_topic_changed_by_you, content.topic)
senderIsYou && !hasRoomTopic -> sp.getString(R.string.state_event_room_topic_removed_by_you) senderIsYou && !hasRoomTopic -> sp.getString(R.string.state_event_room_topic_removed_by_you)
!senderIsYou && hasRoomTopic -> sp.getString(R.string.state_event_room_topic_changed, senderDisplayName, content.topic) !senderIsYou && hasRoomTopic -> sp.getString(R.string.state_event_room_topic_changed, senderDisambiguatedDisplayName, content.topic)
else -> sp.getString(R.string.state_event_room_topic_removed, senderDisplayName) else -> sp.getString(R.string.state_event_room_topic_removed, senderDisambiguatedDisplayName)
} }
} }
is OtherState.Custom -> when (renderingMode) { is OtherState.Custom -> when (renderingMode) {

View file

@ -30,7 +30,7 @@
<string name="state_event_room_join_by_you">"You joined the room"</string> <string name="state_event_room_join_by_you">"You joined the room"</string>
<string name="state_event_room_knock">"%1$s requested to join"</string> <string name="state_event_room_knock">"%1$s requested to join"</string>
<string name="state_event_room_knock_accepted">"%1$s allowed %2$s to join"</string> <string name="state_event_room_knock_accepted">"%1$s allowed %2$s to join"</string>
<string name="state_event_room_knock_accepted_by_you">"%1$s allowed you to join"</string> <string name="state_event_room_knock_accepted_by_you">"You allowed %1$s to join"</string>
<string name="state_event_room_knock_by_you">"You requested to join"</string> <string name="state_event_room_knock_by_you">"You requested to join"</string>
<string name="state_event_room_knock_denied">"%1$s rejected %2$s\'s request to join"</string> <string name="state_event_room_knock_denied">"%1$s rejected %2$s\'s request to join"</string>
<string name="state_event_room_knock_denied_by_you">"You rejected %1$s\'s request to join"</string> <string name="state_event_room_knock_denied_by_you">"You rejected %1$s\'s request to join"</string>

View file

@ -38,7 +38,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.OtherState import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
@ -54,6 +53,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.test.timeline.aPollContent import io.element.android.libraries.matrix.test.timeline.aPollContent
import io.element.android.libraries.matrix.test.timeline.aProfileChangeMessageContent import io.element.android.libraries.matrix.test.timeline.aProfileChangeMessageContent
import io.element.android.libraries.matrix.test.timeline.aProfileTimelineDetails
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
import org.junit.Before import org.junit.Before
@ -264,7 +264,7 @@ class DefaultRoomLastMessageFormatterTest {
val someoneJoinedRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) val someoneJoinedRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneJoinedRoom = formatter.format(someoneJoinedRoomEvent, false) val someoneJoinedRoom = formatter.format(someoneJoinedRoomEvent, false)
assertThat(someoneJoinedRoom).isEqualTo("${someoneContent.userId} joined the room") assertThat(someoneJoinedRoom).isEqualTo("$otherName joined the room")
} }
@Test @Test
@ -280,7 +280,7 @@ class DefaultRoomLastMessageFormatterTest {
val someoneLeftRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) val someoneLeftRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneLeftRoom = formatter.format(someoneLeftRoomEvent, false) val someoneLeftRoom = formatter.format(someoneLeftRoomEvent, false)
assertThat(someoneLeftRoom).isEqualTo("${someoneContent.userId} left the room") assertThat(someoneLeftRoom).isEqualTo("$otherName left the room")
} }
@Test @Test
@ -421,7 +421,7 @@ class DefaultRoomLastMessageFormatterTest {
val someoneKnockedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) val someoneKnockedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneKnocked = formatter.format(someoneKnockedEvent, false) val someoneKnocked = formatter.format(someoneKnockedEvent, false)
assertThat(someoneKnocked).isEqualTo("${someoneContent.userId} requested to join") assertThat(someoneKnocked).isEqualTo("$otherName requested to join")
} }
@Test @Test
@ -432,7 +432,7 @@ class DefaultRoomLastMessageFormatterTest {
val youAcceptedKnockEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent) val youAcceptedKnockEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent)
val youAcceptedKnock = formatter.format(youAcceptedKnockEvent, false) val youAcceptedKnock = formatter.format(youAcceptedKnockEvent, false)
assertThat(youAcceptedKnock).isEqualTo("${someoneContent.userId} allowed you to join") assertThat(youAcceptedKnock).isEqualTo("You allowed ${someoneContent.userId} to join")
val someoneAcceptedKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) val someoneAcceptedKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneAcceptedKnock = formatter.format(someoneAcceptedKnockEvent, false) val someoneAcceptedKnock = formatter.format(someoneAcceptedKnockEvent, false)
@ -452,7 +452,7 @@ class DefaultRoomLastMessageFormatterTest {
val someoneRetractedKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) val someoneRetractedKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneRetractedKnock = formatter.format(someoneRetractedKnockEvent, false) val someoneRetractedKnock = formatter.format(someoneRetractedKnockEvent, false)
assertThat(someoneRetractedKnock).isEqualTo("${someoneContent.userId} is no longer interested in joining") assertThat(someoneRetractedKnock).isEqualTo("$otherName is no longer interested in joining")
} }
@Test @Test
@ -829,9 +829,13 @@ class DefaultRoomLastMessageFormatterTest {
// endregion // endregion
private fun createRoomEvent(sentByYou: Boolean, senderDisplayName: String?, content: EventContent): EventTimelineItem { private fun createRoomEvent(
sentByYou: Boolean,
senderDisplayName: String?,
content: EventContent,
): EventTimelineItem {
val sender = if (sentByYou) A_USER_ID else someoneElseId val sender = if (sentByYou) A_USER_ID else someoneElseId
val profile = ProfileTimelineDetails.Ready(senderDisplayName, false, null) val profile = aProfileTimelineDetails(senderDisplayName)
return anEventTimelineItem( return anEventTimelineItem(
content = content, content = content,
senderProfile = profile, senderProfile = profile,

View file

@ -27,7 +27,7 @@ data class NotificationData(
val roomId: RoomId, val roomId: RoomId,
// mxc url // mxc url
val senderAvatarUrl: String?, val senderAvatarUrl: String?,
// private, must use `getSenderName` // private, must use `getDisambiguatedDisplayName`
private val senderDisplayName: String?, private val senderDisplayName: String?,
private val senderIsNameAmbiguous: Boolean, private val senderIsNameAmbiguous: Boolean,
val roomAvatarUrl: String?, val roomAvatarUrl: String?,
@ -39,7 +39,7 @@ data class NotificationData(
val content: NotificationContent, val content: NotificationContent,
val hasMention: Boolean, val hasMention: Boolean,
) { ) {
fun getSenderName(userId: UserId): String = when { fun getDisambiguatedDisplayName(userId: UserId): String = when {
senderDisplayName.isNullOrBlank() -> userId.value senderDisplayName.isNullOrBlank() -> userId.value
senderIsNameAmbiguous -> "$senderDisplayName ($userId)" senderIsNameAmbiguous -> "$senderDisplayName ($userId)"
else -> senderDisplayName else -> senderDisplayName
@ -52,6 +52,7 @@ sealed interface NotificationContent {
data class CallInvite( data class CallInvite(
val senderId: UserId, val senderId: UserId,
) : MessageLike ) : MessageLike
data object CallHangup : MessageLike data object CallHangup : MessageLike
data object CallCandidates : MessageLike data object CallCandidates : MessageLike
data object KeyVerificationReady : MessageLike data object KeyVerificationReady : MessageLike

View file

@ -33,8 +33,7 @@ sealed interface InReplyTo {
val eventId: EventId, val eventId: EventId,
val content: EventContent, val content: EventContent,
val senderId: UserId, val senderId: UserId,
val senderDisplayName: String?, val senderProfile: ProfileTimelineDetails,
val senderAvatarUrl: String?,
) : InReplyTo ) : InReplyTo
/** /**

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
}
}

View file

@ -29,7 +29,7 @@ class NotificationDataTest {
senderDisplayName = null, senderDisplayName = null,
senderIsNameAmbiguous = false, senderIsNameAmbiguous = false,
) )
assertThat(sut.getSenderName(A_USER_ID)).isEqualTo("@alice:server.org") assertThat(sut.getDisambiguatedDisplayName(A_USER_ID)).isEqualTo("@alice:server.org")
} }
@Test @Test
@ -38,7 +38,7 @@ class NotificationDataTest {
senderDisplayName = "Alice", senderDisplayName = "Alice",
senderIsNameAmbiguous = false, senderIsNameAmbiguous = false,
) )
assertThat(sut.getSenderName(A_USER_ID)).isEqualTo("Alice") assertThat(sut.getDisambiguatedDisplayName(A_USER_ID)).isEqualTo("Alice")
} }
@Test @Test
@ -47,7 +47,7 @@ class NotificationDataTest {
senderDisplayName = "Alice", senderDisplayName = "Alice",
senderIsNameAmbiguous = true, senderIsNameAmbiguous = true,
) )
assertThat(sut.getSenderName(A_USER_ID)).isEqualTo("Alice (@alice:server.org)") assertThat(sut.getDisambiguatedDisplayName(A_USER_ID)).isEqualTo("Alice (@alice:server.org)")
} }
private fun aNotificationData( private fun aNotificationData(

View file

@ -35,7 +35,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageT
import io.element.android.libraries.matrix.impl.media.map import io.element.android.libraries.matrix.impl.media.map
import org.matrix.rustcomponents.sdk.Message import org.matrix.rustcomponents.sdk.Message
import org.matrix.rustcomponents.sdk.MessageType import org.matrix.rustcomponents.sdk.MessageType
import org.matrix.rustcomponents.sdk.ProfileDetails
import org.matrix.rustcomponents.sdk.RepliedToEventDetails import org.matrix.rustcomponents.sdk.RepliedToEventDetails
import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.use
import org.matrix.rustcomponents.sdk.FormattedBody as RustFormattedBody import org.matrix.rustcomponents.sdk.FormattedBody as RustFormattedBody
@ -51,13 +50,11 @@ class EventMessageMapper {
val inReplyToId = EventId(details.eventId) val inReplyToId = EventId(details.eventId)
when (val event = details.event) { when (val event = details.event) {
is RepliedToEventDetails.Ready -> { is RepliedToEventDetails.Ready -> {
val senderProfile = event.senderProfile as? ProfileDetails.Ready
InReplyTo.Ready( InReplyTo.Ready(
eventId = inReplyToId, eventId = inReplyToId,
content = timelineEventContentMapper.map(event.content), content = timelineEventContentMapper.map(event.content),
senderId = UserId(event.sender), senderId = UserId(event.sender),
senderDisplayName = senderProfile?.displayName, senderProfile = event.senderProfile.map(),
senderAvatarUrl = senderProfile?.avatarUrl,
) )
} }
is RepliedToEventDetails.Error -> InReplyTo.Error is RepliedToEventDetails.Error -> InReplyTo.Error

View file

@ -93,8 +93,8 @@ class NotifiableEventResolver @Inject constructor(
): NotifiableEvent? { ): NotifiableEvent? {
return when (val content = this.content) { return when (val content = this.content) {
is NotificationContent.MessageLike.RoomMessage -> { is NotificationContent.MessageLike.RoomMessage -> {
val senderName = getSenderName(content.senderId) val senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId)
val messageBody = descriptionFromMessageContent(content, senderName) val messageBody = descriptionFromMessageContent(content, senderDisambiguatedDisplayName)
val notificationBody = if (hasMention) { val notificationBody = if (hasMention) {
stringProvider.getString(R.string.notification_mentioned_you_body, messageBody) stringProvider.getString(R.string.notification_mentioned_you_body, messageBody)
} else { } else {
@ -107,7 +107,7 @@ class NotifiableEventResolver @Inject constructor(
eventId = eventId, eventId = eventId,
noisy = isNoisy, noisy = isNoisy,
timestamp = this.timestamp, timestamp = this.timestamp,
senderName = senderName, senderDisambiguatedDisplayName = senderDisambiguatedDisplayName,
body = notificationBody, body = notificationBody,
imageUriString = fetchImageIfPresent(client)?.toString(), imageUriString = fetchImageIfPresent(client)?.toString(),
roomName = roomDisplayName, roomName = roomDisplayName,
@ -154,7 +154,7 @@ class NotifiableEventResolver @Inject constructor(
eventId = eventId, eventId = eventId,
noisy = isNoisy, noisy = isNoisy,
timestamp = this.timestamp, timestamp = this.timestamp,
senderName = null, senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId),
body = stringProvider.getString(CommonStrings.common_call_invite), body = stringProvider.getString(CommonStrings.common_call_invite),
imageUriString = fetchImageIfPresent(client)?.toString(), imageUriString = fetchImageIfPresent(client)?.toString(),
roomName = roomDisplayName, roomName = roomDisplayName,
@ -180,7 +180,7 @@ class NotifiableEventResolver @Inject constructor(
eventId = eventId, eventId = eventId,
noisy = isNoisy, noisy = isNoisy,
timestamp = this.timestamp, timestamp = this.timestamp,
senderName = getSenderName(content.senderId), senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId),
body = stringProvider.getString(CommonStrings.common_poll_summary, content.question), body = stringProvider.getString(CommonStrings.common_poll_summary, content.question),
imageUriString = null, imageUriString = null,
roomName = roomDisplayName, roomName = roomDisplayName,
@ -244,12 +244,12 @@ class NotifiableEventResolver @Inject constructor(
private fun descriptionFromMessageContent( private fun descriptionFromMessageContent(
content: NotificationContent.MessageLike.RoomMessage, content: NotificationContent.MessageLike.RoomMessage,
senderDisplayName: String, senderDisambiguatedDisplayName: String,
): String { ): String {
return when (val messageType = content.messageType) { return when (val messageType = content.messageType) {
is AudioMessageType -> messageType.body is AudioMessageType -> messageType.body
is VoiceMessageType -> stringProvider.getString(CommonStrings.common_voice_message) is VoiceMessageType -> stringProvider.getString(CommonStrings.common_voice_message)
is EmoteMessageType -> "* $senderDisplayName ${messageType.body}" is EmoteMessageType -> "* $senderDisambiguatedDisplayName ${messageType.body}"
is FileMessageType -> messageType.body is FileMessageType -> messageType.body
is ImageMessageType -> messageType.body is ImageMessageType -> messageType.body
is StickerMessageType -> messageType.body is StickerMessageType -> messageType.body
@ -310,7 +310,7 @@ private fun buildNotifiableMessageEvent(
canBeReplaced: Boolean = false, canBeReplaced: Boolean = false,
noisy: Boolean, noisy: Boolean,
timestamp: Long, timestamp: Long,
senderName: String?, senderDisambiguatedDisplayName: String?,
body: String?, body: String?,
// We cannot use Uri? type here, as that could trigger a // We cannot use Uri? type here, as that could trigger a
// NotSerializableException when persisting this to storage // NotSerializableException when persisting this to storage
@ -335,7 +335,7 @@ private fun buildNotifiableMessageEvent(
canBeReplaced = canBeReplaced, canBeReplaced = canBeReplaced,
noisy = noisy, noisy = noisy,
timestamp = timestamp, timestamp = timestamp,
senderName = senderName, senderDisambiguatedDisplayName = senderDisambiguatedDisplayName,
body = body, body = body,
imageUriString = imageUriString, imageUriString = imageUriString,
threadId = threadId, threadId = threadId,

View file

@ -45,7 +45,7 @@ class RoomGroupMessageCreator @Inject constructor(
imageLoader: ImageLoader, imageLoader: ImageLoader,
): RoomNotification.Message { ): RoomNotification.Message {
val lastKnownRoomEvent = events.last() val lastKnownRoomEvent = events.last()
val roomName = lastKnownRoomEvent.roomName ?: lastKnownRoomEvent.senderName ?: "Room name (${roomId.value.take(8)}…)" val roomName = lastKnownRoomEvent.roomName ?: lastKnownRoomEvent.senderDisambiguatedDisplayName ?: "Room name (${roomId.value.take(8)}…)"
val roomIsGroup = !lastKnownRoomEvent.roomIsDirect val roomIsGroup = !lastKnownRoomEvent.roomIsDirect
val style = NotificationCompat.MessagingStyle( val style = NotificationCompat.MessagingStyle(
Person.Builder() Person.Builder()
@ -60,9 +60,9 @@ class RoomGroupMessageCreator @Inject constructor(
} }
val tickerText = if (roomIsGroup) { val tickerText = if (roomIsGroup) {
stringProvider.getString(R.string.notification_ticker_text_group, roomName, events.last().senderName, events.last().description) stringProvider.getString(R.string.notification_ticker_text_group, roomName, events.last().senderDisambiguatedDisplayName, events.last().description)
} else { } else {
stringProvider.getString(R.string.notification_ticker_text_dm, events.last().senderName, events.last().description) stringProvider.getString(R.string.notification_ticker_text_dm, events.last().senderDisambiguatedDisplayName, events.last().description)
} }
val largeBitmap = getRoomBitmap(events, imageLoader) val largeBitmap = getRoomBitmap(events, imageLoader)
@ -108,7 +108,7 @@ class RoomGroupMessageCreator @Inject constructor(
null null
} else { } else {
Person.Builder() Person.Builder()
.setName(event.senderName?.annotateForDebug(70)) .setName(event.senderDisambiguatedDisplayName?.annotateForDebug(70))
.setIcon(bitmapLoader.getUserIcon(event.senderAvatarPath, imageLoader)) .setIcon(bitmapLoader.getUserIcon(event.senderAvatarPath, imageLoader))
.setKey(event.senderId.value) .setKey(event.senderId.value)
.build() .build()
@ -152,7 +152,7 @@ class RoomGroupMessageCreator @Inject constructor(
private fun createFirstMessageSummaryLine(event: NotifiableMessageEvent, roomName: String, roomIsDirect: Boolean): CharSequence { private fun createFirstMessageSummaryLine(event: NotifiableMessageEvent, roomName: String, roomIsDirect: Boolean): CharSequence {
return if (roomIsDirect) { return if (roomIsDirect) {
buildSpannedString { buildSpannedString {
event.senderName?.let { event.senderDisambiguatedDisplayName?.let {
inSpans(StyleSpan(Typeface.BOLD)) { inSpans(StyleSpan(Typeface.BOLD)) {
append(it) append(it)
append(": ") append(": ")
@ -165,7 +165,7 @@ class RoomGroupMessageCreator @Inject constructor(
inSpans(StyleSpan(Typeface.BOLD)) { inSpans(StyleSpan(Typeface.BOLD)) {
append(roomName) append(roomName)
append(": ") append(": ")
event.senderName?.let { event.senderDisambiguatedDisplayName?.let {
append(it) append(it)
append(" ") append(" ")
} }

View file

@ -36,7 +36,7 @@ data class NotifiableMessageEvent(
val senderId: UserId, val senderId: UserId,
val noisy: Boolean, val noisy: Boolean,
val timestamp: Long, val timestamp: Long,
val senderName: String?, val senderDisambiguatedDisplayName: String?,
val body: String?, val body: String?,
// We cannot use Uri? type here, as that could trigger a // We cannot use Uri? type here, as that could trigger a
// NotSerializableException when persisting this to storage // NotSerializableException when persisting this to storage
@ -55,7 +55,6 @@ data class NotifiableMessageEvent(
) : NotifiableEvent { ) : NotifiableEvent {
val type: String = EventType.MESSAGE val type: String = EventType.MESSAGE
override val description: String = body ?: "" override val description: String = body ?: ""
val title: String = senderName ?: ""
// Example of value: // Example of value:
// content://io.element.android.x.debug.notifications.fileprovider/downloads/temp/notif/matrix.org/XGItzSDOnSyXjYtOPfiKexDJ // content://io.element.android.x.debug.notifications.fileprovider/downloads/temp/notif/matrix.org/XGItzSDOnSyXjYtOPfiKexDJ

View file

@ -462,7 +462,7 @@ class NotifiableEventResolverTest {
senderId = A_USER_ID_2, senderId = A_USER_ID_2,
noisy = false, noisy = false,
timestamp = A_TIMESTAMP, timestamp = A_TIMESTAMP,
senderName = null, senderDisambiguatedDisplayName = "Bob",
body = "Call in progress (unsupported)", body = "Call in progress (unsupported)",
imageUriString = null, imageUriString = null,
threadId = null, threadId = null,
@ -586,7 +586,7 @@ class NotifiableEventResolverTest {
senderId = A_USER_ID_2, senderId = A_USER_ID_2,
noisy = false, noisy = false,
timestamp = A_TIMESTAMP, timestamp = A_TIMESTAMP,
senderName = "Bob", senderDisambiguatedDisplayName = "Bob",
body = body, body = body,
imageUriString = null, imageUriString = null,
threadId = null, threadId = null,

View file

@ -85,7 +85,7 @@ fun aNotifiableMessageEvent(
editedEventId = null, editedEventId = null,
noisy = false, noisy = false,
timestamp = timestamp, timestamp = timestamp,
senderName = "sender-name", senderDisambiguatedDisplayName = "sender-name",
senderId = UserId("@sending-id:domain.com"), senderId = UserId("@sending-id:domain.com"),
body = "message-body", body = "message-body",
roomId = roomId, roomId = roomId,

View file

@ -566,6 +566,8 @@ private fun ReplyToModeView(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clipToBounds(), .clipToBounds(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = ElementTheme.typography.fontBodySmMedium, style = ElementTheme.typography.fontBodySmMedium,
textAlign = TextAlign.Start, textAlign = TextAlign.Start,
color = ElementTheme.materialColors.primary, color = ElementTheme.materialColors.primary,
@ -731,7 +733,7 @@ internal fun TextComposerReplyPreview() = ElementPreview {
voiceMessageState = VoiceMessageState.Idle, voiceMessageState = VoiceMessageState.Idle,
composerMode = MessageComposerMode.Reply( composerMode = MessageComposerMode.Reply(
isThreaded = true, isThreaded = true,
senderName = "Alice", senderName = "Alice with a very long name to test overflow in the composer",
eventId = EventId("$1234"), eventId = EventId("$1234"),
attachmentThumbnailInfo = null, attachmentThumbnailInfo = null,
defaultContent = "A message\n" + defaultContent = "A message\n" +

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:dfe460e8bab0a27a31984ce8c85ed98aef2a994584f724375cd436d5c1c7c6ca oid sha256:d0a65496766e211ffed310034d3575e5421a4ab1b72c41a09bb6ea17ff7b98e2
size 44758 size 47043

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:63b40ee6d9b4fa6f049dedbfc628380069360e67129b8e53b94e04d25b5897bf oid sha256:dd7e754bf281cc44e6b6e6175e18c66a1575bbf5f25f588f465af37b364b5d63
size 42898 size 45074

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f24d7e9c2ce18612ecc82ebbe28598ebdbe527a7ca6d3f4507a303d41da708c0
size 6497

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7e91d1ffe38ca636208a1e093205893b460f2b7940892bfa641c1401d0f1365d
size 8941

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8fc88d9c3c4ad18d26441e6e8da4f58b8d9e281dd7712adc31080c900dc13e53
size 7101

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d73d668bdc2d792978dd36d4b366264f02f93843e6526ba7533992c77cb881a1
size 6619

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b20874adf3b3cd1e369d11f631bfc6187dae44a816eccb7f6510217409554716
size 8663

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:01b47b2ed0cbb29c8f302bbe5f7c24b9c1579ae92f558e7d5c8a579908f04b50
size 7094

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ba7f63a6d8bda28570e062f62583b7c05bdea6dd477c1cc09ef4f8a2f4041193
size 7125

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ed81c75139e4c049b0749e2ea89012f8e6e87e455b068ef15ee710444c860f4c
size 9536

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dab27eb42b0634e4a3833c08846a4ca42fa687c35c381c768b2ddab68e1a31c8
size 7728

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0162d5572e58c7dfb8697a55a1f9c988758c708173b35816907e569a36f0ab75
size 6580

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:526004d2ac5d035bf945a097f2239430ce8395513a5d3b7f2ea74066d98a0785
size 8938

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f639a20541b3be5de0508abcf70ef86472968b546d8926b8b91742b37637aece
size 7247

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:83edcd9d31bd25873b0448f442322e0932744a409cf8582da0f90aaa5feac21e
size 6468

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:731261ff2c90e668774218eb721e9fad88e2430ed71e355c218cc7f9af40b425
size 8436

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9cf7fca11dc7f6f7e91cd5ff4012552812665eeebdfa146b416fa70f297123c9
size 6837

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e357210874255c01ae10d17bad0a9b2ddf738f9d5d757ecfba8690280a2ec498
size 6906

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:016935a98b882d4d21df4838e5ac5e679a5854ca9cbd73045b9e07cff6673b2b
size 9319

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ee629c6d2e8b01ee2c5ba393563466d1dd39bfadd7a4d3159f6f4fe2d899d57e
size 7358

Some files were not shown because too many files have changed in this diff Show more