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:
@@ -48,8 +48,11 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||
),
|
||||
anActionListState().copy(
|
||||
target = ActionListState.Target.Success(
|
||||
event = aTimelineItemEvent(content = aTimelineItemImageContent()).copy(
|
||||
reactionsState = reactionsState
|
||||
event = aTimelineItemEvent(
|
||||
content = aTimelineItemImageContent(),
|
||||
displayNameAmbiguous = true,
|
||||
).copy(
|
||||
reactionsState = reactionsState,
|
||||
),
|
||||
displayEmojiReactions = true,
|
||||
actions = aTimelineItemActionList(),
|
||||
@@ -142,6 +145,7 @@ fun aTimelineItemActionList(): ImmutableList<TimelineItemAction> {
|
||||
TimelineItemAction.ViewSource,
|
||||
)
|
||||
}
|
||||
|
||||
fun aTimelineItemPollActionList(): ImmutableList<TimelineItemAction> {
|
||||
return persistentListOf(
|
||||
TimelineItemAction.EndPoll,
|
||||
|
||||
@@ -55,6 +55,8 @@ import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
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.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.event.TimelineItemAudioContent
|
||||
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()
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row {
|
||||
if (event.senderDisplayName != null) {
|
||||
Text(
|
||||
text = event.senderDisplayName,
|
||||
style = ElementTheme.typography.fontBodySmMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
SenderName(
|
||||
senderId = event.senderId,
|
||||
senderProfile = event.senderProfile,
|
||||
senderNameMode = SenderNameMode.ActionList,
|
||||
)
|
||||
content()
|
||||
}
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.sender
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||
|
||||
// https://www.figma.com/file/Ni6Ii8YKtmXCKYNE90cC67/Timeline-(new)?type=design&node-id=917-80169&mode=design&t=A0CJCBbMqR8NOwUQ-0
|
||||
@Composable
|
||||
fun SenderName(
|
||||
senderId: UserId,
|
||||
senderProfile: ProfileTimelineDetails,
|
||||
senderNameMode: SenderNameMode,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
when (senderProfile) {
|
||||
is ProfileTimelineDetails.Error,
|
||||
ProfileTimelineDetails.Pending,
|
||||
ProfileTimelineDetails.Unavailable -> {
|
||||
MainText(text = senderId.value, mode = senderNameMode)
|
||||
}
|
||||
is ProfileTimelineDetails.Ready -> {
|
||||
val displayName = senderProfile.displayName
|
||||
if (displayName.isNullOrEmpty()) {
|
||||
MainText(text = senderId.value, mode = senderNameMode)
|
||||
} else {
|
||||
MainText(text = displayName, mode = senderNameMode)
|
||||
if (senderProfile.displayNameAmbiguous) {
|
||||
SecondaryText(text = senderId.value, mode = senderNameMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.MainText(
|
||||
text: String,
|
||||
mode: SenderNameMode,
|
||||
) {
|
||||
val style = when (mode) {
|
||||
is SenderNameMode.Timeline -> ElementTheme.typography.fontBodyMdMedium
|
||||
SenderNameMode.ActionList,
|
||||
SenderNameMode.Reply -> ElementTheme.typography.fontBodySmMedium
|
||||
}
|
||||
val modifier = when (mode) {
|
||||
is SenderNameMode.Timeline -> Modifier.alignByBaseline()
|
||||
SenderNameMode.ActionList,
|
||||
SenderNameMode.Reply -> Modifier
|
||||
}
|
||||
val color = when (mode) {
|
||||
is SenderNameMode.Timeline -> mode.mainColor
|
||||
SenderNameMode.ActionList,
|
||||
SenderNameMode.Reply -> MaterialTheme.colorScheme.primary
|
||||
}
|
||||
Text(
|
||||
modifier = modifier.clipToBounds(),
|
||||
text = text,
|
||||
style = style,
|
||||
color = color,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.SecondaryText(
|
||||
text: String,
|
||||
mode: SenderNameMode,
|
||||
) {
|
||||
val style = when (mode) {
|
||||
is SenderNameMode.Timeline -> ElementTheme.typography.fontBodySmRegular
|
||||
SenderNameMode.ActionList,
|
||||
SenderNameMode.Reply -> ElementTheme.typography.fontBodyXsRegular
|
||||
}
|
||||
val modifier = when (mode) {
|
||||
is SenderNameMode.Timeline -> Modifier.alignByBaseline()
|
||||
SenderNameMode.ActionList,
|
||||
SenderNameMode.Reply -> Modifier
|
||||
}
|
||||
Text(
|
||||
modifier = modifier.clipToBounds(),
|
||||
text = text,
|
||||
style = style,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun SenderNamePreview(
|
||||
@PreviewParameter(SenderNameDataProvider::class) senderNameData: SenderNameData,
|
||||
) = ElementPreview {
|
||||
SenderName(
|
||||
senderId = senderNameData.userId,
|
||||
senderProfile = senderNameData.profileTimelineDetails,
|
||||
senderNameMode = senderNameData.senderNameMode,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,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,
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.sender
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
sealed interface SenderNameMode {
|
||||
data class Timeline(val mainColor: Color) : SenderNameMode
|
||||
data object Reply : SenderNameMode
|
||||
data object ActionList : SenderNameMode
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package io.element.android.features.messages.impl.timeline
|
||||
|
||||
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.model.InReplyToDetails
|
||||
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
||||
@@ -131,6 +132,7 @@ internal fun aTimelineItemEvent(
|
||||
isMine: Boolean = false,
|
||||
isEditable: Boolean = false,
|
||||
senderDisplayName: String = "Sender",
|
||||
displayNameAmbiguous: Boolean = false,
|
||||
content: TimelineItemEventContent = aTimelineItemTextContent(),
|
||||
groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
|
||||
sendState: LocalEventSendState? = null,
|
||||
@@ -152,7 +154,10 @@ internal fun aTimelineItemEvent(
|
||||
sentTime = "12:34",
|
||||
isMine = isMine,
|
||||
isEditable = isEditable,
|
||||
senderDisplayName = senderDisplayName,
|
||||
senderProfile = aProfileTimelineDetailsReady(
|
||||
displayName = senderDisplayName,
|
||||
displayNameAmbiguous = displayNameAmbiguous,
|
||||
),
|
||||
groupPosition = groupPosition,
|
||||
localSendState = sendState,
|
||||
inReplyTo = inReplyTo,
|
||||
|
||||
@@ -69,6 +69,8 @@ import androidx.constraintlayout.compose.ConstrainScope
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
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.TimelineRoomInfo
|
||||
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.matrix.api.core.EventId
|
||||
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.testtags.TestTags
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
@@ -291,7 +295,8 @@ private fun TimelineItemEventRowContent(
|
||||
val avatarStrokeSize = 3.dp
|
||||
if (event.showSenderInformation && !timelineRoomInfo.isDm) {
|
||||
MessageSenderInformation(
|
||||
event.safeSenderName,
|
||||
event.senderId,
|
||||
event.senderProfile,
|
||||
event.senderAvatar,
|
||||
avatarStrokeSize,
|
||||
Modifier
|
||||
@@ -371,7 +376,8 @@ private fun TimelineItemEventRowContent(
|
||||
|
||||
@Composable
|
||||
private fun MessageSenderInformation(
|
||||
sender: String,
|
||||
senderId: UserId,
|
||||
senderProfile: ProfileTimelineDetails,
|
||||
senderAvatar: AvatarData,
|
||||
avatarStrokeSize: Dp,
|
||||
modifier: Modifier = Modifier
|
||||
@@ -398,13 +404,10 @@ private fun MessageSenderInformation(
|
||||
Row {
|
||||
Avatar(senderAvatar)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
modifier = Modifier.clipToBounds(),
|
||||
text = sender,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = avatarColors.foreground,
|
||||
style = ElementTheme.typography.fontBodyMdMedium,
|
||||
SenderName(
|
||||
senderId = senderId,
|
||||
senderProfile = senderProfile,
|
||||
senderNameMode = SenderNameMode.Timeline(avatarColors.foreground),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -561,10 +564,10 @@ private fun MessageEventBubbleContent(
|
||||
}
|
||||
}
|
||||
val inReplyTo = @Composable { inReplyTo: InReplyToDetails ->
|
||||
val senderName = inReplyTo.senderDisplayName ?: inReplyTo.senderId.value
|
||||
val topPadding = if (showThreadDecoration) 0.dp else 8.dp
|
||||
ReplyToContent(
|
||||
senderName = senderName,
|
||||
senderId = inReplyTo.senderId,
|
||||
senderProfile = inReplyTo.senderProfile,
|
||||
metadata = inReplyTo.metadata(),
|
||||
modifier = Modifier
|
||||
.padding(top = topPadding, start = 8.dp, end = 8.dp)
|
||||
@@ -609,7 +612,8 @@ private fun MessageEventBubbleContent(
|
||||
|
||||
@Composable
|
||||
private fun ReplyToContent(
|
||||
senderName: String,
|
||||
senderId: UserId,
|
||||
senderProfile: ProfileTimelineDetails,
|
||||
metadata: InReplyToMetadata?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -633,18 +637,15 @@ private fun ReplyToContent(
|
||||
)
|
||||
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) {
|
||||
Text(
|
||||
SenderName(
|
||||
senderId = senderId,
|
||||
senderProfile = senderProfile,
|
||||
senderNameMode = SenderNameMode.Reply,
|
||||
modifier = Modifier.semantics {
|
||||
contentDescription = a11InReplyToText
|
||||
},
|
||||
text = senderName,
|
||||
style = ElementTheme.typography.fontBodySmMedium,
|
||||
textAlign = TextAlign.Start,
|
||||
color = ElementTheme.materialColors.primary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
ReplyToContentText(metadata)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,6 @@ internal fun TimelineItemEventRowTimestampPreview(
|
||||
body = str,
|
||||
),
|
||||
reactionsState = aTimelineItemReactions(count = 0),
|
||||
senderDisplayName = "A sender",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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.NoticeMessageType
|
||||
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.TextMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
|
||||
@@ -58,7 +59,10 @@ internal fun TimelineItemEventRowWithReplyPreview(
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun TimelineItemEventRowWithReplyContentToPreview(inReplyToDetails: InReplyToDetails) {
|
||||
internal fun TimelineItemEventRowWithReplyContentToPreview(
|
||||
inReplyToDetails: InReplyToDetails,
|
||||
displayNameAmbiguous: Boolean = false,
|
||||
) {
|
||||
Column {
|
||||
sequenceOf(false, true).forEach {
|
||||
ATimelineItemEventRow(
|
||||
@@ -69,6 +73,7 @@ internal fun TimelineItemEventRowWithReplyContentToPreview(inReplyToDetails: InR
|
||||
body = "A reply."
|
||||
),
|
||||
inReplyTo = inReplyToDetails,
|
||||
displayNameAmbiguous = displayNameAmbiguous,
|
||||
groupPosition = TimelineItemGroupPosition.First,
|
||||
),
|
||||
)
|
||||
@@ -80,6 +85,7 @@ internal fun TimelineItemEventRowWithReplyContentToPreview(inReplyToDetails: InR
|
||||
aspectRatio = 2.5f
|
||||
),
|
||||
inReplyTo = inReplyToDetails,
|
||||
displayNameAmbiguous = displayNameAmbiguous,
|
||||
isThreaded = true,
|
||||
groupPosition = TimelineItemGroupPosition.Last,
|
||||
),
|
||||
@@ -150,7 +156,7 @@ open class InReplyToDetailsProvider : PreviewParameterProvider<InReplyToDetails>
|
||||
)
|
||||
}
|
||||
|
||||
private fun aMessageContent(
|
||||
protected fun aMessageContent(
|
||||
body: String,
|
||||
type: MessageType,
|
||||
) = MessageContent(
|
||||
@@ -163,12 +169,24 @@ open class InReplyToDetailsProvider : PreviewParameterProvider<InReplyToDetails>
|
||||
|
||||
protected fun aInReplyToDetails(
|
||||
eventContent: EventContent,
|
||||
displayNameAmbiguous: Boolean = false,
|
||||
) = InReplyToDetails(
|
||||
eventId = EventId("\$event"),
|
||||
eventContent = eventContent,
|
||||
senderId = UserId("@Sender:domain"),
|
||||
senderDisplayName = "Sender",
|
||||
senderAvatarUrl = null,
|
||||
senderProfile = aProfileTimelineDetailsReady(
|
||||
displayNameAmbiguous = displayNameAmbiguous,
|
||||
),
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -52,8 +52,12 @@ class TimelineItemContentFactory @Inject constructor(
|
||||
is FailedToParseMessageLikeContent -> failedToParseMessageFactory.create(itemContent)
|
||||
is FailedToParseStateContent -> failedToParseStateFactory.create(itemContent)
|
||||
is MessageContent -> {
|
||||
val senderDisplayName = eventTimelineItem.senderProfile.getDisambiguatedDisplayName(eventTimelineItem.sender)
|
||||
messageFactory.create(itemContent, senderDisplayName, eventTimelineItem.eventId)
|
||||
val senderDisambiguatedDisplayName = eventTimelineItem.senderProfile.getDisambiguatedDisplayName(eventTimelineItem.sender)
|
||||
messageFactory.create(
|
||||
content = itemContent,
|
||||
senderDisambiguatedDisplayName = senderDisambiguatedDisplayName,
|
||||
eventId = eventTimelineItem.eventId,
|
||||
)
|
||||
}
|
||||
is ProfileChangeContent -> profileChangeFactory.create(eventTimelineItem)
|
||||
is RedactedContent -> redactedMessageFactory.create(itemContent)
|
||||
|
||||
@@ -70,17 +70,21 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||
private val htmlConverterProvider: HtmlConverterProvider,
|
||||
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) {
|
||||
is EmoteMessageType -> {
|
||||
val emoteBody = "* $senderDisplayName ${messageType.body.trimEnd()}"
|
||||
val emoteBody = "* $senderDisambiguatedDisplayName ${messageType.body.trimEnd()}"
|
||||
TimelineItemEmoteContent(
|
||||
body = emoteBody,
|
||||
htmlDocument = messageType.formatted?.toHtmlDocument(
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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.room.RoomMember
|
||||
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 kotlinx.collections.immutable.toImmutableList
|
||||
import java.text.DateFormat
|
||||
@@ -55,15 +55,14 @@ class TimelineItemEventFactory @Inject constructor(
|
||||
val currentSender = currentTimelineItem.event.sender
|
||||
val groupPosition =
|
||||
computeGroupPosition(currentTimelineItem, timelineItems, index)
|
||||
val (senderDisplayName, senderAvatarUrl) = currentTimelineItem.getSenderInfo()
|
||||
|
||||
val senderProfile = currentTimelineItem.event.senderProfile
|
||||
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
|
||||
val sentTime = timeFormatter.format(Date(currentTimelineItem.event.timestamp))
|
||||
|
||||
val senderAvatarData = AvatarData(
|
||||
id = currentSender.value,
|
||||
name = senderDisplayName ?: currentSender.value,
|
||||
url = senderAvatarUrl,
|
||||
name = senderProfile.getDisambiguatedDisplayName(currentSender),
|
||||
url = senderProfile.getAvatarUrl(),
|
||||
size = AvatarSize.TimelineSender
|
||||
)
|
||||
currentTimelineItem.event
|
||||
@@ -72,7 +71,7 @@ class TimelineItemEventFactory @Inject constructor(
|
||||
eventId = currentTimelineItem.eventId,
|
||||
transactionId = currentTimelineItem.transactionId,
|
||||
senderId = currentSender,
|
||||
senderDisplayName = senderDisplayName,
|
||||
senderProfile = senderProfile,
|
||||
senderAvatar = senderAvatarData,
|
||||
content = contentFactory.create(currentTimelineItem.event),
|
||||
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 {
|
||||
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
|
||||
var aggregatedReactions = event.reactions.map { reaction ->
|
||||
|
||||
@@ -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.InReplyTo
|
||||
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.TextMessageType
|
||||
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(
|
||||
val eventId: EventId,
|
||||
val senderId: UserId,
|
||||
val senderDisplayName: String?,
|
||||
val senderAvatarUrl: String?,
|
||||
val senderProfile: ProfileTimelineDetails,
|
||||
val eventContent: EventContent?,
|
||||
val textContent: String?,
|
||||
)
|
||||
@@ -41,8 +41,7 @@ fun InReplyTo.map(
|
||||
is InReplyTo.Ready -> InReplyToDetails(
|
||||
eventId = eventId,
|
||||
senderId = senderId,
|
||||
senderDisplayName = senderDisplayName,
|
||||
senderAvatarUrl = senderAvatarUrl,
|
||||
senderProfile = senderProfile,
|
||||
eventContent = content,
|
||||
textContent = when (content) {
|
||||
is MessageContent -> {
|
||||
|
||||
@@ -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.timeline.item.TimelineItemDebugInfo
|
||||
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.getDisambiguatedDisplayName
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
@@ -57,7 +59,7 @@ sealed interface TimelineItem {
|
||||
val eventId: EventId? = null,
|
||||
val transactionId: TransactionId? = null,
|
||||
val senderId: UserId,
|
||||
val senderDisplayName: String?,
|
||||
val senderProfile: ProfileTimelineDetails,
|
||||
val senderAvatar: AvatarData,
|
||||
val content: TimelineItemEventContent,
|
||||
val sentTime: String = "",
|
||||
@@ -74,7 +76,7 @@ sealed interface TimelineItem {
|
||||
) : TimelineItem {
|
||||
val showSenderInformation = groupPosition.isNew() && !isMine
|
||||
|
||||
val safeSenderName: String = senderDisplayName ?: senderId.value
|
||||
val safeSenderName: String = senderProfile.getDisambiguatedDisplayName(senderId)
|
||||
|
||||
val failedToSend: Boolean = localSendState is LocalEventSendState.SendingFailed
|
||||
|
||||
|
||||
@@ -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.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.ReadReceiptData
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
@@ -48,7 +49,7 @@ internal fun aMessageEvent(
|
||||
id = eventId?.value.orEmpty(),
|
||||
eventId = eventId,
|
||||
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),
|
||||
content = content,
|
||||
sentTime = "",
|
||||
|
||||
@@ -82,7 +82,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||
val sut = createTimelineItemContentMessageFactory()
|
||||
val result = sut.create(
|
||||
content = createMessageContent(type = OtherMessageType(msgType = "a_type", body = "body")),
|
||||
senderDisplayName = "Bob",
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
val expected = TimelineItemTextContent(
|
||||
@@ -100,7 +100,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||
val sut = createTimelineItemContentMessageFactory()
|
||||
val result = sut.create(
|
||||
content = createMessageContent(type = LocationMessageType("body", "geo:1,2", "description")),
|
||||
senderDisplayName = "Bob",
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
val expected = TimelineItemLocationContent(
|
||||
@@ -116,7 +116,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||
val sut = createTimelineItemContentMessageFactory()
|
||||
val result = sut.create(
|
||||
content = createMessageContent(type = LocationMessageType("body", "", null)),
|
||||
senderDisplayName = "Bob",
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
val expected = TimelineItemTextContent(
|
||||
@@ -134,7 +134,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||
val sut = createTimelineItemContentMessageFactory()
|
||||
val result = sut.create(
|
||||
content = createMessageContent(type = TextMessageType("body", null)),
|
||||
senderDisplayName = "Bob",
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
val expected = TimelineItemTextContent(
|
||||
@@ -152,7 +152,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||
val sut = createTimelineItemContentMessageFactory()
|
||||
val result = sut.create(
|
||||
content = createMessageContent(type = TextMessageType("https://www.example.org", null)),
|
||||
senderDisplayName = "Bob",
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
eventId = AN_EVENT_ID,
|
||||
) as TimelineItemTextContent
|
||||
val expected = TimelineItemTextContent(
|
||||
@@ -200,7 +200,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||
formatted = FormattedBody(MessageFormat.HTML, expected.toString())
|
||||
)
|
||||
),
|
||||
senderDisplayName = "Bob",
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
assertThat((result as TimelineItemTextContent).formattedBody).isEqualTo(expected)
|
||||
@@ -218,7 +218,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||
formatted = FormattedBody(MessageFormat.UNKNOWN, "formatted")
|
||||
)
|
||||
),
|
||||
senderDisplayName = "Bob",
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
assertThat((result as TimelineItemTextContent).formattedBody).isNull()
|
||||
@@ -229,7 +229,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||
val sut = createTimelineItemContentMessageFactory()
|
||||
val result = sut.create(
|
||||
content = createMessageContent(type = VideoMessageType("body", null, null, MediaSource("url"), null)),
|
||||
senderDisplayName = "Bob",
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
val expected = TimelineItemVideoContent(
|
||||
@@ -277,7 +277,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||
),
|
||||
)
|
||||
),
|
||||
senderDisplayName = "Bob",
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
val expected = TimelineItemVideoContent(
|
||||
@@ -303,7 +303,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||
val sut = createTimelineItemContentMessageFactory()
|
||||
val result = sut.create(
|
||||
content = createMessageContent(type = AudioMessageType("body", MediaSource("url"), null)),
|
||||
senderDisplayName = "Bob",
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
val expected = TimelineItemAudioContent(
|
||||
@@ -332,7 +332,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||
)
|
||||
)
|
||||
),
|
||||
senderDisplayName = "Bob",
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
val expected = TimelineItemAudioContent(
|
||||
@@ -351,7 +351,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||
val sut = createTimelineItemContentMessageFactory()
|
||||
val result = sut.create(
|
||||
content = createMessageContent(type = VoiceMessageType("body", MediaSource("url"), null, null)),
|
||||
senderDisplayName = "Bob",
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
val expected = TimelineItemVoiceContent(
|
||||
@@ -384,7 +384,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||
),
|
||||
)
|
||||
),
|
||||
senderDisplayName = "Bob",
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
val expected = TimelineItemVoiceContent(
|
||||
@@ -409,7 +409,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||
)
|
||||
val result = sut.create(
|
||||
content = createMessageContent(type = VoiceMessageType("body", MediaSource("url"), null, null)),
|
||||
senderDisplayName = "Bob",
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
val expected = TimelineItemAudioContent(
|
||||
@@ -428,7 +428,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||
val sut = createTimelineItemContentMessageFactory()
|
||||
val result = sut.create(
|
||||
content = createMessageContent(type = ImageMessageType("body", null, null, MediaSource("url"), null)),
|
||||
senderDisplayName = "Bob",
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
val expected = TimelineItemImageContent(
|
||||
@@ -499,7 +499,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||
)
|
||||
)
|
||||
),
|
||||
senderDisplayName = "Bob",
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
val expected = TimelineItemImageContent(
|
||||
@@ -524,7 +524,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||
val sut = createTimelineItemContentMessageFactory()
|
||||
val result = sut.create(
|
||||
content = createMessageContent(type = FileMessageType("body", MediaSource("url"), null)),
|
||||
senderDisplayName = "Bob",
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
val expected = TimelineItemFileContent(
|
||||
@@ -559,7 +559,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||
)
|
||||
)
|
||||
),
|
||||
senderDisplayName = "Bob",
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
val expected = TimelineItemFileContent(
|
||||
@@ -578,7 +578,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||
val sut = createTimelineItemContentMessageFactory()
|
||||
val result = sut.create(
|
||||
content = createMessageContent(type = NoticeMessageType("body", null)),
|
||||
senderDisplayName = "Bob",
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
val expected = TimelineItemNoticeContent(
|
||||
@@ -601,7 +601,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||
formatted = FormattedBody(MessageFormat.HTML, "formatted")
|
||||
)
|
||||
),
|
||||
senderDisplayName = "Bob",
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
assertThat((result as TimelineItemNoticeContent).formattedBody).isEqualTo("formatted")
|
||||
@@ -612,7 +612,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||
val sut = createTimelineItemContentMessageFactory()
|
||||
val result = sut.create(
|
||||
content = createMessageContent(type = EmoteMessageType("body", null)),
|
||||
senderDisplayName = "Bob",
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
val expected = TimelineItemEmoteContent(
|
||||
@@ -635,7 +635,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||
formatted = FormattedBody(MessageFormat.HTML, "formatted")
|
||||
)
|
||||
),
|
||||
senderDisplayName = "Bob",
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
assertThat((result as TimelineItemEmoteContent).formattedBody).isEqualTo(SpannableString("* Bob formatted"))
|
||||
|
||||
@@ -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.timeline.aTimelineItemDebugInfo
|
||||
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.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts
|
||||
@@ -39,7 +40,7 @@ class TimelineItemGrouperTest {
|
||||
id = "0",
|
||||
senderId = A_USER_ID,
|
||||
senderAvatar = anAvatarData(),
|
||||
senderDisplayName = "",
|
||||
senderProfile = aProfileTimelineDetailsReady(displayName = ""),
|
||||
content = TimelineItemStateEventContent(body = "a state event"),
|
||||
reactionsState = aTimelineItemReactions(count = 0),
|
||||
readReceiptState = TimelineItemReadReceipts(emptyList<ReadReceiptData>().toImmutableList()),
|
||||
|
||||
@@ -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.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||
import io.element.android.libraries.matrix.test.timeline.aProfileTimelineDetails
|
||||
import org.junit.Test
|
||||
|
||||
class InReplyToDetailTest {
|
||||
@@ -54,8 +55,7 @@ class InReplyToDetailTest {
|
||||
val inReplyTo = InReplyTo.Ready(
|
||||
eventId = AN_EVENT_ID,
|
||||
senderId = A_USER_ID,
|
||||
senderDisplayName = "senderDisplayName",
|
||||
senderAvatarUrl = "senderAvatarUrl",
|
||||
senderProfile = aProfileTimelineDetails(),
|
||||
content = RoomMembershipContent(
|
||||
userId = A_USER_ID,
|
||||
change = MembershipChange.INVITED,
|
||||
@@ -73,8 +73,7 @@ class InReplyToDetailTest {
|
||||
val inReplyTo = InReplyTo.Ready(
|
||||
eventId = AN_EVENT_ID,
|
||||
senderId = A_USER_ID,
|
||||
senderDisplayName = "senderDisplayName",
|
||||
senderAvatarUrl = "senderAvatarUrl",
|
||||
senderProfile = aProfileTimelineDetails(),
|
||||
content = MessageContent(
|
||||
body = "**Hello!**",
|
||||
inReplyTo = null,
|
||||
@@ -101,8 +100,7 @@ class InReplyToDetailTest {
|
||||
val inReplyTo = InReplyTo.Ready(
|
||||
eventId = AN_EVENT_ID,
|
||||
senderId = A_USER_ID,
|
||||
senderDisplayName = "senderDisplayName",
|
||||
senderAvatarUrl = "senderAvatarUrl",
|
||||
senderProfile = aProfileTimelineDetails(),
|
||||
content = MessageContent(
|
||||
body = "**Hello!**",
|
||||
inReplyTo = null,
|
||||
|
||||
@@ -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.OtherState
|
||||
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.RoomMembershipContent
|
||||
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.timeline.aMessageContent
|
||||
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.AttachmentThumbnailInfo
|
||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
|
||||
@@ -430,15 +432,13 @@ class InReplyToMetadataKtTest {
|
||||
fun anInReplyToDetails(
|
||||
eventId: EventId = AN_EVENT_ID,
|
||||
senderId: UserId = A_USER_ID,
|
||||
senderDisplayName: String? = "senderDisplayName",
|
||||
senderAvatarUrl: String? = "senderAvatarUrl",
|
||||
senderProfile: ProfileTimelineDetails = aProfileTimelineDetails(),
|
||||
eventContent: EventContent? = aMessageContent(),
|
||||
textContent: String? = "textContent",
|
||||
) = InReplyToDetails(
|
||||
eventId = eventId,
|
||||
senderId = senderId,
|
||||
senderDisplayName = senderDisplayName,
|
||||
senderAvatarUrl = senderAvatarUrl,
|
||||
senderProfile = senderProfile,
|
||||
eventContent = eventContent,
|
||||
textContent = textContent,
|
||||
)
|
||||
|
||||
@@ -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.PollContent
|
||||
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.RoomMembershipContent
|
||||
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.VideoMessageType
|
||||
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.ui.strings.CommonStrings
|
||||
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? {
|
||||
val isOutgoing = event.isOwn
|
||||
// Note: we do not use disambiguated display name here, see
|
||||
// https://github.com/element-hq/element-x-ios/issues/1845#issuecomment-1888707428
|
||||
val senderDisplayName = (event.senderProfile as? ProfileTimelineDetails.Ready)?.displayName ?: event.sender.value
|
||||
val senderDisambiguatedDisplayName = event.senderProfile.getDisambiguatedDisplayName(event.sender)
|
||||
return when (val content = event.content) {
|
||||
is MessageContent -> processMessageContents(content, senderDisplayName, isDmRoom)
|
||||
is MessageContent -> processMessageContents(content, senderDisambiguatedDisplayName, isDmRoom)
|
||||
RedactedContent -> {
|
||||
val message = sp.getString(CommonStrings.common_message_removed)
|
||||
if (!isDmRoom) {
|
||||
prefix(message, senderDisplayName)
|
||||
prefix(message, senderDisambiguatedDisplayName)
|
||||
} else {
|
||||
message
|
||||
}
|
||||
@@ -91,36 +89,40 @@ class DefaultRoomLastMessageFormatter @Inject constructor(
|
||||
is UnableToDecryptContent -> {
|
||||
val message = sp.getString(CommonStrings.common_waiting_for_decryption_key)
|
||||
if (!isDmRoom) {
|
||||
prefix(message, senderDisplayName)
|
||||
prefix(message, senderDisambiguatedDisplayName)
|
||||
} else {
|
||||
message
|
||||
}
|
||||
}
|
||||
is RoomMembershipContent -> {
|
||||
roomMembershipContentFormatter.format(content, senderDisplayName, isOutgoing)
|
||||
roomMembershipContentFormatter.format(content, senderDisambiguatedDisplayName, isOutgoing)
|
||||
}
|
||||
is ProfileChangeContent -> {
|
||||
profileChangeContentFormatter.format(content, event.sender, senderDisplayName, isOutgoing)
|
||||
profileChangeContentFormatter.format(content, event.sender, senderDisambiguatedDisplayName, isOutgoing)
|
||||
}
|
||||
is StateContent -> {
|
||||
stateContentFormatter.format(content, senderDisplayName, isOutgoing, RenderingMode.RoomList)
|
||||
stateContentFormatter.format(content, senderDisambiguatedDisplayName, isOutgoing, RenderingMode.RoomList)
|
||||
}
|
||||
is PollContent -> {
|
||||
val message = sp.getString(CommonStrings.common_poll_summary, content.question)
|
||||
prefixIfNeeded(message, senderDisplayName, isDmRoom)
|
||||
prefixIfNeeded(message, senderDisambiguatedDisplayName, isDmRoom)
|
||||
}
|
||||
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)
|
||||
}?.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) {
|
||||
// Doesn't need a prefix
|
||||
is EmoteMessageType -> {
|
||||
return "* $senderDisplayName ${messageType.body}"
|
||||
return "* $senderDisambiguatedDisplayName ${messageType.body}"
|
||||
}
|
||||
is TextMessageType -> {
|
||||
messageType.toPlainText(permalinkParser)
|
||||
@@ -153,19 +155,23 @@ class DefaultRoomLastMessageFormatter @Inject constructor(
|
||||
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
|
||||
} 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 {
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(senderDisplayName)
|
||||
append(senderDisambiguatedDisplayName)
|
||||
}
|
||||
append(": ")
|
||||
append(message)
|
||||
|
||||
@@ -49,16 +49,16 @@ class DefaultTimelineEventFormatter @Inject constructor(
|
||||
) : TimelineEventFormatter {
|
||||
override fun format(event: EventTimelineItem): CharSequence? {
|
||||
val isOutgoing = event.isOwn
|
||||
val senderDisplayName = event.senderProfile.getDisambiguatedDisplayName(event.sender)
|
||||
val senderDisambiguatedDisplayName = event.senderProfile.getDisambiguatedDisplayName(event.sender)
|
||||
return when (val content = event.content) {
|
||||
is RoomMembershipContent -> {
|
||||
roomMembershipContentFormatter.format(content, senderDisplayName, isOutgoing)
|
||||
roomMembershipContentFormatter.format(content, senderDisambiguatedDisplayName, isOutgoing)
|
||||
}
|
||||
is ProfileChangeContent -> {
|
||||
profileChangeContentFormatter.format(content, event.sender, senderDisplayName, isOutgoing)
|
||||
profileChangeContentFormatter.format(content, event.sender, senderDisambiguatedDisplayName, isOutgoing)
|
||||
}
|
||||
is StateContent -> {
|
||||
stateContentFormatter.format(content, senderDisplayName, isOutgoing, RenderingMode.Timeline)
|
||||
stateContentFormatter.format(content, senderDisambiguatedDisplayName, isOutgoing, RenderingMode.Timeline)
|
||||
}
|
||||
is LegacyCallInviteContent -> {
|
||||
sp.getString(CommonStrings.common_call_invite)
|
||||
|
||||
@@ -27,14 +27,19 @@ class ProfileChangeContentFormatter @Inject constructor(
|
||||
fun format(
|
||||
profileChangeContent: ProfileChangeContent,
|
||||
senderId: UserId,
|
||||
senderDisplayName: String,
|
||||
senderDisambiguatedDisplayName: String,
|
||||
senderIsYou: Boolean,
|
||||
): String? = profileChangeContent.run {
|
||||
val displayNameChanged = displayName != prevDisplayName
|
||||
val avatarChanged = avatarUrl != prevAvatarUrl
|
||||
return when {
|
||||
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)
|
||||
"$message\n$avatarChangedToo"
|
||||
}
|
||||
@@ -63,7 +68,7 @@ class ProfileChangeContentFormatter @Inject constructor(
|
||||
if (senderIsYou) {
|
||||
sp.getString(R.string.state_event_avatar_url_changed_by_you)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_avatar_url_changed, senderDisplayName)
|
||||
sp.getString(R.string.state_event_avatar_url_changed, senderDisambiguatedDisplayName)
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
|
||||
@@ -29,7 +29,7 @@ class RoomMembershipContentFormatter @Inject constructor(
|
||||
) {
|
||||
fun format(
|
||||
membershipContent: RoomMembershipContent,
|
||||
senderDisplayName: String,
|
||||
senderDisambiguatedDisplayName: String,
|
||||
senderIsYou: Boolean,
|
||||
): CharSequence? {
|
||||
val userId = membershipContent.userId
|
||||
@@ -38,34 +38,34 @@ class RoomMembershipContentFormatter @Inject constructor(
|
||||
MembershipChange.JOINED -> if (memberIsYou) {
|
||||
sp.getString(R.string.state_event_room_join_by_you)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_join, userId.value)
|
||||
sp.getString(R.string.state_event_room_join, senderDisambiguatedDisplayName)
|
||||
}
|
||||
MembershipChange.LEFT -> if (memberIsYou) {
|
||||
sp.getString(R.string.state_event_room_leave_by_you)
|
||||
} 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) {
|
||||
sp.getString(R.string.state_event_room_ban_by_you, userId.value)
|
||||
} 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) {
|
||||
sp.getString(R.string.state_event_room_unban_by_you, userId.value)
|
||||
} 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) {
|
||||
sp.getString(R.string.state_event_room_remove_by_you, userId.value)
|
||||
} 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) {
|
||||
sp.getString(R.string.state_event_room_invite_by_you, userId.value)
|
||||
} else if (memberIsYou) {
|
||||
sp.getString(R.string.state_event_room_invite_you, senderDisplayName)
|
||||
sp.getString(R.string.state_event_room_invite_you, senderDisambiguatedDisplayName)
|
||||
} 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) {
|
||||
sp.getString(R.string.state_event_room_invite_accepted_by_you)
|
||||
@@ -80,34 +80,34 @@ class RoomMembershipContentFormatter @Inject constructor(
|
||||
MembershipChange.INVITATION_REVOKED -> if (senderIsYou) {
|
||||
sp.getString(R.string.state_event_room_third_party_revoked_invite_by_you, userId.value)
|
||||
} 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) {
|
||||
sp.getString(R.string.state_event_room_knock_by_you)
|
||||
} 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) {
|
||||
sp.getString(R.string.state_event_room_knock_accepted_by_you, userId.value)
|
||||
} 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) {
|
||||
sp.getString(R.string.state_event_room_knock_retracted_by_you)
|
||||
} 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) {
|
||||
sp.getString(R.string.state_event_room_knock_denied_by_you, userId.value)
|
||||
} 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 {
|
||||
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) {
|
||||
sp.getString(R.string.state_event_room_none_by_you)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_none, senderDisplayName)
|
||||
sp.getString(R.string.state_event_room_none, senderDisambiguatedDisplayName)
|
||||
}
|
||||
MembershipChange.ERROR -> {
|
||||
Timber.v("Filtering timeline item for room membership: $membershipContent")
|
||||
|
||||
@@ -29,7 +29,7 @@ class StateContentFormatter @Inject constructor(
|
||||
) {
|
||||
fun format(
|
||||
stateContent: StateContent,
|
||||
senderDisplayName: String,
|
||||
senderDisambiguatedDisplayName: String,
|
||||
senderIsYou: Boolean,
|
||||
renderingMode: RenderingMode,
|
||||
): CharSequence? {
|
||||
@@ -39,15 +39,15 @@ class StateContentFormatter @Inject constructor(
|
||||
when {
|
||||
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_changed, senderDisplayName)
|
||||
else -> sp.getString(R.string.state_event_room_avatar_removed, senderDisplayName)
|
||||
!senderIsYou && hasAvatarUrl -> sp.getString(R.string.state_event_room_avatar_changed, senderDisambiguatedDisplayName)
|
||||
else -> sp.getString(R.string.state_event_room_avatar_removed, senderDisambiguatedDisplayName)
|
||||
}
|
||||
}
|
||||
is OtherState.RoomCreate -> {
|
||||
if (senderIsYou) {
|
||||
sp.getString(R.string.state_event_room_created_by_you)
|
||||
} 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)
|
||||
@@ -56,8 +56,8 @@ class StateContentFormatter @Inject constructor(
|
||||
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_removed_by_you)
|
||||
!senderIsYou && hasRoomName -> sp.getString(R.string.state_event_room_name_changed, senderDisplayName, content.name)
|
||||
else -> sp.getString(R.string.state_event_room_name_removed, senderDisplayName)
|
||||
!senderIsYou && hasRoomName -> sp.getString(R.string.state_event_room_name_changed, senderDisambiguatedDisplayName, content.name)
|
||||
else -> sp.getString(R.string.state_event_room_name_removed, senderDisambiguatedDisplayName)
|
||||
}
|
||||
}
|
||||
is OtherState.RoomThirdPartyInvite -> {
|
||||
@@ -68,7 +68,7 @@ class StateContentFormatter @Inject constructor(
|
||||
if (senderIsYou) {
|
||||
sp.getString(R.string.state_event_room_third_party_invite_by_you, content.displayName)
|
||||
} 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 -> {
|
||||
@@ -76,8 +76,8 @@ class StateContentFormatter @Inject constructor(
|
||||
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_removed_by_you)
|
||||
!senderIsYou && hasRoomTopic -> sp.getString(R.string.state_event_room_topic_changed, senderDisplayName, content.topic)
|
||||
else -> sp.getString(R.string.state_event_room_topic_removed, senderDisplayName)
|
||||
!senderIsYou && hasRoomTopic -> sp.getString(R.string.state_event_room_topic_changed, senderDisambiguatedDisplayName, content.topic)
|
||||
else -> sp.getString(R.string.state_event_room_topic_removed, senderDisambiguatedDisplayName)
|
||||
}
|
||||
}
|
||||
is OtherState.Custom -> when (renderingMode) {
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<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_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_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>
|
||||
|
||||
@@ -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.OtherMessageType
|
||||
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.RoomMembershipContent
|
||||
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.timeline.aPollContent
|
||||
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.services.toolbox.impl.strings.AndroidStringProvider
|
||||
import org.junit.Before
|
||||
@@ -264,7 +264,7 @@ class DefaultRoomLastMessageFormatterTest {
|
||||
|
||||
val someoneJoinedRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
|
||||
val someoneJoinedRoom = formatter.format(someoneJoinedRoomEvent, false)
|
||||
assertThat(someoneJoinedRoom).isEqualTo("${someoneContent.userId} joined the room")
|
||||
assertThat(someoneJoinedRoom).isEqualTo("$otherName joined the room")
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -280,7 +280,7 @@ class DefaultRoomLastMessageFormatterTest {
|
||||
|
||||
val someoneLeftRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
|
||||
val someoneLeftRoom = formatter.format(someoneLeftRoomEvent, false)
|
||||
assertThat(someoneLeftRoom).isEqualTo("${someoneContent.userId} left the room")
|
||||
assertThat(someoneLeftRoom).isEqualTo("$otherName left the room")
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -421,7 +421,7 @@ class DefaultRoomLastMessageFormatterTest {
|
||||
|
||||
val someoneKnockedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
|
||||
val someoneKnocked = formatter.format(someoneKnockedEvent, false)
|
||||
assertThat(someoneKnocked).isEqualTo("${someoneContent.userId} requested to join")
|
||||
assertThat(someoneKnocked).isEqualTo("$otherName requested to join")
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -432,7 +432,7 @@ class DefaultRoomLastMessageFormatterTest {
|
||||
|
||||
val youAcceptedKnockEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent)
|
||||
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 someoneAcceptedKnock = formatter.format(someoneAcceptedKnockEvent, false)
|
||||
@@ -452,7 +452,7 @@ class DefaultRoomLastMessageFormatterTest {
|
||||
|
||||
val someoneRetractedKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
|
||||
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
|
||||
@@ -829,9 +829,13 @@ class DefaultRoomLastMessageFormatterTest {
|
||||
|
||||
// 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 profile = ProfileTimelineDetails.Ready(senderDisplayName, false, null)
|
||||
val profile = aProfileTimelineDetails(senderDisplayName)
|
||||
return anEventTimelineItem(
|
||||
content = content,
|
||||
senderProfile = profile,
|
||||
|
||||
@@ -27,7 +27,7 @@ data class NotificationData(
|
||||
val roomId: RoomId,
|
||||
// mxc url
|
||||
val senderAvatarUrl: String?,
|
||||
// private, must use `getSenderName`
|
||||
// private, must use `getDisambiguatedDisplayName`
|
||||
private val senderDisplayName: String?,
|
||||
private val senderIsNameAmbiguous: Boolean,
|
||||
val roomAvatarUrl: String?,
|
||||
@@ -39,7 +39,7 @@ data class NotificationData(
|
||||
val content: NotificationContent,
|
||||
val hasMention: Boolean,
|
||||
) {
|
||||
fun getSenderName(userId: UserId): String = when {
|
||||
fun getDisambiguatedDisplayName(userId: UserId): String = when {
|
||||
senderDisplayName.isNullOrBlank() -> userId.value
|
||||
senderIsNameAmbiguous -> "$senderDisplayName ($userId)"
|
||||
else -> senderDisplayName
|
||||
@@ -52,6 +52,7 @@ sealed interface NotificationContent {
|
||||
data class CallInvite(
|
||||
val senderId: UserId,
|
||||
) : MessageLike
|
||||
|
||||
data object CallHangup : MessageLike
|
||||
data object CallCandidates : MessageLike
|
||||
data object KeyVerificationReady : MessageLike
|
||||
|
||||
@@ -33,8 +33,7 @@ sealed interface InReplyTo {
|
||||
val eventId: EventId,
|
||||
val content: EventContent,
|
||||
val senderId: UserId,
|
||||
val senderDisplayName: String?,
|
||||
val senderAvatarUrl: String?,
|
||||
val senderProfile: ProfileTimelineDetails,
|
||||
) : InReplyTo
|
||||
|
||||
/**
|
||||
|
||||
@@ -52,3 +52,10 @@ fun ProfileTimelineDetails.getDisambiguatedDisplayName(userId: UserId): String {
|
||||
else -> userId.value
|
||||
}
|
||||
}
|
||||
|
||||
fun ProfileTimelineDetails.getAvatarUrl(): String? {
|
||||
return when (this) {
|
||||
is ProfileTimelineDetails.Ready -> avatarUrl
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ class NotificationDataTest {
|
||||
senderDisplayName = null,
|
||||
senderIsNameAmbiguous = false,
|
||||
)
|
||||
assertThat(sut.getSenderName(A_USER_ID)).isEqualTo("@alice:server.org")
|
||||
assertThat(sut.getDisambiguatedDisplayName(A_USER_ID)).isEqualTo("@alice:server.org")
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -38,7 +38,7 @@ class NotificationDataTest {
|
||||
senderDisplayName = "Alice",
|
||||
senderIsNameAmbiguous = false,
|
||||
)
|
||||
assertThat(sut.getSenderName(A_USER_ID)).isEqualTo("Alice")
|
||||
assertThat(sut.getDisambiguatedDisplayName(A_USER_ID)).isEqualTo("Alice")
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -47,7 +47,7 @@ class NotificationDataTest {
|
||||
senderDisplayName = "Alice",
|
||||
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(
|
||||
|
||||
@@ -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 org.matrix.rustcomponents.sdk.Message
|
||||
import org.matrix.rustcomponents.sdk.MessageType
|
||||
import org.matrix.rustcomponents.sdk.ProfileDetails
|
||||
import org.matrix.rustcomponents.sdk.RepliedToEventDetails
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import org.matrix.rustcomponents.sdk.FormattedBody as RustFormattedBody
|
||||
@@ -51,13 +50,11 @@ class EventMessageMapper {
|
||||
val inReplyToId = EventId(details.eventId)
|
||||
when (val event = details.event) {
|
||||
is RepliedToEventDetails.Ready -> {
|
||||
val senderProfile = event.senderProfile as? ProfileDetails.Ready
|
||||
InReplyTo.Ready(
|
||||
eventId = inReplyToId,
|
||||
content = timelineEventContentMapper.map(event.content),
|
||||
senderId = UserId(event.sender),
|
||||
senderDisplayName = senderProfile?.displayName,
|
||||
senderAvatarUrl = senderProfile?.avatarUrl,
|
||||
senderProfile = event.senderProfile.map(),
|
||||
)
|
||||
}
|
||||
is RepliedToEventDetails.Error -> InReplyTo.Error
|
||||
|
||||
@@ -93,8 +93,8 @@ class NotifiableEventResolver @Inject constructor(
|
||||
): NotifiableEvent? {
|
||||
return when (val content = this.content) {
|
||||
is NotificationContent.MessageLike.RoomMessage -> {
|
||||
val senderName = getSenderName(content.senderId)
|
||||
val messageBody = descriptionFromMessageContent(content, senderName)
|
||||
val senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId)
|
||||
val messageBody = descriptionFromMessageContent(content, senderDisambiguatedDisplayName)
|
||||
val notificationBody = if (hasMention) {
|
||||
stringProvider.getString(R.string.notification_mentioned_you_body, messageBody)
|
||||
} else {
|
||||
@@ -107,7 +107,7 @@ class NotifiableEventResolver @Inject constructor(
|
||||
eventId = eventId,
|
||||
noisy = isNoisy,
|
||||
timestamp = this.timestamp,
|
||||
senderName = senderName,
|
||||
senderDisambiguatedDisplayName = senderDisambiguatedDisplayName,
|
||||
body = notificationBody,
|
||||
imageUriString = fetchImageIfPresent(client)?.toString(),
|
||||
roomName = roomDisplayName,
|
||||
@@ -154,7 +154,7 @@ class NotifiableEventResolver @Inject constructor(
|
||||
eventId = eventId,
|
||||
noisy = isNoisy,
|
||||
timestamp = this.timestamp,
|
||||
senderName = null,
|
||||
senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId),
|
||||
body = stringProvider.getString(CommonStrings.common_call_invite),
|
||||
imageUriString = fetchImageIfPresent(client)?.toString(),
|
||||
roomName = roomDisplayName,
|
||||
@@ -180,7 +180,7 @@ class NotifiableEventResolver @Inject constructor(
|
||||
eventId = eventId,
|
||||
noisy = isNoisy,
|
||||
timestamp = this.timestamp,
|
||||
senderName = getSenderName(content.senderId),
|
||||
senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId),
|
||||
body = stringProvider.getString(CommonStrings.common_poll_summary, content.question),
|
||||
imageUriString = null,
|
||||
roomName = roomDisplayName,
|
||||
@@ -244,12 +244,12 @@ class NotifiableEventResolver @Inject constructor(
|
||||
|
||||
private fun descriptionFromMessageContent(
|
||||
content: NotificationContent.MessageLike.RoomMessage,
|
||||
senderDisplayName: String,
|
||||
senderDisambiguatedDisplayName: String,
|
||||
): String {
|
||||
return when (val messageType = content.messageType) {
|
||||
is AudioMessageType -> messageType.body
|
||||
is VoiceMessageType -> stringProvider.getString(CommonStrings.common_voice_message)
|
||||
is EmoteMessageType -> "* $senderDisplayName ${messageType.body}"
|
||||
is EmoteMessageType -> "* $senderDisambiguatedDisplayName ${messageType.body}"
|
||||
is FileMessageType -> messageType.body
|
||||
is ImageMessageType -> messageType.body
|
||||
is StickerMessageType -> messageType.body
|
||||
@@ -310,7 +310,7 @@ private fun buildNotifiableMessageEvent(
|
||||
canBeReplaced: Boolean = false,
|
||||
noisy: Boolean,
|
||||
timestamp: Long,
|
||||
senderName: String?,
|
||||
senderDisambiguatedDisplayName: String?,
|
||||
body: String?,
|
||||
// We cannot use Uri? type here, as that could trigger a
|
||||
// NotSerializableException when persisting this to storage
|
||||
@@ -335,7 +335,7 @@ private fun buildNotifiableMessageEvent(
|
||||
canBeReplaced = canBeReplaced,
|
||||
noisy = noisy,
|
||||
timestamp = timestamp,
|
||||
senderName = senderName,
|
||||
senderDisambiguatedDisplayName = senderDisambiguatedDisplayName,
|
||||
body = body,
|
||||
imageUriString = imageUriString,
|
||||
threadId = threadId,
|
||||
|
||||
@@ -45,7 +45,7 @@ class RoomGroupMessageCreator @Inject constructor(
|
||||
imageLoader: ImageLoader,
|
||||
): RoomNotification.Message {
|
||||
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 style = NotificationCompat.MessagingStyle(
|
||||
Person.Builder()
|
||||
@@ -60,9 +60,9 @@ class RoomGroupMessageCreator @Inject constructor(
|
||||
}
|
||||
|
||||
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 {
|
||||
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)
|
||||
@@ -108,7 +108,7 @@ class RoomGroupMessageCreator @Inject constructor(
|
||||
null
|
||||
} else {
|
||||
Person.Builder()
|
||||
.setName(event.senderName?.annotateForDebug(70))
|
||||
.setName(event.senderDisambiguatedDisplayName?.annotateForDebug(70))
|
||||
.setIcon(bitmapLoader.getUserIcon(event.senderAvatarPath, imageLoader))
|
||||
.setKey(event.senderId.value)
|
||||
.build()
|
||||
@@ -152,7 +152,7 @@ class RoomGroupMessageCreator @Inject constructor(
|
||||
private fun createFirstMessageSummaryLine(event: NotifiableMessageEvent, roomName: String, roomIsDirect: Boolean): CharSequence {
|
||||
return if (roomIsDirect) {
|
||||
buildSpannedString {
|
||||
event.senderName?.let {
|
||||
event.senderDisambiguatedDisplayName?.let {
|
||||
inSpans(StyleSpan(Typeface.BOLD)) {
|
||||
append(it)
|
||||
append(": ")
|
||||
@@ -165,7 +165,7 @@ class RoomGroupMessageCreator @Inject constructor(
|
||||
inSpans(StyleSpan(Typeface.BOLD)) {
|
||||
append(roomName)
|
||||
append(": ")
|
||||
event.senderName?.let {
|
||||
event.senderDisambiguatedDisplayName?.let {
|
||||
append(it)
|
||||
append(" ")
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ data class NotifiableMessageEvent(
|
||||
val senderId: UserId,
|
||||
val noisy: Boolean,
|
||||
val timestamp: Long,
|
||||
val senderName: String?,
|
||||
val senderDisambiguatedDisplayName: String?,
|
||||
val body: String?,
|
||||
// We cannot use Uri? type here, as that could trigger a
|
||||
// NotSerializableException when persisting this to storage
|
||||
@@ -55,7 +55,6 @@ data class NotifiableMessageEvent(
|
||||
) : NotifiableEvent {
|
||||
val type: String = EventType.MESSAGE
|
||||
override val description: String = body ?: ""
|
||||
val title: String = senderName ?: ""
|
||||
|
||||
// Example of value:
|
||||
// content://io.element.android.x.debug.notifications.fileprovider/downloads/temp/notif/matrix.org/XGItzSDOnSyXjYtOPfiKexDJ
|
||||
|
||||
@@ -462,7 +462,7 @@ class NotifiableEventResolverTest {
|
||||
senderId = A_USER_ID_2,
|
||||
noisy = false,
|
||||
timestamp = A_TIMESTAMP,
|
||||
senderName = null,
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
body = "Call in progress (unsupported)",
|
||||
imageUriString = null,
|
||||
threadId = null,
|
||||
@@ -586,7 +586,7 @@ class NotifiableEventResolverTest {
|
||||
senderId = A_USER_ID_2,
|
||||
noisy = false,
|
||||
timestamp = A_TIMESTAMP,
|
||||
senderName = "Bob",
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
body = body,
|
||||
imageUriString = null,
|
||||
threadId = null,
|
||||
|
||||
@@ -85,7 +85,7 @@ fun aNotifiableMessageEvent(
|
||||
editedEventId = null,
|
||||
noisy = false,
|
||||
timestamp = timestamp,
|
||||
senderName = "sender-name",
|
||||
senderDisambiguatedDisplayName = "sender-name",
|
||||
senderId = UserId("@sending-id:domain.com"),
|
||||
body = "message-body",
|
||||
roomId = roomId,
|
||||
|
||||
@@ -566,6 +566,8 @@ private fun ReplyToModeView(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clipToBounds(),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = ElementTheme.typography.fontBodySmMedium,
|
||||
textAlign = TextAlign.Start,
|
||||
color = ElementTheme.materialColors.primary,
|
||||
@@ -731,7 +733,7 @@ internal fun TextComposerReplyPreview() = ElementPreview {
|
||||
voiceMessageState = VoiceMessageState.Idle,
|
||||
composerMode = MessageComposerMode.Reply(
|
||||
isThreaded = true,
|
||||
senderName = "Alice",
|
||||
senderName = "Alice with a very long name to test overflow in the composer",
|
||||
eventId = EventId("$1234"),
|
||||
attachmentThumbnailInfo = null,
|
||||
defaultContent = "A message\n" +
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user