mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 12:07:28 +00:00
Merge remote-tracking branch 'origin/develop' into HEAD
This commit is contained in:
+2
-2
@@ -43,7 +43,7 @@ import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPr
|
||||
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.RoomMemberModerationEvent
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationRenderer
|
||||
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
||||
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
|
||||
@@ -348,7 +348,7 @@ class MessagesNode(
|
||||
onSelectAction = { action, target ->
|
||||
when (action) {
|
||||
is ModerationAction.DisplayProfile -> callback.navigateToRoomMemberDetails(target.userId)
|
||||
else -> state.roomMemberModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(action, target))
|
||||
else -> state.roomMemberModerationState.eventSink(RoomMemberModerationEvent.ProcessAction(action, target))
|
||||
}
|
||||
},
|
||||
onAvatarClick = { user ->
|
||||
|
||||
+10
-4
@@ -51,10 +51,11 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.captionOrNull
|
||||
import io.element.android.features.messages.impl.timeline.model.event.htmlCaptionOrNull
|
||||
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
|
||||
import io.element.android.features.messages.impl.voicemessages.composer.DefaultVoiceMessageComposerPresenter
|
||||
import io.element.android.features.roomcall.api.RoomCallState
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvent
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
|
||||
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
@@ -263,7 +264,7 @@ class MessagesPresenter(
|
||||
}
|
||||
}
|
||||
is MessagesEvent.OnUserClicked -> {
|
||||
roomMemberModerationState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(event.user))
|
||||
roomMemberModerationState.eventSink(RoomMemberModerationEvent.ShowActionsForUser(event.user))
|
||||
}
|
||||
MessagesEvent.StopLiveLocationShare -> {
|
||||
localCoroutineScope.launch {
|
||||
@@ -377,7 +378,7 @@ class MessagesPresenter(
|
||||
TimelineItemAction.Edit,
|
||||
TimelineItemAction.EditPoll -> handleActionEdit(targetEvent, composerState, enableTextFormatting)
|
||||
TimelineItemAction.AddCaption -> handleActionAddCaption(targetEvent, composerState)
|
||||
TimelineItemAction.EditCaption -> handleActionEditCaption(targetEvent, composerState)
|
||||
TimelineItemAction.EditCaption -> handleActionEditCaption(targetEvent, composerState, enableTextFormatting)
|
||||
TimelineItemAction.RemoveCaption -> handleRemoveCaption(targetEvent)
|
||||
TimelineItemAction.Reply -> handleActionReply(targetEvent, composerState, timelineProtectionState)
|
||||
TimelineItemAction.ReplyInThread -> {
|
||||
@@ -546,10 +547,15 @@ class MessagesPresenter(
|
||||
private suspend fun handleActionEditCaption(
|
||||
targetEvent: TimelineItem.Event,
|
||||
composerState: MessageComposerState,
|
||||
enableTextFormatting: Boolean,
|
||||
) {
|
||||
val composerMode = MessageComposerMode.EditCaption(
|
||||
eventOrTransactionId = targetEvent.eventOrTransactionId,
|
||||
content = targetEvent.content.captionOrNull().orEmpty(),
|
||||
content = if (enableTextFormatting) {
|
||||
targetEvent.content.htmlCaptionOrNull() ?: targetEvent.content.captionOrNull()
|
||||
} else {
|
||||
targetEvent.content.captionOrNull()
|
||||
}.orEmpty(),
|
||||
)
|
||||
composerState.eventSink(
|
||||
MessageComposerEvent.SetMode(composerMode)
|
||||
|
||||
+2
-2
@@ -38,7 +38,7 @@ import io.element.android.features.messages.impl.timeline.protection.TimelinePro
|
||||
import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState
|
||||
import io.element.android.features.roomcall.api.RoomCallState
|
||||
import io.element.android.features.roomcall.api.aStandByCallState
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvent
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationPermissions
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
@@ -168,7 +168,7 @@ fun aRoomMemberModerationState(
|
||||
permissions: RoomMemberModerationPermissions = RoomMemberModerationPermissions.DEFAULT,
|
||||
) = object : RoomMemberModerationState {
|
||||
override val permissions: RoomMemberModerationPermissions = permissions
|
||||
override val eventSink: (RoomMemberModerationEvents) -> Unit = {}
|
||||
override val eventSink: (RoomMemberModerationEvent) -> Unit = {}
|
||||
}
|
||||
|
||||
fun aUserEventPermissions(
|
||||
|
||||
+2
-2
@@ -46,7 +46,7 @@ import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPr
|
||||
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.RoomMemberModerationEvent
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationRenderer
|
||||
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
||||
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
|
||||
@@ -343,7 +343,7 @@ class ThreadedMessagesNode(
|
||||
onSelectAction = { action, target ->
|
||||
when (action) {
|
||||
is ModerationAction.DisplayProfile -> callback.navigateToRoomMemberDetails(target.userId)
|
||||
else -> state.roomMemberModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(action, target))
|
||||
else -> state.roomMemberModerationState.eventSink(RoomMemberModerationEvent.ProcessAction(action, target))
|
||||
}
|
||||
},
|
||||
onAvatarClick = { user ->
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* 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.threads.list
|
||||
|
||||
sealed interface ThreadsListEvent {
|
||||
data object Paginate : ThreadsListEvent
|
||||
}
|
||||
+2
-19
@@ -20,13 +20,10 @@ import io.element.android.features.messages.impl.utils.messagesummary.MessageSum
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||
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.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.room.threads.ThreadListPaginationStatus
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
@@ -104,9 +101,9 @@ class ThreadsListPresenter(
|
||||
derivedStateOf { roomInfo.heroes.map { it.getAvatarData(AvatarSize.CurrentUserTopBar) }.toImmutableList() }
|
||||
}
|
||||
|
||||
fun handleEvent(event: ThreadsListEvents) {
|
||||
fun handleEvent(event: ThreadsListEvent) {
|
||||
when (event) {
|
||||
ThreadsListEvents.Paginate -> if ((paginationStatus as? ThreadListPaginationStatus.Idle)?.hasMoreToLoad == true) {
|
||||
ThreadsListEvent.Paginate -> if ((paginationStatus as? ThreadListPaginationStatus.Idle)?.hasMoreToLoad == true) {
|
||||
coroutineScope.launch {
|
||||
Timber.d("Paginating thread list: $paginationStatus")
|
||||
threadsListService.paginate()
|
||||
@@ -129,17 +126,3 @@ class ThreadsListPresenter(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class ThreadsListState(
|
||||
val roomId: RoomId,
|
||||
val roomName: String,
|
||||
val roomAvatarUrl: String?,
|
||||
val isRoomTombstoned: Boolean,
|
||||
val heroes: ImmutableList<AvatarData>,
|
||||
val threads: ImmutableList<ThreadListRowItem>,
|
||||
val eventSink: (ThreadsListEvents) -> Unit,
|
||||
)
|
||||
|
||||
sealed interface ThreadsListEvents {
|
||||
data object Paginate : ThreadsListEvents
|
||||
}
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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.threads.list
|
||||
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class ThreadsListState(
|
||||
val roomId: RoomId,
|
||||
val roomName: String,
|
||||
val roomAvatarUrl: String?,
|
||||
val isRoomTombstoned: Boolean,
|
||||
val heroes: ImmutableList<AvatarData>,
|
||||
val threads: ImmutableList<ThreadListRowItem>,
|
||||
val eventSink: (ThreadsListEvent) -> Unit,
|
||||
)
|
||||
+1
-1
@@ -156,7 +156,7 @@ fun ThreadsListView(
|
||||
}
|
||||
|
||||
ScrollHelper(lazyListState) {
|
||||
state.eventSink(ThreadsListEvents.Paginate)
|
||||
state.eventSink(ThreadsListEvent.Paginate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
@@ -108,6 +108,7 @@ class TimelineItemContentMessageFactory(
|
||||
fileSize = messageType.info?.size ?: 0,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = formattedCaption,
|
||||
htmlCaption = messageType.formattedCaption?.body,
|
||||
isEdited = content.isEdited,
|
||||
mediaSource = messageType.source,
|
||||
thumbnailSource = messageType.info?.thumbnailSource,
|
||||
@@ -132,6 +133,7 @@ class TimelineItemContentMessageFactory(
|
||||
fileSize = messageType.info?.size ?: 0,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = formattedCaption,
|
||||
htmlCaption = messageType.formattedCaption?.body,
|
||||
isEdited = content.isEdited,
|
||||
mediaSource = messageType.source,
|
||||
thumbnailSource = messageType.info?.thumbnailSource,
|
||||
@@ -174,6 +176,7 @@ class TimelineItemContentMessageFactory(
|
||||
fileSize = messageType.info?.size ?: 0,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = formattedCaption,
|
||||
htmlCaption = messageType.formattedCaption?.body,
|
||||
isEdited = content.isEdited,
|
||||
thumbnailSource = messageType.info?.thumbnailSource,
|
||||
mediaSource = messageType.source,
|
||||
@@ -198,6 +201,7 @@ class TimelineItemContentMessageFactory(
|
||||
fileSize = messageType.info?.size ?: 0,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = formattedCaption,
|
||||
htmlCaption = messageType.formattedCaption?.body,
|
||||
isEdited = content.isEdited,
|
||||
mediaSource = messageType.source,
|
||||
duration = messageType.info?.duration ?: Duration.ZERO,
|
||||
@@ -216,6 +220,7 @@ class TimelineItemContentMessageFactory(
|
||||
fileSize = messageType.info?.size ?: 0,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = formattedCaption,
|
||||
htmlCaption = messageType.formattedCaption?.body,
|
||||
isEdited = content.isEdited,
|
||||
mediaSource = messageType.source,
|
||||
duration = messageType.info?.duration ?: Duration.ZERO,
|
||||
@@ -235,6 +240,7 @@ class TimelineItemContentMessageFactory(
|
||||
fileSize = messageType.info?.size ?: 0,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = formattedCaption,
|
||||
htmlCaption = messageType.formattedCaption?.body,
|
||||
isEdited = content.isEdited,
|
||||
thumbnailSource = messageType.info?.thumbnailSource,
|
||||
mediaSource = messageType.source,
|
||||
@@ -345,6 +351,7 @@ class TimelineItemContentMessageFactory(
|
||||
body = messageType.body,
|
||||
caption = messageType.body.trimEnd().takeIf { it.isNotEmpty() },
|
||||
formattedCaption = formattedCaption,
|
||||
htmlCaption = messageType.formatted?.body,
|
||||
isEdited = content.isEdited,
|
||||
items = galleryItems.toImmutableList(),
|
||||
)
|
||||
@@ -402,6 +409,7 @@ class TimelineItemContentMessageFactory(
|
||||
body = messageType.body,
|
||||
caption = messageType.body.trimEnd().takeIf { it.isNotEmpty() },
|
||||
formattedCaption = formattedCaption,
|
||||
htmlCaption = messageType.formatted?.body,
|
||||
isEdited = content.isEdited,
|
||||
attachments = attachments.toImmutableList(),
|
||||
)
|
||||
|
||||
+1
-1
@@ -91,7 +91,7 @@ internal fun MatrixTimelineItem.Event.canBeDisplayedInBubbleBlock(): Boolean {
|
||||
// Can be grouped
|
||||
is FailedToParseMessageLikeContent,
|
||||
is MessageContent,
|
||||
RedactedContent,
|
||||
is RedactedContent,
|
||||
is StickerContent,
|
||||
is PollContent,
|
||||
is UnableToDecryptContent,
|
||||
|
||||
+4
-2
@@ -12,6 +12,7 @@ import androidx.annotation.VisibleForTesting
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
@@ -99,7 +100,8 @@ internal const val MIN_REDACTED_RUN_SIZE = 3
|
||||
* Fold runs of [MIN_REDACTED_RUN_SIZE] or more consecutive deleted (redacted) messages into a single
|
||||
* [TimelineItem.GroupedEvents], so a long stretch of "Message removed" tiles shows as one expandable
|
||||
* "N removed messages" line, like element-web. Shorter runs, and anything that is not a redacted event
|
||||
* (day dividers included), are left untouched.
|
||||
* (day dividers included), are left untouched. A deleted message that still heads a thread is left
|
||||
* untouched too, so that its thread summary stays reachable.
|
||||
*
|
||||
* This runs as the final step of [TimelineItemGrouper.group], after the state and membership events
|
||||
* have been grouped. It is deliberately kept out of the normal grouping: a run needs a higher minimum
|
||||
@@ -134,7 +136,7 @@ internal fun List<TimelineItem>.collapseRedactedRuns(groupIds: MutableMap<String
|
||||
}
|
||||
|
||||
for (item in this) {
|
||||
if (item is TimelineItem.Event && item.content is TimelineItemRedactedContent) {
|
||||
if (item is TimelineItem.Event && item.content is TimelineItemRedactedContent && item.threadInfo !is TimelineItemThreadInfo.ThreadRoot) {
|
||||
run.add(item)
|
||||
} else {
|
||||
flushRun()
|
||||
|
||||
+1
@@ -14,6 +14,7 @@ data class TimelineItemAttachmentsContent(
|
||||
val body: String,
|
||||
val caption: String?,
|
||||
val formattedCaption: CharSequence?,
|
||||
val htmlCaption: String? = null,
|
||||
override val isEdited: Boolean,
|
||||
val attachments: ImmutableList<AttachmentItem>,
|
||||
) : TimelineItemEventContent, TimelineItemEventMutableContent {
|
||||
|
||||
+1
@@ -17,6 +17,7 @@ data class TimelineItemAudioContent(
|
||||
override val fileSize: Long?,
|
||||
override val caption: String?,
|
||||
override val formattedCaption: CharSequence?,
|
||||
override val htmlCaption: String? = null,
|
||||
override val isEdited: Boolean,
|
||||
val duration: Duration,
|
||||
override val mediaSource: MediaSource,
|
||||
|
||||
+10
@@ -33,6 +33,9 @@ sealed interface TimelineItemEventContentWithAttachment :
|
||||
val fileSize: Long?
|
||||
val caption: String?
|
||||
val formattedCaption: CharSequence?
|
||||
|
||||
/** The caption as it was written, in HTML, so that editing it can start from the formatted text. */
|
||||
val htmlCaption: String?
|
||||
val mediaSource: MediaSource
|
||||
val mimeType: String
|
||||
val formattedFileSize: String
|
||||
@@ -127,6 +130,13 @@ fun TimelineItemEventContent.formattedCaptionOrNull(): CharSequence? = when (thi
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun TimelineItemEventContent.htmlCaptionOrNull(): String? = when (this) {
|
||||
is TimelineItemEventContentWithAttachment -> htmlCaption
|
||||
is TimelineItemGalleryContent -> htmlCaption
|
||||
is TimelineItemAttachmentsContent -> htmlCaption
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun TimelineItemEventContentWithAttachment.duration(): Duration? {
|
||||
return when (this) {
|
||||
is TimelineItemAudioContent -> duration
|
||||
|
||||
+1
@@ -16,6 +16,7 @@ data class TimelineItemFileContent(
|
||||
override val fileSize: Long?,
|
||||
override val caption: String?,
|
||||
override val formattedCaption: CharSequence?,
|
||||
override val htmlCaption: String? = null,
|
||||
override val isEdited: Boolean,
|
||||
override val mediaSource: MediaSource,
|
||||
val thumbnailSource: MediaSource?,
|
||||
|
||||
+1
@@ -16,6 +16,7 @@ data class TimelineItemGalleryContent(
|
||||
val body: String,
|
||||
val caption: String?,
|
||||
val formattedCaption: CharSequence?,
|
||||
val htmlCaption: String? = null,
|
||||
override val isEdited: Boolean,
|
||||
val items: ImmutableList<GalleryItem>,
|
||||
) : TimelineItemEventContent, TimelineItemEventMutableContent {
|
||||
|
||||
+1
@@ -19,6 +19,7 @@ data class TimelineItemImageContent(
|
||||
override val fileSize: Long?,
|
||||
override val caption: String?,
|
||||
override val formattedCaption: CharSequence?,
|
||||
override val htmlCaption: String? = null,
|
||||
override val isEdited: Boolean,
|
||||
override val mediaSource: MediaSource,
|
||||
val thumbnailSource: MediaSource?,
|
||||
|
||||
+2
@@ -28,6 +28,7 @@ fun aTimelineItemImageContent(
|
||||
blurhash: String? = A_BLUR_HASH,
|
||||
filename: String = "A picture.jpg",
|
||||
caption: String? = null,
|
||||
htmlCaption: String? = null,
|
||||
width: Int? = null,
|
||||
height: Int? = 300,
|
||||
) = TimelineItemImageContent(
|
||||
@@ -35,6 +36,7 @@ fun aTimelineItemImageContent(
|
||||
fileSize = 4 * 1024 * 1024L,
|
||||
caption = caption,
|
||||
formattedCaption = null,
|
||||
htmlCaption = htmlCaption,
|
||||
isEdited = false,
|
||||
mediaSource = MediaSource(""),
|
||||
thumbnailSource = null,
|
||||
|
||||
+1
@@ -15,6 +15,7 @@ data class TimelineItemStickerContent(
|
||||
override val fileSize: Long?,
|
||||
override val caption: String?,
|
||||
override val formattedCaption: CharSequence?,
|
||||
override val htmlCaption: String? = null,
|
||||
override val isEdited: Boolean,
|
||||
override val mediaSource: MediaSource,
|
||||
val thumbnailSource: MediaSource?,
|
||||
|
||||
+1
@@ -16,6 +16,7 @@ data class TimelineItemVideoContent(
|
||||
override val fileSize: Long?,
|
||||
override val caption: String?,
|
||||
override val formattedCaption: CharSequence?,
|
||||
override val htmlCaption: String? = null,
|
||||
override val isEdited: Boolean,
|
||||
val duration: Duration,
|
||||
override val mediaSource: MediaSource,
|
||||
|
||||
+1
@@ -19,6 +19,7 @@ data class TimelineItemVoiceContent(
|
||||
override val fileSize: Long?,
|
||||
override val caption: String?,
|
||||
override val formattedCaption: CharSequence?,
|
||||
override val htmlCaption: String? = null,
|
||||
override val isEdited: Boolean,
|
||||
val duration: Duration,
|
||||
override val mediaSource: MediaSource,
|
||||
|
||||
+27
@@ -989,6 +989,33 @@ class MessagesPresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - handle action edit caption starts from the formatted caption`() = runTest {
|
||||
val messageEvent = aMessageEvent(
|
||||
content = aTimelineItemImageContent(
|
||||
caption = "Hello world",
|
||||
htmlCaption = "<b>Hello</b> world",
|
||||
)
|
||||
)
|
||||
val composerRecorder = EventsRecorder<MessageComposerEvent>()
|
||||
val presenter = createMessagesPresenter(
|
||||
messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder, showTextFormatting = true) },
|
||||
)
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MessagesEvent.HandleAction(TimelineItemAction.EditCaption, messageEvent))
|
||||
awaitItem()
|
||||
composerRecorder.assertSingle(
|
||||
MessageComposerEvent.SetMode(
|
||||
composerMode = MessageComposerMode.EditCaption(
|
||||
eventOrTransactionId = AN_EVENT_ID.toEventOrTransactionId(),
|
||||
content = "<b>Hello</b> world",
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - handle action add caption`() = runTest {
|
||||
val composerRecorder = EventsRecorder<MessageComposerEvent>()
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@ package io.element.android.features.messages.impl.threads
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.messages.impl.fixtures.aTimelineItemContentFactory
|
||||
import io.element.android.features.messages.impl.messagesummary.FakeMessageSummaryFormatter
|
||||
import io.element.android.features.messages.impl.threads.list.ThreadsListEvents
|
||||
import io.element.android.features.messages.impl.threads.list.ThreadsListEvent
|
||||
import io.element.android.features.messages.impl.threads.list.ThreadsListPresenter
|
||||
import io.element.android.features.messages.impl.threads.list.aThreadListItem
|
||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||
@@ -48,7 +48,7 @@ class ThreadsListPresenterTest {
|
||||
// Pagination is automatically triggered on start, so we should have one call to paginate already
|
||||
paginateRecorder.assertions().isCalledOnce()
|
||||
|
||||
initialItem.eventSink(ThreadsListEvents.Paginate)
|
||||
initialItem.eventSink(ThreadsListEvent.Paginate)
|
||||
|
||||
// Simulate a pagination result
|
||||
threadsListService.emit(listOf(aThreadListItem()))
|
||||
|
||||
+2
-2
@@ -51,7 +51,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.Receipt
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
|
||||
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
@@ -69,6 +68,7 @@ import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||
import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions
|
||||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||
import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
||||
import io.element.android.libraries.matrix.test.timeline.aRedactedContent
|
||||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||
import io.element.android.libraries.matrix.test.timeline.item.event.aRoomMembershipContent
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
@@ -880,7 +880,7 @@ class TimelinePresenterTest {
|
||||
(0 until 3).map { index ->
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = UniqueId("redacted_$index"),
|
||||
event = anEventTimelineItem(eventId = EventId("\$R$index"), content = RedactedContent),
|
||||
event = anEventTimelineItem(eventId = EventId("\$R$index"), content = aRedactedContent()),
|
||||
)
|
||||
}
|
||||
),
|
||||
|
||||
+79
@@ -12,12 +12,18 @@ import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.ThreadSummary
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType
|
||||
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
||||
import io.element.android.libraries.matrix.test.timeline.aRedactedContent
|
||||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
@@ -84,6 +90,42 @@ class TimelineItemsFactoryTest {
|
||||
).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `redacted message keeps its thread info`() = runTest {
|
||||
val factory = aTimelineItemsFactory(
|
||||
config = TimelineItemsFactoryConfig(
|
||||
computeReadReceipts = false,
|
||||
computeReactions = false,
|
||||
)
|
||||
)
|
||||
val items = listOf(
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = UniqueId("event-0"),
|
||||
event = anEventTimelineItem(
|
||||
sender = A_USER_ID,
|
||||
timestamp = 0L,
|
||||
content = aRedactedContent(
|
||||
threadInfo = EventThreadInfo.ThreadResponse(A_THREAD_ID),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
var threadInfo: TimelineItemThreadInfo? = null
|
||||
factory.timelineItems.test {
|
||||
factory.replaceWith(
|
||||
timelineItems = items,
|
||||
roomMembers = emptyList(),
|
||||
renderReadReceipts = false,
|
||||
)
|
||||
threadInfo = awaitItem()
|
||||
.filterIsInstance<TimelineItem.Event>()
|
||||
.firstOrNull()
|
||||
?.threadInfo
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
assertThat(threadInfo).isEqualTo(TimelineItemThreadInfo.ThreadResponse(A_THREAD_ID))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a key verification request is not rendered`() = runTest {
|
||||
val factory = aTimelineItemsFactory(
|
||||
@@ -128,6 +170,43 @@ class TimelineItemsFactoryTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a deleted message keeps the summary of the thread it heads`() = runTest {
|
||||
val factory = aTimelineItemsFactory(
|
||||
config = TimelineItemsFactoryConfig(
|
||||
computeReadReceipts = false,
|
||||
computeReactions = false,
|
||||
)
|
||||
)
|
||||
val items = listOf(
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = UniqueId("event-0"),
|
||||
event = anEventTimelineItem(
|
||||
sender = A_USER_ID,
|
||||
content = aRedactedContent(
|
||||
threadInfo = EventThreadInfo.ThreadRoot(
|
||||
summary = ThreadSummary(latestEvent = AsyncData.Uninitialized, numberOfReplies = 3),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
factory.timelineItems.test {
|
||||
factory.replaceWith(
|
||||
timelineItems = items,
|
||||
roomMembers = emptyList(),
|
||||
renderReadReceipts = false,
|
||||
)
|
||||
val threadInfo = awaitItem()
|
||||
.filterIsInstance<TimelineItem.Event>()
|
||||
.single()
|
||||
.threadInfo
|
||||
assertThat(threadInfo).isInstanceOf(TimelineItemThreadInfo.ThreadRoot::class.java)
|
||||
assertThat((threadInfo as TimelineItemThreadInfo.ThreadRoot).summary.numberOfReplies).isEqualTo(3)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun TestScope.groupPositionsOf(timestamps: List<Long>): List<TimelineItemGroupPosition> {
|
||||
val factory = aTimelineItemsFactory(
|
||||
config = TimelineItemsFactoryConfig(
|
||||
|
||||
+2
@@ -297,6 +297,7 @@ class TimelineItemContentMessageFactoryTest : RobolectricTest() {
|
||||
fileSize = 555L,
|
||||
caption = "body.mp4 caption",
|
||||
formattedCaption = SpannedString("formatted"),
|
||||
htmlCaption = "formatted",
|
||||
isEdited = true,
|
||||
duration = 1.minutes,
|
||||
mediaSource = MediaSource(url = "url", json = null),
|
||||
@@ -539,6 +540,7 @@ class TimelineItemContentMessageFactoryTest : RobolectricTest() {
|
||||
fileSize = 888L,
|
||||
caption = "body.jpg caption",
|
||||
formattedCaption = SpannedString("formatted"),
|
||||
htmlCaption = "formatted",
|
||||
isEdited = true,
|
||||
mediaSource = MediaSource(url = "url", json = null),
|
||||
thumbnailSource = MediaSource("url_thumbnail"),
|
||||
|
||||
+19
@@ -15,11 +15,14 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
||||
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
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.avatar.anAvatarData
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.ThreadSummary
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
@@ -190,6 +193,22 @@ class TimelineItemGrouperTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a deleted message that still heads a thread is left out of the removed messages group`() {
|
||||
val threadRoot = aGroupableItem.copy(
|
||||
id = UniqueId("r1"),
|
||||
content = TimelineItemRedactedContent,
|
||||
threadInfo = TimelineItemThreadInfo.ThreadRoot(
|
||||
summary = ThreadSummary(latestEvent = AsyncData.Uninitialized, numberOfReplies = 2),
|
||||
latestEventText = null,
|
||||
),
|
||||
)
|
||||
val r0 = aGroupableItem.copy(id = UniqueId("r0"), content = TimelineItemRedactedContent)
|
||||
val r2 = aGroupableItem.copy(id = UniqueId("r2"), content = TimelineItemRedactedContent)
|
||||
val result = sut.group(listOf(r0, threadRoot, r2))
|
||||
assertThat(result).isEqualTo(listOf(r0, threadRoot, r2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a run of fewer than three redacted events is left untouched`() {
|
||||
val r0 = aGroupableItem.copy(id = UniqueId("r0"), content = TimelineItemRedactedContent)
|
||||
|
||||
+2
-2
@@ -16,11 +16,11 @@ import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
||||
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.test.timeline.aRedactedContent
|
||||
import io.element.android.libraries.mediaplayer.api.MediaPlayer
|
||||
import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
@@ -88,7 +88,7 @@ fun aRedactedMatrixTimeline(eventId: EventId) = listOf<MatrixTimelineItem>(
|
||||
sender = A_USER_ID,
|
||||
senderProfile = ProfileDetails.Unavailable,
|
||||
timestamp = 9442,
|
||||
content = RedactedContent,
|
||||
content = aRedactedContent(),
|
||||
origin = null,
|
||||
timelineItemDebugInfoProvider = {
|
||||
TimelineItemDebugInfo(
|
||||
|
||||
Reference in New Issue
Block a user