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:
commit
8637a97cac
407 changed files with 602 additions and 234 deletions
|
|
@ -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" +
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dfe460e8bab0a27a31984ce8c85ed98aef2a994584f724375cd436d5c1c7c6ca
|
||||
size 44758
|
||||
oid sha256:d0a65496766e211ffed310034d3575e5421a4ab1b72c41a09bb6ea17ff7b98e2
|
||||
size 47043
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:63b40ee6d9b4fa6f049dedbfc628380069360e67129b8e53b94e04d25b5897bf
|
||||
size 42898
|
||||
oid sha256:dd7e754bf281cc44e6b6e6175e18c66a1575bbf5f25f588f465af37b364b5d63
|
||||
size 45074
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f24d7e9c2ce18612ecc82ebbe28598ebdbe527a7ca6d3f4507a303d41da708c0
|
||||
size 6497
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7e91d1ffe38ca636208a1e093205893b460f2b7940892bfa641c1401d0f1365d
|
||||
size 8941
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8fc88d9c3c4ad18d26441e6e8da4f58b8d9e281dd7712adc31080c900dc13e53
|
||||
size 7101
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d73d668bdc2d792978dd36d4b366264f02f93843e6526ba7533992c77cb881a1
|
||||
size 6619
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b20874adf3b3cd1e369d11f631bfc6187dae44a816eccb7f6510217409554716
|
||||
size 8663
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:01b47b2ed0cbb29c8f302bbe5f7c24b9c1579ae92f558e7d5c8a579908f04b50
|
||||
size 7094
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ba7f63a6d8bda28570e062f62583b7c05bdea6dd477c1cc09ef4f8a2f4041193
|
||||
size 7125
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ed81c75139e4c049b0749e2ea89012f8e6e87e455b068ef15ee710444c860f4c
|
||||
size 9536
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dab27eb42b0634e4a3833c08846a4ca42fa687c35c381c768b2ddab68e1a31c8
|
||||
size 7728
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0162d5572e58c7dfb8697a55a1f9c988758c708173b35816907e569a36f0ab75
|
||||
size 6580
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:526004d2ac5d035bf945a097f2239430ce8395513a5d3b7f2ea74066d98a0785
|
||||
size 8938
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f639a20541b3be5de0508abcf70ef86472968b546d8926b8b91742b37637aece
|
||||
size 7247
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:83edcd9d31bd25873b0448f442322e0932744a409cf8582da0f90aaa5feac21e
|
||||
size 6468
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:731261ff2c90e668774218eb721e9fad88e2430ed71e355c218cc7f9af40b425
|
||||
size 8436
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9cf7fca11dc7f6f7e91cd5ff4012552812665eeebdfa146b416fa70f297123c9
|
||||
size 6837
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e357210874255c01ae10d17bad0a9b2ddf738f9d5d757ecfba8690280a2ec498
|
||||
size 6906
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:016935a98b882d4d21df4838e5ac5e679a5854ca9cbd73045b9e07cff6673b2b
|
||||
size 9319
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue