Merge branch 'develop' into feature-oled-black

This commit is contained in:
Timur Gilfanov 2026-03-30 11:08:53 +04:00 committed by GitHub
commit d0dcbab750
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
1505 changed files with 14143 additions and 9545 deletions

View file

@ -28,10 +28,10 @@ import io.element.android.features.call.api.CallType
import io.element.android.features.call.api.ElementCallEntryPoint
import io.element.android.features.forward.api.ForwardEntryPoint
import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint
import io.element.android.features.location.api.Location
import io.element.android.features.location.api.LocationService
import io.element.android.features.location.api.SendLocationEntryPoint
import io.element.android.features.location.api.ShareLocationEntryPoint
import io.element.android.features.location.api.ShowLocationEntryPoint
import io.element.android.features.location.api.ShowLocationMode
import io.element.android.features.messages.api.MessagesEntryPoint
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewNode
@ -102,7 +102,7 @@ class MessagesFlowNode(
@Assisted plugins: List<Plugin>,
private val roomListService: RoomListService,
private val sessionId: SessionId,
private val sendLocationEntryPoint: SendLocationEntryPoint,
private val shareLocationEntryPoint: ShareLocationEntryPoint,
private val showLocationEntryPoint: ShowLocationEntryPoint,
private val createPollEntryPoint: CreatePollEntryPoint,
private val elementCallEntryPoint: ElementCallEntryPoint,
@ -148,7 +148,7 @@ class MessagesFlowNode(
data class AttachmentPreview(val timelineMode: Timeline.Mode, val attachment: Attachment, val inReplyToEventId: EventId?) : NavTarget
@Parcelize
data class LocationViewer(val location: Location, val description: String?) : NavTarget
data class LocationViewer(val mode: ShowLocationMode) : NavTarget
@Parcelize
data class EventDebugInfo(val eventId: EventId?, val debugInfo: TimelineItemDebugInfo) : NavTarget
@ -272,10 +272,11 @@ class MessagesFlowNode(
backstack.push(NavTarget.EditPoll(Timeline.Mode.Live, eventId))
}
override fun navigateToRoomCall(roomId: RoomId) {
override fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) {
val callType = CallType.RoomCall(
sessionId = sessionId,
roomId = roomId,
isAudioCall = isAudioCall
)
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
elementCallEntryPoint.startCall(callType)
@ -335,7 +336,7 @@ class MessagesFlowNode(
createNode<AttachmentsPreviewNode>(buildContext, listOf(inputs))
}
is NavTarget.LocationViewer -> {
val inputs = ShowLocationEntryPoint.Inputs(navTarget.location, navTarget.description)
val inputs = ShowLocationEntryPoint.Inputs(navTarget.mode)
showLocationEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
@ -373,7 +374,7 @@ class MessagesFlowNode(
createNode<ReportMessageNode>(buildContext, listOf(inputs))
}
is NavTarget.SendLocation -> {
sendLocationEntryPoint.createNode(
shareLocationEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
timelineMode = navTarget.timelineMode,
@ -488,10 +489,11 @@ class MessagesFlowNode(
backstack.push(NavTarget.EditPoll(Timeline.Mode.Thread(navTarget.threadRootId), eventId))
}
override fun navigateToRoomCall(roomId: RoomId) {
override fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) {
val callType = CallType.RoomCall(
sessionId = sessionId,
roomId = roomId,
isAudioCall = isAudioCall
)
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
elementCallEntryPoint.startCall(callType)
@ -556,9 +558,16 @@ class MessagesFlowNode(
)
}
is TimelineItemLocationContent -> {
NavTarget.LocationViewer(
val mode = ShowLocationMode.Static(
location = event.content.location,
description = event.content.description,
senderName = event.safeSenderName,
senderId = event.senderId,
senderAvatarUrl = event.senderAvatar.url,
timestamp = event.sentTimeMillis,
assetType = event.content.assetType,
)
NavTarget.LocationViewer(
mode = mode
).takeIf { locationService.isServiceAvailable() }
}
else -> null

View file

@ -125,7 +125,7 @@ class MessagesNode(
fun navigateToSendLocation()
fun navigateToCreatePoll()
fun navigateToEditPoll(eventId: EventId)
fun navigateToRoomCall(roomId: RoomId)
fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean)
fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?)
fun navigateToRoomDetails()
fun navigateToPinnedMessagesList()
@ -279,7 +279,9 @@ class MessagesNode(
},
onSendLocationClick = callback::navigateToSendLocation,
onCreatePollClick = callback::navigateToCreatePoll,
onJoinCallClick = { callback.navigateToRoomCall(room.roomId) },
onJoinCallClick = { isAudioCall ->
callback.navigateToRoomCall(room.roomId, isAudioCall)
},
onViewAllPinnedMessagesClick = callback::navigateToPinnedMessagesList,
modifier = modifier,
knockRequestsBannerView = {

View file

@ -130,7 +130,7 @@ fun MessagesView(
onLinkClick: (String, Boolean) -> Unit,
onSendLocationClick: () -> Unit,
onCreatePollClick: () -> Unit,
onJoinCallClick: () -> Unit,
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
onViewAllPinnedMessagesClick: () -> Unit,
modifier: Modifier = Modifier,
forceJumpToBottomVisibility: Boolean = false,
@ -423,7 +423,7 @@ private fun MessagesViewContent(
onMessageLongClick: (TimelineItem.Event) -> Unit,
onSendLocationClick: () -> Unit,
onCreatePollClick: () -> Unit,
onJoinCallClick: () -> Unit,
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
onViewAllPinnedMessagesClick: () -> Unit,
forceJumpToBottomVisibility: Boolean,
onSwipeToReply: (TimelineItem.Event) -> Unit,

View file

@ -115,7 +115,7 @@ private fun ViolationAlert(
},
submitText = stringResource(submitTextId),
onSubmitClick = onSubmitClick,
level = if (isCritical) ComposerAlertLevel.Critical else ComposerAlertLevel.Default,
level = if (isCritical) ComposerAlertLevel.Critical else ComposerAlertLevel.Info,
)
}

View file

@ -706,14 +706,14 @@ class MessageComposerPresenter(
val draft = createDraftFromState(markdownTextEditorState, richTextEditorState)
updateDraft(draft, isVolatile = true).join()
}
setText(newComposerMode.content, markdownTextEditorState, richTextEditorState)
setText(newComposerMode.content, markdownTextEditorState, richTextEditorState, requestFocus = true)
}
is MessageComposerMode.EditCaption -> {
if (currentComposerMode.isEditing.not()) {
val draft = createDraftFromState(markdownTextEditorState, richTextEditorState)
updateDraft(draft, isVolatile = true).join()
}
setText(newComposerMode.content, markdownTextEditorState, richTextEditorState)
setText(newComposerMode.content, markdownTextEditorState, richTextEditorState, requestFocus = true)
}
else -> {
// When coming from edit, just clear the composer as it'd be weird to reset a volatile draft in this scenario.

View file

@ -43,6 +43,9 @@ import io.element.android.features.messages.impl.timeline.TimelinePresenter
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.roommembermoderation.api.ModerationAction
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents
import io.element.android.features.roommembermoderation.api.RoomMemberModerationRenderer
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
import io.element.android.libraries.architecture.NodeInputs
@ -86,6 +89,7 @@ class ThreadedMessagesNode(
private val mediaPlayer: MediaPlayer,
private val permalinkParser: PermalinkParser,
private val appNavigationStateService: AppNavigationStateService,
private val roomMemberModerationRenderer: RoomMemberModerationRenderer,
) : Node(buildContext, plugins = plugins), MessagesNavigator {
data class Inputs(
val threadRootEventId: ThreadId,
@ -130,7 +134,7 @@ class ThreadedMessagesNode(
fun navigateToSendLocation()
fun navigateToCreatePoll()
fun navigateToEditPoll(eventId: EventId)
fun navigateToRoomCall(roomId: RoomId)
fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean)
fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?)
}
@ -281,12 +285,25 @@ class ThreadedMessagesNode(
},
onSendLocationClick = callback::navigateToSendLocation,
onCreatePollClick = callback::navigateToCreatePoll,
onJoinCallClick = { callback.navigateToRoomCall(room.roomId) },
onJoinCallClick = { isAudioCall ->
callback.navigateToRoomCall(room.roomId, isAudioCall)
},
onViewAllPinnedMessagesClick = {},
modifier = modifier,
knockRequestsBannerView = {},
)
roomMemberModerationRenderer.Render(
state = state.roomMemberModerationState,
onSelectAction = { action, target ->
when (action) {
is ModerationAction.DisplayProfile -> callback.navigateToRoomMemberDetails(target.userId)
else -> state.roomMemberModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(action, target))
}
},
modifier = Modifier,
)
var focusedEventId by rememberSaveable {
mutableStateOf(inputs.focusedEventId)
}

View file

@ -39,7 +39,7 @@ import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugIn
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
import io.element.android.libraries.matrix.ui.messages.reply.aProfileTimelineDetailsReady
import io.element.android.libraries.matrix.ui.messages.reply.aProfileDetailsReady
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@ -166,7 +166,7 @@ internal fun aTimelineItemEvent(
isMine = isMine,
isEditable = isEditable,
canBeRepliedTo = canBeRepliedTo,
senderProfile = aProfileTimelineDetailsReady(
senderProfile = aProfileDetailsReady(
displayName = senderDisplayName,
displayNameAmbiguous = displayNameAmbiguous,
),

View file

@ -100,7 +100,7 @@ fun TimelineView(
onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit,
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
onReadReceiptClick: (TimelineItem.Event) -> Unit,
onJoinCallClick: () -> Unit,
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
modifier: Modifier = Modifier,
lazyListState: LazyListState = rememberLazyListState(),
forceJumpToBottomVisibility: Boolean = false,

View file

@ -10,6 +10,7 @@ package io.element.android.features.messages.impl.timeline.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.size
@ -35,7 +36,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable
internal fun CallMenuItem(
roomCallState: RoomCallState,
onJoinCallClick: () -> Unit,
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
when (roomCallState) {
@ -52,7 +53,7 @@ internal fun CallMenuItem(
is RoomCallState.OnGoing -> {
OnGoingCallMenuItem(
roomCallState = roomCallState,
onJoinCallClick = onJoinCallClick,
onJoinCallClick = { onJoinCallClick(roomCallState.isAudioCall) },
modifier = modifier,
)
}
@ -62,18 +63,31 @@ internal fun CallMenuItem(
@Composable
private fun StandByCallMenuItem(
roomCallState: RoomCallState.StandBy,
onJoinCallClick: () -> Unit,
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
IconButton(
modifier = modifier,
onClick = onJoinCallClick,
enabled = roomCallState.canStartCall,
) {
Icon(
imageVector = CompoundIcons.VideoCallSolid(),
contentDescription = stringResource(CommonStrings.a11y_start_call),
)
Row(modifier = modifier) {
// Only show voice call in DMs
if (roomCallState.isDM) {
IconButton(
onClick = { onJoinCallClick(true) },
enabled = roomCallState.canStartCall,
) {
Icon(
imageVector = CompoundIcons.VoiceCallSolid(),
contentDescription = stringResource(CommonStrings.a11y_start_voice_call),
)
}
}
IconButton(
onClick = { onJoinCallClick(false) },
enabled = roomCallState.canStartCall,
) {
Icon(
imageVector = CompoundIcons.VideoCallSolid(),
contentDescription = stringResource(CommonStrings.a11y_start_call),
)
}
}
}
@ -96,7 +110,11 @@ private fun OnGoingCallMenuItem(
) {
Icon(
modifier = Modifier.size(20.dp),
imageVector = CompoundIcons.VideoCallSolid(),
imageVector = if (roomCallState.isAudioCall) {
CompoundIcons.VoiceCallSolid()
} else {
CompoundIcons.VideoCallSolid()
},
contentDescription = null
)
Spacer(Modifier.width(8.dp))

View file

@ -22,14 +22,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.graphics.layer.CompositingStrategy
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
@ -49,6 +47,7 @@ import io.element.android.libraries.designsystem.theme.messageFromMeBackground
import io.element.android.libraries.designsystem.theme.messageFromOtherBackground
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.utils.graphics.drawInLayer
import io.element.android.libraries.ui.utils.time.isTalkbackActive
private val BUBBLE_RADIUS = 12.dp
@ -78,32 +77,45 @@ fun MessageEventBubble(
.onKeyboardContextMenuAction(onLongClick)
}
val cutTopStart = state.cutTopStart
// Ignore state.isHighlighted for now, we need a design decision on it.
val backgroundBubbleColor = MessageEventBubbleDefaults.backgroundBubbleColor(state.isMine)
val bubbleShape = remember(state) { MessageEventBubbleDefaults.shape(state.cutTopStart, state.groupPosition, state.isMine) }
val radiusPx = (avatarRadius + SENDER_AVATAR_BORDER_WIDTH).toPx()
val yOffsetPx = -(NEGATIVE_MARGIN_FOR_BUBBLE + avatarRadius).toPx()
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
BoxWithConstraints(
modifier = modifier
.graphicsLayer {
shape = bubbleShape
clip = true
compositingStrategy = CompositingStrategy.Offscreen
}
.drawWithContent {
drawRect(backgroundBubbleColor)
drawContent()
if (state.cutTopStart) {
drawCircle(
color = Color.Black,
center = Offset(
x = if (isRtl) size.width else 0f,
y = yOffsetPx,
),
radius = radiusPx,
blendMode = BlendMode.Clear,
)
.drawWithCache {
// Calculate the outline of the background and cache it
val outline = bubbleShape.createOutline(size, layoutDirection, this)
onDrawWithContent {
// Draw the contents in a layer to be able to clip them with the same outline
// For some reason, doing this clipping outside a layer messes up with the touch events
drawInLayer(
composingStrategy = CompositingStrategy.Offscreen,
outline = outline,
clip = true,
) {
// Draw the background first, so that it's behind the content
drawRect(backgroundBubbleColor)
// Then draw the content on top of it
drawContent()
// And then clip the top start corner if needed to make room for the avatar
if (cutTopStart) {
drawCircle(
color = Color.Black,
center = Offset(
x = if (layoutDirection == LayoutDirection.Rtl) size.width else 0f,
y = yOffsetPx,
),
radius = radiusPx,
blendMode = BlendMode.Clear,
)
}
}
}
},
// Need to set the contentAlignment again (it's already set in TimelineItemEventRow), for the case

View file

@ -46,7 +46,7 @@ internal fun TimelineItemCallNotifyView(
event: TimelineItem.Event,
roomCallState: RoomCallState,
onLongClick: (TimelineItem.Event) -> Unit,
onJoinCallClick: () -> Unit,
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
modifier: Modifier = Modifier
) {
Row(

View file

@ -10,6 +10,7 @@ package io.element.android.features.messages.impl.timeline.components
import android.annotation.SuppressLint
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
@ -269,7 +270,9 @@ fun TimelineItemEventRow(
if (displayThreadSummaries && timelineMode !is Timeline.Mode.Thread && event.threadInfo is TimelineItemThreadInfo.ThreadRoot) {
ThreadSummaryView(
modifier = if (event.isMine) {
Modifier.align(Alignment.End).padding(end = 16.dp)
Modifier
.align(Alignment.End)
.padding(end = 16.dp)
} else {
if (timelineRoomInfo.isDm) Modifier else Modifier.padding(start = 16.dp)
}.padding(top = 2.dp),
@ -742,11 +745,17 @@ private fun MessageEventBubbleContent(
} else {
inReplyToModifier.clickable(onClick = inReplyToClick)
}
InReplyToView(
inReplyTo = inReplyTo,
hideImage = timelineProtectionState.hideMediaContent(inReplyTo.eventId()),
modifier = talkbackCompatModifier,
)
Box(
modifier = talkbackCompatModifier
.border(1.dp, ElementTheme.colors.borderInteractiveSecondary, RoundedCornerShape(6.dp))
.background(ElementTheme.colors.bgCanvasDefault, RoundedCornerShape(6.dp))
.padding(4.dp)
) {
InReplyToView(
inReplyTo = inReplyTo,
hideImage = timelineProtectionState.hideMediaContent(inReplyTo.eventId()),
)
}
}
if (inReplyToDetails != null) {
// Use SubComposeLayout only if necessary as it can have consequences on the performance.
@ -833,25 +842,28 @@ internal fun TimelineItemEventRowWithThreadSummaryPreview() = ElementPreview {
groupPosition = TimelineItemGroupPosition.First,
threadInfo = TimelineItemThreadInfo.ThreadRoot(
latestEventText = "This is the latest message in the thread",
summary = ThreadSummary(AsyncData.Success(
EmbeddedEventInfo(
eventOrTransactionId = EventOrTransactionId.Event(EventId("\$event-id")),
content = MessageContent(
body = "This is the latest message in the thread",
inReplyTo = null,
isEdited = false,
threadInfo = null,
type = TextMessageType("This is the latest message in the thread", null)
),
senderId = UserId("@user:id"),
senderProfile = ProfileDetails.Ready(
displayName = "Alice",
avatarUrl = null,
displayNameAmbiguous = false,
),
timestamp = 0L,
)
), numberOfReplies = 20L)
summary = ThreadSummary(
latestEvent = AsyncData.Success(
EmbeddedEventInfo(
eventOrTransactionId = EventOrTransactionId.Event(EventId("\$event-id")),
content = MessageContent(
body = "This is the latest message in the thread",
inReplyTo = null,
isEdited = false,
threadInfo = null,
type = TextMessageType("This is the latest message in the thread", null)
),
senderId = UserId("@user:id"),
senderProfile = ProfileDetails.Ready(
displayName = "Alice",
avatarUrl = null,
displayNameAmbiguous = false,
),
timestamp = 0L,
)
),
numberOfReplies = 20L,
)
)
),
displayThreadSummaries = true,

View file

@ -72,7 +72,7 @@ internal fun TimelineItemRow(
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
onReadReceiptClick: (TimelineItem.Event) -> Unit,
onSwipeToReply: (TimelineItem.Event) -> Unit,
onJoinCallClick: () -> Unit,
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
eventSink: (TimelineEvent.TimelineItemEvent) -> Unit,
modifier: Modifier = Modifier,
eventContentView: @Composable (TimelineItem.Event, Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit =

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.messages.impl.timeline.components.event
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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.theme.messageFromMeBackground
@Composable
internal fun ElementTimelineItemPreview(
content: @Composable BoxScope.() -> Unit,
) = ElementPreview {
Box(
modifier = Modifier
.background(ElementTheme.colors.messageFromMeBackground)
.padding(4.dp),
content = content,
)
}

View file

@ -9,46 +9,49 @@
package io.element.android.features.messages.impl.timeline.components.event
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
/**
* package-private, you should only use TimelineItemFileView and TimelineItemAudioView.
* https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=2019-8180
*/
@Composable
fun TimelineItemAttachmentView(
icon: ImageVector,
iconContentDescription: String?,
filename: String,
fileExtensionAndSize: String,
caption: String?,
onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit,
modifier: Modifier = Modifier,
icon: (@Composable () -> Unit) = {},
) {
Column(
modifier = modifier,
) {
TimelineItemAttachmentHeaderView(
icon = icon,
iconContentDescription = iconContentDescription,
filename = filename,
fileExtensionAndSize = fileExtensionAndSize,
hasCaption = caption != null,
onContentLayoutChange = onContentLayoutChange,
icon = icon,
)
if (caption != null) {
TimelineItemAttachmentCaptionView(
@ -62,28 +65,34 @@ fun TimelineItemAttachmentView(
@Composable
private fun TimelineItemAttachmentHeaderView(
icon: ImageVector,
iconContentDescription: String?,
filename: String,
fileExtensionAndSize: String,
hasCaption: Boolean,
onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit,
modifier: Modifier = Modifier,
icon: (@Composable () -> Unit),
) {
val iconSize = 32.dp
val iconSize = 36.dp
val spacing = 8.dp
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(spacing),
) {
Box(
modifier = Modifier
.size(iconSize)
.clip(CircleShape)
.background(ElementTheme.colors.bgCanvasDefault),
.background(ElementTheme.colors.bgCanvasDefault, RoundedCornerShape(4.dp)),
contentAlignment = Alignment.Center,
) {
icon()
Icon(
imageVector = icon,
contentDescription = iconContentDescription,
tint = ElementTheme.colors.iconPrimary,
modifier = Modifier.size(24.dp),
)
}
Spacer(Modifier.width(spacing))
Column {
Text(
text = filename,

View file

@ -8,19 +8,14 @@
package io.element.android.features.messages.impl.timeline.components.event
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewParameter
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.timeline.components.layout.ContentAvoidingLayoutData
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContentProvider
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
@Composable
fun TimelineItemAudioView(
@ -29,27 +24,20 @@ fun TimelineItemAudioView(
modifier: Modifier = Modifier,
) {
TimelineItemAttachmentView(
icon = CompoundIcons.Audio(),
iconContentDescription = null,
filename = content.filename,
fileExtensionAndSize = content.fileExtensionAndSize,
caption = content.caption,
onContentLayoutChange = onContentLayoutChange,
modifier = modifier,
icon = {
Icon(
imageVector = CompoundIcons.Audio(),
contentDescription = null,
tint = ElementTheme.colors.iconPrimary,
modifier = Modifier
.size(16.dp),
)
}
)
}
@PreviewsDayNight
@Composable
internal fun TimelineItemAudioViewPreview(@PreviewParameter(TimelineItemAudioContentProvider::class) content: TimelineItemAudioContent) =
ElementPreview {
ElementTimelineItemPreview {
TimelineItemAudioView(
content,
onContentLayoutChange = {},

View file

@ -8,23 +8,20 @@
package io.element.android.features.messages.impl.timeline.components.event
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
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.timeline.components.layout.ContentAvoidingLayoutData
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContentProvider
import io.element.android.libraries.designsystem.icons.CompoundDrawables
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.ui.strings.CommonStrings
/**
* https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=2019-6477&t=2yr7kvVEdtsP4p26-4
*/
@Composable
fun TimelineItemFileView(
content: TimelineItemFileContent,
@ -32,29 +29,23 @@ fun TimelineItemFileView(
modifier: Modifier = Modifier,
) {
TimelineItemAttachmentView(
icon = CompoundIcons.Attachment(),
iconContentDescription = stringResource(CommonStrings.common_file),
filename = content.filename,
fileExtensionAndSize = content.fileExtensionAndSize,
caption = content.caption,
onContentLayoutChange = onContentLayoutChange,
modifier = modifier,
icon = {
Icon(
resourceId = CompoundDrawables.ic_compound_attachment,
contentDescription = stringResource(CommonStrings.common_file),
tint = ElementTheme.colors.iconPrimary,
modifier = Modifier
.size(16.dp)
.rotate(-45f),
)
}
)
}
@PreviewsDayNight
@Composable
internal fun TimelineItemFileViewPreview(@PreviewParameter(TimelineItemFileContentProvider::class) content: TimelineItemFileContent) = ElementPreview {
TimelineItemFileView(
content,
onContentLayoutChange = {},
)
internal fun TimelineItemFileViewPreview(@PreviewParameter(TimelineItemFileContentProvider::class) content: TimelineItemFileContent) {
ElementTimelineItemPreview {
TimelineItemFileView(
content,
onContentLayoutChange = {},
)
}
}

View file

@ -8,10 +8,8 @@
package io.element.android.features.messages.impl.timeline.components.event
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -21,31 +19,22 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContentProvider
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
@Composable
fun TimelineItemLocationView(
content: TimelineItemLocationContent,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier.fillMaxWidth()) {
content.description?.let {
Text(
text = it,
modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp),
)
}
StaticMapView(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 188.dp),
lat = content.location.lat,
lon = content.location.lon,
zoom = 15.0,
contentDescription = content.body
)
}
StaticMapView(
modifier = modifier
.fillMaxWidth()
.heightIn(max = 188.dp),
pinVariant = content.pinVariant,
lat = content.location.lat,
lon = content.location.lon,
zoom = 15.0,
contentDescription = content.body
)
}
@PreviewsDayNight

View file

@ -9,8 +9,10 @@
package io.element.android.features.messages.impl.timeline.factories.event
import dev.zacsweers.metro.Inject
import io.element.android.features.location.api.Location
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRtcNotificationContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
import io.element.android.libraries.matrix.api.core.EventId
@ -22,6 +24,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventTimeline
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInviteContent
import io.element.android.libraries.matrix.api.timeline.item.event.LiveLocationContent
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
@ -70,10 +73,10 @@ class TimelineItemContentFactory(
is FailedToParseMessageLikeContent -> failedToParseMessageFactory.create(itemContent)
is FailedToParseStateContent -> failedToParseStateFactory.create(itemContent)
is MessageContent -> {
val senderDisambiguatedDisplayName = senderProfile.getDisambiguatedDisplayName(sender)
messageFactory.create(
senderId = sender,
senderProfile = senderProfile,
content = itemContent,
senderDisambiguatedDisplayName = senderDisambiguatedDisplayName,
eventId = eventId,
)
}
@ -96,6 +99,24 @@ class TimelineItemContentFactory(
is UnableToDecryptContent -> utdFactory.create(itemContent)
is CallNotifyContent -> TimelineItemRtcNotificationContent()
is UnknownContent -> TimelineItemUnknownContent
is LiveLocationContent -> {
val lastKnownLocation = itemContent.locations.mapNotNull { beacon ->
Location.fromGeoUri(beacon.geoUri)
}.lastOrNull()
if (lastKnownLocation != null) {
TimelineItemLocationContent(
body = itemContent.body.trimEnd(),
description = itemContent.description?.trimEnd(),
assetType = itemContent.assetType,
senderId = sender,
senderProfile = senderProfile,
location = lastKnownLocation,
mode = TimelineItemLocationContent.Mode.Live(isActive = itemContent.isLive)
)
} else {
TimelineItemUnknownContent
}
}
}
}
}

View file

@ -30,6 +30,7 @@ import io.element.android.libraries.androidutils.filesize.FileSizeFormatter
import io.element.android.libraries.androidutils.text.safeLinkify
import io.element.android.libraries.core.mimetype.MimeTypes
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.permalink.PermalinkParser
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
@ -39,10 +40,12 @@ import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessa
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
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.ProfileDetails
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
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.toHtmlDocument
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor
import kotlinx.collections.immutable.persistentListOf
@ -65,11 +68,13 @@ class TimelineItemContentMessageFactory(
) {
fun create(
content: MessageContent,
senderDisambiguatedDisplayName: String,
senderId: UserId,
senderProfile: ProfileDetails,
eventId: EventId?,
): TimelineItemEventContent {
return when (val messageType = content.type) {
is EmoteMessageType -> {
val senderDisambiguatedDisplayName = senderProfile.getDisambiguatedDisplayName(senderId)
val emoteBody = "* $senderDisambiguatedDisplayName ${messageType.body.trimEnd()}"
val dom = messageType.formatted?.toHtmlDocument(
permalinkParser = permalinkParser,
@ -135,8 +140,8 @@ class TimelineItemContentMessageFactory(
}
is LocationMessageType -> {
val location = Location.fromGeoUri(messageType.geoUri)
val body = messageType.body.trimEnd()
if (location == null) {
val body = messageType.body.trimEnd()
TimelineItemTextContent(
body = body,
htmlDocument = null,
@ -145,9 +150,13 @@ class TimelineItemContentMessageFactory(
)
} else {
TimelineItemLocationContent(
body = messageType.body.trimEnd(),
body = body,
location = location,
description = messageType.description
description = messageType.description,
senderId = senderId,
senderProfile = senderProfile,
assetType = messageType.assetType,
mode = TimelineItemLocationContent.Mode.Static
)
}
}

View file

@ -31,6 +31,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyCon
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInviteContent
import io.element.android.libraries.matrix.api.timeline.item.event.LiveLocationContent
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
@ -81,7 +82,8 @@ internal fun MatrixTimelineItem.Event.canBeDisplayedInBubbleBlock(): Boolean {
RedactedContent,
is StickerContent,
is PollContent,
is UnableToDecryptContent -> true
is UnableToDecryptContent,
is LiveLocationContent -> true
// Can't be grouped
is FailedToParseStateContent,
is ProfileChangeContent,

View file

@ -28,14 +28,14 @@ class TimelineItemEventContentProvider : PreviewParameterProvider<TimelineItemEv
aTimelineItemAudioContent("An even bigger bigger bigger bigger bigger bigger bigger sound name which doesn't fit .mp3"),
aTimelineItemVoiceContent(),
aTimelineItemLocationContent(),
aTimelineItemLocationContent("Location description"),
aTimelineItemPollContent(),
aTimelineItemNoticeContent(),
aTimelineItemRedactedContent(),
aTimelineItemTextContent(),
aTimelineItemUnknownContent(),
aTimelineItemTextContent().copy(isEdited = true),
aTimelineItemTextContent(body = AN_EMOJI_ONLY_TEXT)
aTimelineItemTextContent(body = AN_EMOJI_ONLY_TEXT),
aTimelineItemLocationContent(mode = TimelineItemLocationContent.Mode.Live(isActive = true)),
)
}

View file

@ -9,11 +9,53 @@
package io.element.android.features.messages.impl.timeline.model.event
import io.element.android.features.location.api.Location
import io.element.android.libraries.designsystem.components.PinVariant
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl
import io.element.android.libraries.matrix.api.timeline.item.event.getDisplayName
data class TimelineItemLocationContent(
val body: String,
val senderId: UserId,
val senderProfile: ProfileDetails,
val location: Location,
val description: String? = null,
val assetType: AssetType? = null,
val mode: Mode,
) : TimelineItemEventContent {
val pinVariant = when (mode) {
is Mode.Live -> {
if (mode.isActive) {
PinVariant.UserLocation(avatarData = senderAvatar(), isLive = true)
} else {
PinVariant.StaleLocation
}
}
Mode.Static -> {
when (assetType) {
AssetType.PIN -> PinVariant.PinnedLocation
AssetType.SENDER,
AssetType.UNKNOWN,
null -> PinVariant.UserLocation(avatarData = senderAvatar(), isLive = false)
}
}
}
private fun senderAvatar() = AvatarData(
senderId.value,
name = senderProfile.getDisplayName(),
url = senderProfile.getAvatarUrl(),
size = AvatarSize.LocationPin
)
sealed interface Mode {
data object Static : Mode
data class Live(val isActive: Boolean) : Mode
}
override val type: String = "TimelineItemLocationContent"
}

View file

@ -10,21 +10,32 @@ package io.element.android.features.messages.impl.timeline.model.event
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.location.api.Location
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
import io.element.android.libraries.matrix.ui.messages.reply.aProfileDetailsReady
open class TimelineItemLocationContentProvider : PreviewParameterProvider<TimelineItemLocationContent> {
override val values: Sequence<TimelineItemLocationContent>
get() = sequenceOf(
aTimelineItemLocationContent(),
aTimelineItemLocationContent("This is a description!"),
aTimelineItemLocationContent(mode = TimelineItemLocationContent.Mode.Live(isActive = true)),
aTimelineItemLocationContent(mode = TimelineItemLocationContent.Mode.Live(isActive = false)),
)
}
fun aTimelineItemLocationContent(description: String? = null) = TimelineItemLocationContent(
body = "User location geo:52.2445,0.7186;u=5000",
fun aTimelineItemLocationContent(
body: String = "",
senderId: UserId = UserId("@sender:matrix.org"),
senderProfile: ProfileDetails = aProfileDetailsReady(),
mode: TimelineItemLocationContent.Mode = TimelineItemLocationContent.Mode.Static,
) = TimelineItemLocationContent(
body = body,
location = Location(
lat = 52.2445,
lon = 0.7186,
accuracy = 5000f,
),
description = description,
senderId = senderId,
senderProfile = senderProfile,
mode = mode
)

View file

@ -30,5 +30,5 @@ data class TimelineItemStickerContent(
/* Stickers are supposed to be small images so
we allow using the mediaSource (unless the url is empty) */
val preferredMediaSource = if (mediaSource.url.isEmpty()) thumbnailSource else mediaSource
val preferredMediaSource = if (mediaSource.safeUrl.isEmpty()) thumbnailSource else mediaSource
}

View file

@ -66,7 +66,7 @@ internal fun MessagesViewTopBar(
dmUserIdentityState: IdentityState?,
sharedHistoryIcon: SharedHistoryIcon,
onRoomDetailsClick: () -> Unit,
onJoinCallClick: () -> Unit,
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {

View file

@ -35,7 +35,7 @@
<string name="screen_room_attachment_source_camera_video">"Natočit video"</string>
<string name="screen_room_attachment_source_files">"Příloha"</string>
<string name="screen_room_attachment_source_gallery">"Knihovna fotografií a videí"</string>
<string name="screen_room_attachment_source_location">"Poloha"</string>
<string name="screen_room_attachment_source_location">"Sdílejte polohu"</string>
<string name="screen_room_attachment_source_poll">"Hlasování"</string>
<string name="screen_room_attachment_text_formatting">"Formátování textu"</string>
<string name="screen_room_encrypted_history_banner">"Historie zpráv je momentálně v této místnosti nedostupná"</string>

View file

@ -35,7 +35,7 @@
<string name="screen_room_attachment_source_camera_video">"Optag video"</string>
<string name="screen_room_attachment_source_files">"Vedhæftning"</string>
<string name="screen_room_attachment_source_gallery">"Foto- og videobibliotek"</string>
<string name="screen_room_attachment_source_location">"Lokation"</string>
<string name="screen_room_attachment_source_location">"Del lokation"</string>
<string name="screen_room_attachment_source_poll">"Afstemning"</string>
<string name="screen_room_attachment_text_formatting">"Tekstformatering"</string>
<string name="screen_room_encrypted_history_banner">"Beskedhistorikken er i øjeblikket ikke tilgængelig."</string>

View file

@ -35,7 +35,7 @@
<string name="screen_room_attachment_source_camera_video">"Εγγραφή βίντεο"</string>
<string name="screen_room_attachment_source_files">"Επισύναψη"</string>
<string name="screen_room_attachment_source_gallery">"Βιβλιοθήκη Φωτογραφιών &amp; Βίντεο"</string>
<string name="screen_room_attachment_source_location">"Τοποθεσία"</string>
<string name="screen_room_attachment_source_location">"Κοινή χρήση τοποθεσίας"</string>
<string name="screen_room_attachment_source_poll">"Δημοσκόπηση"</string>
<string name="screen_room_attachment_text_formatting">"Μορφοποίηση Κειμένου"</string>
<string name="screen_room_encrypted_history_banner">"Το ιστορικό μηνυμάτων δεν είναι διαθέσιμο προς το παρόν."</string>

View file

@ -35,7 +35,7 @@
<string name="screen_room_attachment_source_camera_video">"Nauhoita video"</string>
<string name="screen_room_attachment_source_files">"Liite"</string>
<string name="screen_room_attachment_source_gallery">"Kuva- ja videokirjasto"</string>
<string name="screen_room_attachment_source_location">"Sijainti"</string>
<string name="screen_room_attachment_source_location">"Jaa sijainti"</string>
<string name="screen_room_attachment_source_poll">"Kysely"</string>
<string name="screen_room_attachment_text_formatting">"Tekstin muotoilu"</string>
<string name="screen_room_encrypted_history_banner">"Viestihistoria ei ole tällä hetkellä saatavilla"</string>

View file

@ -35,7 +35,7 @@
<string name="screen_room_attachment_source_camera_video">"Enregistrer une vidéo"</string>
<string name="screen_room_attachment_source_files">"Pièce jointe"</string>
<string name="screen_room_attachment_source_gallery">"Galerie Photo et Vidéo"</string>
<string name="screen_room_attachment_source_location">"Position"</string>
<string name="screen_room_attachment_source_location">"Partager votre position"</string>
<string name="screen_room_attachment_source_poll">"Sondage"</string>
<string name="screen_room_attachment_text_formatting">"Formatage du texte"</string>
<string name="screen_room_encrypted_history_banner">"Lhistorique des messages nest actuellement pas disponible dans ce salon"</string>

View file

@ -35,7 +35,7 @@
<string name="screen_room_attachment_source_camera_video">"Videó rögzítése"</string>
<string name="screen_room_attachment_source_files">"Melléklet"</string>
<string name="screen_room_attachment_source_gallery">"Fénykép- és videótár"</string>
<string name="screen_room_attachment_source_location">"Hely"</string>
<string name="screen_room_attachment_source_location">"Hely megosztása"</string>
<string name="screen_room_attachment_source_poll">"Szavazás"</string>
<string name="screen_room_attachment_text_formatting">"Szövegformázás"</string>
<string name="screen_room_encrypted_history_banner">"Az üzenetelőzmények jelenleg nem érhetők el."</string>

View file

@ -14,10 +14,17 @@
<string name="emoji_picker_category_objects">"Objek"</string>
<string name="emoji_picker_category_people">"Senyuman &amp; Orang"</string>
<string name="emoji_picker_category_places">"Wisata &amp; Tempat"</string>
<string name="emoji_picker_category_recent">"Emojis Sebelumnya"</string>
<string name="emoji_picker_category_symbols">"Simbol"</string>
<string name="screen_media_upload_preview_caption_warning">"Keterangan mungkin tidak terlihat oleh orang yang menggunakan aplikasi lama."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Ketuk untuk mengubah kualitas unggahan video"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Dokumen tidak dapat diunggah."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Gagal memproses media untuk diunggah, silakan coba lagi."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Gagal mengunggah media, silakan coba lagi."</string>
<string name="screen_media_upload_preview_error_too_large_message">"Ukuran file maksimum yang diizinkan adalah%1$s ."</string>
<string name="screen_media_upload_preview_error_too_large_title">"Ukuran file terlalu besar untuk diunggah."</string>
<string name="screen_media_upload_preview_optimize_image_quality_title">"Optimalkan kualitas gambar"</string>
<string name="screen_media_upload_preview_processing">"Memproses…"</string>
<string name="screen_report_content_block_user">"Blokir pengguna"</string>
<string name="screen_report_content_block_user_hint">"Centang jika Anda ingin menyembunyikan semua pesan saat ini dan yang akan datang dari pengguna ini"</string>
<string name="screen_report_content_explanation">"Pesan ini akan dilaporkan ke administrator homeserver Anda. Mereka tidak akan dapat membaca pesan terenkripsi apa pun."</string>

View file

@ -14,6 +14,7 @@
<string name="emoji_picker_category_objects">"사물"</string>
<string name="emoji_picker_category_people">"표정 &amp; 사람"</string>
<string name="emoji_picker_category_places">"여행 &amp; 장소"</string>
<string name="emoji_picker_category_recent">"최근 이모지"</string>
<string name="emoji_picker_category_symbols">"상징"</string>
<string name="screen_media_upload_preview_caption_warning">"캡션은 오래된 앱을 사용하는 사용자에게 표시되지 않을 수 있습니다."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"비디오 업로드 품질을 변경하려면 탭하세요"</string>
@ -22,6 +23,7 @@
<string name="screen_media_upload_preview_error_failed_sending">"미디어 파일 업로드에 실패했습니다. 다시 시도해 주세요."</string>
<string name="screen_media_upload_preview_error_too_large_message">"허용되는 최대 파일 크기는 %1$s 입니다."</string>
<string name="screen_media_upload_preview_error_too_large_title">"파일 크기가 너무 커서 업로드할 수 없습니다."</string>
<string name="screen_media_upload_preview_item_count">"전체 %2$d개 중 %1$d번째 파일"</string>
<string name="screen_media_upload_preview_optimize_image_quality_title">"이미지 품질 최적화"</string>
<string name="screen_media_upload_preview_processing">"처리 중…"</string>
<string name="screen_report_content_block_user">"사용자 차단하기"</string>
@ -33,7 +35,7 @@
<string name="screen_room_attachment_source_camera_video">"동영상 녹화"</string>
<string name="screen_room_attachment_source_files">"첨부 파일"</string>
<string name="screen_room_attachment_source_gallery">"사진 &amp; 동영상 라이브러리"</string>
<string name="screen_room_attachment_source_location">"위치"</string>
<string name="screen_room_attachment_source_location">"위치 공유"</string>
<string name="screen_room_attachment_source_poll">"투표"</string>
<string name="screen_room_attachment_text_formatting">"텍스트 서식"</string>
<string name="screen_room_encrypted_history_banner">"메시지 기록은 현재 사용할 수 없습니다."</string>

View file

@ -19,8 +19,20 @@
<string name="screen_room_attachment_source_camera_video">"Įrašyti vaizdo įrašą"</string>
<string name="screen_room_attachment_source_files">"Priedas"</string>
<string name="screen_room_attachment_source_gallery">"Nuotraukų ir vaizdo įrašų biblioteka"</string>
<string name="screen_room_attachment_source_location">"Bendrinti vietą"</string>
<string name="screen_room_encrypted_history_banner">"Šiuo metu žinučių istorija nepasiekiama."</string>
<string name="screen_room_invite_again_alert_message">"Ar norėtumėte juos pakviesti atgal?"</string>
<string name="screen_room_invite_again_alert_title">"Šiame pokalbyje esate vieni."</string>
<string name="screen_room_retry_send_menu_send_again_action">"Siųsti vėl"</string>
<string name="screen_room_retry_send_menu_title">"Jūsų žinutė nepavyko išsiųsti."</string>
<string name="screen_room_timeline_add_reaction">"Pridėti reakciją"</string>
<string name="screen_room_timeline_beginning_of_room">"Tai yra %1$s pradžia."</string>
<string name="screen_room_timeline_beginning_of_room_no_name">"Tai yra šio pokalbio pradžia."</string>
<string name="screen_room_timeline_less_reactions">"Rodyti mažiau"</string>
<string name="screen_room_timeline_message_copied">"Žinutė nukopijuota"</string>
<string name="screen_room_timeline_no_permission_to_post">"Neturite leidimą skelbti šiame kambaryje."</string>
<string name="screen_room_timeline_reactions_show_less">"Rodyti mažiau"</string>
<string name="screen_room_timeline_reactions_show_more">"Rodyti daugiau"</string>
<string name="screen_room_timeline_read_marker_title">"Naujų"</string>
<plurals name="screen_room_timeline_state_changes">
<item quantity="one">"%1$d kambario pakeitimas"</item>

View file

@ -35,7 +35,7 @@
<string name="screen_room_attachment_source_camera_video">"Ta opp video"</string>
<string name="screen_room_attachment_source_files">"Vedlegg"</string>
<string name="screen_room_attachment_source_gallery">"Foto- og videobibliotek"</string>
<string name="screen_room_attachment_source_location">"Posisjon"</string>
<string name="screen_room_attachment_source_location">"Del posisjon"</string>
<string name="screen_room_attachment_source_poll">"Avstemning"</string>
<string name="screen_room_attachment_text_formatting">"Tekstformatering"</string>
<string name="screen_room_encrypted_history_banner">"Meldingshistorikken er for øyeblikket ikke tilgjengelig."</string>

View file

@ -7,12 +7,12 @@
<string name="crypto_event_authenticity_unknown_device">"Зашифровано неизвестным или удаленным устройством."</string>
<string name="crypto_event_authenticity_unsigned_device">"Зашифровано устройством, не проверенным его владельцем."</string>
<string name="crypto_event_authenticity_unverified_identity">"Зашифровано непроверенным пользователем."</string>
<string name="emoji_picker_category_activity">"Деятельность"</string>
<string name="emoji_picker_category_activity">"Активности"</string>
<string name="emoji_picker_category_flags">"Флаги"</string>
<string name="emoji_picker_category_foods">"Еда и напитки"</string>
<string name="emoji_picker_category_nature">"Животные и природа"</string>
<string name="emoji_picker_category_objects">"Объекты"</string>
<string name="emoji_picker_category_people">"Улыбки и люди"</string>
<string name="emoji_picker_category_people">"Эмодзи и люди"</string>
<string name="emoji_picker_category_places">"Путешествия и места"</string>
<string name="emoji_picker_category_recent">"Недавние эмодзи"</string>
<string name="emoji_picker_category_symbols">"Символы"</string>
@ -23,19 +23,19 @@
<string name="screen_media_upload_preview_error_failed_sending">"Не удалось загрузить медиафайлы, попробуйте еще раз."</string>
<string name="screen_media_upload_preview_error_too_large_message">"Максимальный размер файла: %1$s."</string>
<string name="screen_media_upload_preview_error_too_large_title">"Файл слишком большой для загрузки."</string>
<string name="screen_media_upload_preview_item_count">"Элемент %1$d из %2$d"</string>
<string name="screen_media_upload_preview_item_count">"%1$d из %2$d"</string>
<string name="screen_media_upload_preview_optimize_image_quality_title">"Оптимизировать качество изображения"</string>
<string name="screen_media_upload_preview_processing">"Обработка…"</string>
<string name="screen_report_content_block_user">"Заблокировать пользователя"</string>
<string name="screen_report_content_block_user_hint">"Отметьте, хотите ли вы скрыть все текущие и будущие сообщения от этого пользователя"</string>
<string name="screen_report_content_explanation">"Это сообщение будет передано администратору вашего домашнего сервера. Они не смогут прочитать зашифрованные сообщения."</string>
<string name="screen_report_content_hint">"Причина, по которой вы пожаловались на этот контент"</string>
<string name="screen_report_content_hint">"Причина жалобы"</string>
<string name="screen_room_attachment_source_camera">"Камера"</string>
<string name="screen_room_attachment_source_camera_photo">"Сделать фото"</string>
<string name="screen_room_attachment_source_camera_video">"Записать видео"</string>
<string name="screen_room_attachment_source_files">"Вложение"</string>
<string name="screen_room_attachment_source_gallery">"Фото и видео"</string>
<string name="screen_room_attachment_source_location">"Местоположение"</string>
<string name="screen_room_attachment_source_location">"Поделиться местоположением"</string>
<string name="screen_room_attachment_source_poll">"Опрос"</string>
<string name="screen_room_attachment_text_formatting">"Форматирование текста"</string>
<string name="screen_room_encrypted_history_banner">"В настоящее время история сообщений недоступна в этой комнате."</string>
@ -46,13 +46,13 @@
<string name="screen_room_mentions_at_room_title">"Все"</string>
<string name="screen_room_retry_send_menu_send_again_action">"Отправить снова"</string>
<string name="screen_room_retry_send_menu_title">"Не удалось отправить ваше сообщение"</string>
<string name="screen_room_timeline_add_reaction">"Добавить эмодзи"</string>
<string name="screen_room_timeline_add_reaction">"Добавить реакцию"</string>
<string name="screen_room_timeline_beginning_of_room">"Это начало %1$s."</string>
<string name="screen_room_timeline_beginning_of_room_no_name">"Это начало разговора."</string>
<string name="screen_room_timeline_legacy_call">"Неподдерживаемый вызов. уточните, может ли звонящий использовать новое приложение Element X."</string>
<string name="screen_room_timeline_beginning_of_room_no_name">"Это начало беседы."</string>
<string name="screen_room_timeline_legacy_call">"Звонок не поддерживается. Собеседник должен использовать новое приложение Element X."</string>
<string name="screen_room_timeline_less_reactions">"Показать меньше"</string>
<string name="screen_room_timeline_message_copied">"Сообщение скопировано"</string>
<string name="screen_room_timeline_no_permission_to_post">"У вас нет разрешения публиковать сообщения в этой комнате"</string>
<string name="screen_room_timeline_no_permission_to_post">"Вы не можете писать сообщения в этой комнате"</string>
<plurals name="screen_room_timeline_reaction_a11y">
<item quantity="one">"%1$d участник отреагировал %2$s"</item>
<item quantity="few">"%1$d участника отреагировало %2$s"</item>
@ -63,11 +63,11 @@
<item quantity="few">"Вы и %1$d участника отреагировали %2$s"</item>
<item quantity="many">"Вы и %1$d участников отреагировали %2$s"</item>
</plurals>
<string name="screen_room_timeline_reaction_you_a11y">"Вы отреагировали %1$s"</string>
<string name="screen_room_timeline_reaction_you_a11y">"Вы отреагировали: %1$s"</string>
<string name="screen_room_timeline_reactions_show_less">"Показать меньше"</string>
<string name="screen_room_timeline_reactions_show_more">"Показать больше"</string>
<string name="screen_room_timeline_reactions_show_reactions_summary">"Показать сводку реакций"</string>
<string name="screen_room_timeline_read_marker_title">"Новый"</string>
<string name="screen_room_timeline_read_marker_title">"Новое"</string>
<plurals name="screen_room_timeline_state_changes">
<item quantity="one">"%1$d изменение в комнате"</item>
<item quantity="few">"%1$d изменения в комнате"</item>
@ -75,7 +75,7 @@
</plurals>
<string name="screen_room_timeline_tombstoned_room_action">"Перейти в новую комнату"</string>
<string name="screen_room_timeline_tombstoned_room_message">"Эта комната была заменена и больше не активна"</string>
<string name="screen_room_timeline_upgraded_room_action">"Посмотреть старые сообщения"</string>
<string name="screen_room_timeline_upgraded_room_action">росмотреть старые сообщения"</string>
<string name="screen_room_timeline_upgraded_room_message">"Эта комната является продолжением другой комнаты"</string>
<plurals name="screen_room_typing_many_members">
<item quantity="one">"%1$s, %2$s и %3$d"</item>
@ -83,9 +83,9 @@
<item quantity="many">"%1$s, %2$s и другие %3$d"</item>
</plurals>
<plurals name="screen_room_typing_notification">
<item quantity="one">"%1$s набирает сообщение"</item>
<item quantity="few">"%1$s набирают сообщения"</item>
<item quantity="many">"%1$s набирают сообщения"</item>
<item quantity="one">"%1$s печатает"</item>
<item quantity="few">"%1$s печатают"</item>
<item quantity="many">"%1$s печатают"</item>
</plurals>
<string name="screen_room_typing_two_members">"%1$s и %2$s"</string>
</resources>

View file

@ -14,6 +14,7 @@
<string name="emoji_picker_category_objects">"Об\'єкти"</string>
<string name="emoji_picker_category_people">"Смайлики та люди"</string>
<string name="emoji_picker_category_places">"Подорожі та місця"</string>
<string name="emoji_picker_category_recent">"Нещодавні емодзі"</string>
<string name="emoji_picker_category_symbols">"Символи"</string>
<string name="screen_media_upload_preview_caption_warning">"Користувачі старих застосунків можуть не бачити підписи."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Натисніть, щоб змінити якість вивантажуваного відео"</string>
@ -22,6 +23,7 @@
<string name="screen_media_upload_preview_error_failed_sending">"Не вдалося завантажити медіафайл, спробуйте ще раз."</string>
<string name="screen_media_upload_preview_error_too_large_message">"Максимально дозволений розмір файлу — %1$s."</string>
<string name="screen_media_upload_preview_error_too_large_title">"Файл завеликий для вивантаження"</string>
<string name="screen_media_upload_preview_item_count">"Елемент %1$d з %2$d"</string>
<string name="screen_media_upload_preview_optimize_image_quality_title">"Оптимізувати якість зображення"</string>
<string name="screen_media_upload_preview_processing">"Обробка…"</string>
<string name="screen_report_content_block_user">"Заблокувати користувача"</string>

View file

@ -35,7 +35,7 @@
<string name="screen_room_attachment_source_camera_video">"Record video"</string>
<string name="screen_room_attachment_source_files">"Attachment"</string>
<string name="screen_room_attachment_source_gallery">"Photo &amp; Video Library"</string>
<string name="screen_room_attachment_source_location">"Location"</string>
<string name="screen_room_attachment_source_location">"Share location"</string>
<string name="screen_room_attachment_source_poll">"Poll"</string>
<string name="screen_room_attachment_text_formatting">"Text Formatting"</string>
<string name="screen_room_encrypted_history_banner">"Message history is currently unavailable."</string>

View file

@ -17,7 +17,7 @@ import io.element.android.features.call.test.FakeElementCallEntryPoint
import io.element.android.features.forward.test.FakeForwardEntryPoint
import io.element.android.features.knockrequests.test.FakeKnockRequestsListEntryPoint
import io.element.android.features.location.test.FakeLocationService
import io.element.android.features.location.test.FakeSendLocationEntryPoint
import io.element.android.features.location.test.FakeShareLocationEntryPoint
import io.element.android.features.location.test.FakeShowLocationEntryPoint
import io.element.android.features.messages.api.MessagesEntryPoint
import io.element.android.features.messages.impl.pinned.banner.createPinnedEventsTimelineProvider
@ -62,7 +62,7 @@ class DefaultMessagesEntryPointTest {
plugins = plugins,
roomListService = FakeRoomListService(),
sessionId = A_SESSION_ID,
sendLocationEntryPoint = FakeSendLocationEntryPoint(),
shareLocationEntryPoint = FakeShareLocationEntryPoint(),
showLocationEntryPoint = FakeShowLocationEntryPoint(),
createPollEntryPoint = FakeCreatePollEntryPoint(),
elementCallEntryPoint = FakeElementCallEntryPoint(),

View file

@ -53,6 +53,7 @@ import io.element.android.features.messages.impl.timeline.components.receipt.aRe
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvent
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
import io.element.android.features.roomcall.api.aStandByCallState
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
@ -71,6 +72,7 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.assertNoNodeWithText
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.setSafeContent
import kotlinx.collections.immutable.persistentListOf
@ -122,7 +124,7 @@ class MessagesViewTest {
val state = aMessagesState(
eventSink = eventsRecorder
)
ensureCalledOnce { callback ->
ensureCalledOnceWithParam(false) { callback ->
rule.setMessagesView(
state = state,
onJoinCallClick = callback,
@ -132,6 +134,23 @@ class MessagesViewTest {
}
}
@Test
fun `clicking on join voice call invoke expected callback`() {
val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false)
val state = aMessagesState(
eventSink = eventsRecorder,
roomCallState = aStandByCallState(isDM = true)
)
ensureCalledOnceWithParam(true) { callback ->
rule.setMessagesView(
state = state,
onJoinCallClick = callback,
)
val joinVoiceCallContentDescription = rule.activity.getString(CommonStrings.a11y_start_voice_call)
rule.onNodeWithContentDescription(joinVoiceCallContentDescription).performClick()
}
}
@Test
fun `clicking on an Event invoke expected callback`() {
val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false)
@ -609,7 +628,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMessa
onLinkClick: (String, Boolean) -> Unit = EnsureNeverCalledWithTwoParams(),
onSendLocationClick: () -> Unit = EnsureNeverCalled(),
onCreatePollClick: () -> Unit = EnsureNeverCalled(),
onJoinCallClick: () -> Unit = EnsureNeverCalled(),
onJoinCallClick: (Boolean) -> Unit = EnsureNeverCalledWithParam(),
onViewAllPinnedMessagesClick: () -> Unit = EnsureNeverCalled(),
) {
setSafeContent {

View file

@ -31,7 +31,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.core.FakeSendHandle
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
import io.element.android.libraries.matrix.ui.messages.reply.aProfileTimelineDetailsReady
import io.element.android.libraries.matrix.ui.messages.reply.aProfileDetailsReady
import kotlinx.collections.immutable.toImmutableList
internal fun aMessageEvent(
@ -52,7 +52,7 @@ internal fun aMessageEvent(
eventId = eventId,
transactionId = transactionId,
senderId = A_USER_ID,
senderProfile = aProfileTimelineDetailsReady(displayName = A_USER_NAME),
senderProfile = aProfileDetailsReady(displayName = A_USER_NAME),
senderAvatar = AvatarData(A_USER_ID.value, A_USER_NAME, size = AvatarSize.TimelineSender),
content = content,
sentTime = "",

View file

@ -30,7 +30,6 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams
import io.element.android.tests.testutils.EventsRecorder
@ -39,6 +38,7 @@ import io.element.android.tests.testutils.setSafeContent
import io.element.android.wysiwyg.link.Link
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
@ -142,6 +142,10 @@ class TimelineViewTest {
eventsRecorder.assertSingle(TimelineEvent.HideShieldDialog)
}
@Ignore(
"performScrollToIndex in compose tests no longer sets LazyListState.isScrollInProgress to true, so the LoadMore event is not emitted." +
"This needs to be reworked to use a different approach to check the LoadMore event was emitted."
)
@Test
fun `scrolling near to the start of the loaded items triggers a pre-fetch`() {
val eventsRecorder = EventsRecorder<TimelineEvent>()
@ -186,7 +190,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setTimel
onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit = EnsureNeverCalledWithTwoParams(),
onMoreReactionsClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
onReadReceiptClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
onJoinCallClick: () -> Unit = EnsureNeverCalled(),
onJoinCallClick: (Boolean) -> Unit = EnsureNeverCalledWithParam(),
forceJumpToBottomVisibility: Boolean = false,
) {
setSafeContent(clearAndroidUiDispatcher = true) {

View file

@ -41,6 +41,7 @@ import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.media.ThumbnailInfo
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
@ -59,8 +60,10 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy
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.test.AN_EVENT_ID
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.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.test.timeline.aProfileDetails
import io.element.android.libraries.matrix.test.timeline.aStickerContent
import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH
import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation
@ -83,7 +86,8 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory()
val result = sut.create(
content = createMessageContent(type = OtherMessageType(msgType = "a_type", body = "body")),
senderDisambiguatedDisplayName = "Bob",
senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID,
)
val expected = TimelineItemTextContent(
@ -98,15 +102,21 @@ class TimelineItemContentMessageFactoryTest {
@Test
fun `test create LocationMessageType not null`() = runTest {
val sut = createTimelineItemContentMessageFactory()
val assetType = AssetType.SENDER
val result = sut.create(
content = createMessageContent(type = LocationMessageType("body", "geo:1,2", "description")),
senderDisambiguatedDisplayName = "Bob",
content = createMessageContent(type = LocationMessageType("body", "geo:1,2", "description", assetType)),
senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID,
)
val expected = TimelineItemLocationContent(
body = "body",
location = Location(lat = 1.0, lon = 2.0, accuracy = 0.0F),
location = Location(lat = 1.0, lon = 2.0, accuracy = null),
description = "description",
assetType = assetType,
mode = TimelineItemLocationContent.Mode.Static,
senderId = A_USER_ID,
senderProfile = aProfileDetails(),
)
assertThat(result).isEqualTo(expected)
}
@ -115,8 +125,9 @@ class TimelineItemContentMessageFactoryTest {
fun `test create LocationMessageType null`() = runTest {
val sut = createTimelineItemContentMessageFactory()
val result = sut.create(
content = createMessageContent(type = LocationMessageType("body", "", null)),
senderDisambiguatedDisplayName = "Bob",
content = createMessageContent(type = LocationMessageType("body", "", null, null)),
senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID,
)
val expected = TimelineItemTextContent(
@ -133,7 +144,8 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory()
val result = sut.create(
content = createMessageContent(type = TextMessageType("body", null)),
senderDisambiguatedDisplayName = "Bob",
senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID,
)
val expected = TimelineItemTextContent(
@ -150,7 +162,8 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory()
val result = sut.create(
content = createMessageContent(type = TextMessageType("https://www.example.org", null)),
senderDisambiguatedDisplayName = "Bob",
senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID,
) as TimelineItemTextContent
val expected = TimelineItemTextContent(
@ -197,7 +210,8 @@ class TimelineItemContentMessageFactoryTest {
formatted = FormattedBody(MessageFormat.HTML, expected.toString())
)
),
senderDisambiguatedDisplayName = "Bob",
senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID,
)
assertThat((result as TimelineItemTextContent).formattedBody).isEqualTo(expected)
@ -215,7 +229,8 @@ class TimelineItemContentMessageFactoryTest {
formatted = FormattedBody(MessageFormat.UNKNOWN, "formatted")
)
),
senderDisambiguatedDisplayName = "Bob",
senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID,
)
assertThat((result as TimelineItemTextContent).formattedBody).isEqualTo(SpannedString("body"))
@ -226,7 +241,8 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory()
val result = sut.create(
content = createMessageContent(type = VideoMessageType("filename", null, null, MediaSource("url"), null)),
senderDisambiguatedDisplayName = "Bob",
senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID,
)
val expected = TimelineItemVideoContent(
@ -279,7 +295,8 @@ class TimelineItemContentMessageFactoryTest {
),
isEdited = true,
),
senderDisambiguatedDisplayName = "Bob",
senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID,
)
val expected = TimelineItemVideoContent(
@ -309,7 +326,8 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory()
val result = sut.create(
content = createMessageContent(type = AudioMessageType("filename", null, null, MediaSource("url"), null)),
senderDisambiguatedDisplayName = "Bob",
senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID,
)
val expected = TimelineItemAudioContent(
@ -345,7 +363,8 @@ class TimelineItemContentMessageFactoryTest {
),
isEdited = true,
),
senderDisambiguatedDisplayName = "Bob",
senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID,
)
val expected = TimelineItemAudioContent(
@ -368,7 +387,8 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory()
val result = sut.create(
content = createMessageContent(type = VoiceMessageType("filename", null, null, MediaSource("url"), null, null)),
senderDisambiguatedDisplayName = "Bob",
senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID,
)
val expected = TimelineItemVoiceContent(
@ -410,7 +430,8 @@ class TimelineItemContentMessageFactoryTest {
),
isEdited = true,
),
senderDisambiguatedDisplayName = "Bob",
senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID,
)
val expected = TimelineItemVoiceContent(
@ -435,7 +456,8 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory()
val result = sut.create(
content = createMessageContent(type = ImageMessageType("filename", "body", null, MediaSource("url"), null)),
senderDisambiguatedDisplayName = "Bob",
senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID,
)
val expected = TimelineItemImageContent(
@ -515,7 +537,8 @@ class TimelineItemContentMessageFactoryTest {
),
isEdited = true,
),
senderDisambiguatedDisplayName = "Bob",
senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID,
)
val expected = TimelineItemImageContent(
@ -544,7 +567,8 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory()
val result = sut.create(
content = createMessageContent(type = FileMessageType("filename", null, null, MediaSource("url"), null)),
senderDisambiguatedDisplayName = "Bob",
senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID,
)
val expected = TimelineItemFileContent(
@ -586,7 +610,8 @@ class TimelineItemContentMessageFactoryTest {
),
isEdited = true,
),
senderDisambiguatedDisplayName = "Bob",
senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID,
)
val expected = TimelineItemFileContent(
@ -609,7 +634,8 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory()
val result = sut.create(
content = createMessageContent(type = NoticeMessageType("body", null)),
senderDisambiguatedDisplayName = "Bob",
senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID,
)
val expected = TimelineItemNoticeContent(
@ -631,7 +657,8 @@ class TimelineItemContentMessageFactoryTest {
formatted = FormattedBody(MessageFormat.HTML, "formatted")
)
),
senderDisambiguatedDisplayName = "Bob",
senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID,
)
(result as TimelineItemNoticeContent).formattedBody.assertSpannedEquals(SpannedString("formatted"))
@ -642,7 +669,8 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentMessageFactory()
val result = sut.create(
content = createMessageContent(type = EmoteMessageType("body", null)),
senderDisambiguatedDisplayName = "Bob",
senderId = A_USER_ID,
senderProfile = aProfileDetails("Bob"),
eventId = AN_EVENT_ID,
)
val expected = TimelineItemEmoteContent(
@ -664,7 +692,8 @@ class TimelineItemContentMessageFactoryTest {
formatted = FormattedBody(MessageFormat.HTML, "formatted")
)
),
senderDisambiguatedDisplayName = "Bob",
senderId = A_USER_ID,
senderProfile = aProfileDetails("Bob"),
eventId = AN_EVENT_ID,
)
@ -690,7 +719,8 @@ class TimelineItemContentMessageFactoryTest {
formatted = FormattedBody(MessageFormat.HTML, "Test <a href=\"https://www.example.org\">me@matrix.org</a>")
)
),
senderDisambiguatedDisplayName = "Bob",
senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID,
)
(result as TimelineItemTextContent).formattedBody.assertSpannedEquals(expectedSpanned)
@ -715,7 +745,8 @@ class TimelineItemContentMessageFactoryTest {
formatted = FormattedBody(MessageFormat.HTML, "Test https://www.example.org")
)
),
senderDisambiguatedDisplayName = "Bob",
senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID,
)
(result as TimelineItemTextContent).formattedBody.assertSpannedEquals(expectedSpanned)
@ -741,7 +772,8 @@ class TimelineItemContentMessageFactoryTest {
formatted = FormattedBody(MessageFormat.HTML, "Test https://www.example.org")
)
),
senderDisambiguatedDisplayName = "Bob",
senderId = A_USER_ID,
senderProfile = aProfileDetails(),
eventId = AN_EVENT_ID,
)

View file

@ -23,7 +23,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSen
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.core.FakeSendHandle
import io.element.android.libraries.matrix.ui.messages.reply.aProfileTimelineDetailsReady
import io.element.android.libraries.matrix.ui.messages.reply.aProfileDetailsReady
import kotlinx.collections.immutable.toImmutableList
import org.junit.Test
@ -34,7 +34,7 @@ class TimelineItemGrouperTest {
id = UniqueId("0"),
senderId = A_USER_ID,
senderAvatar = anAvatarData(),
senderProfile = aProfileTimelineDetailsReady(displayName = ""),
senderProfile = aProfileDetailsReady(displayName = ""),
content = TimelineItemStateEventContent(body = "a state event"),
reactionsState = aTimelineItemReactions(count = 0),
readReceiptState = TimelineItemReadReceipts(emptyList<ReadReceiptData>().toImmutableList()),