Merge branch 'develop' into feature/fga/pdf_renderer

This commit is contained in:
ganfra 2023-06-02 16:43:55 +02:00
commit 26adc55ea9
435 changed files with 6832 additions and 1041 deletions

View file

@ -40,6 +40,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.media.MediaSource
import kotlinx.collections.immutable.ImmutableList
import kotlinx.parcelize.Parcelize
@ -90,6 +91,10 @@ class MessagesFlowNode @AssistedInject constructor(
override fun onPreviewAttachments(attachments: ImmutableList<Attachment>) {
backstack.push(NavTarget.AttachmentPreview(attachments.first()))
}
override fun onUserDataClicked(userId: UserId) {
callback?.onUserDataClicked(userId)
}
}
createNode<MessagesNode>(buildContext, listOf(callback))
}

View file

@ -28,6 +28,7 @@ import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.collections.immutable.ImmutableList
@ContributesNode(RoomScope::class)
@ -43,6 +44,7 @@ class MessagesNode @AssistedInject constructor(
fun onRoomDetailsClicked()
fun onEventClicked(event: TimelineItem.Event)
fun onPreviewAttachments(attachments: ImmutableList<Attachment>)
fun onUserDataClicked(userId: UserId)
}
private fun onRoomDetailsClicked() {
@ -57,6 +59,10 @@ class MessagesNode @AssistedInject constructor(
callback?.onPreviewAttachments(attachments)
}
private fun onUserDataClicked(userId: UserId) {
callback?.onUserDataClicked(userId)
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
@ -66,6 +72,7 @@ class MessagesNode @AssistedInject constructor(
onRoomDetailsClicked = this::onRoomDetailsClicked,
onEventClicked = this::onEventClicked,
onPreviewAttachments = this::onPreviewAttachments,
onUserDataClicked = this::onUserDataClicked,
modifier = modifier,
)
}

View file

@ -85,6 +85,7 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.LogCompositions
import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.launch
import timber.log.Timber
@ -97,6 +98,7 @@ fun MessagesView(
onBackPressed: () -> Unit,
onRoomDetailsClicked: () -> Unit,
onEventClicked: (event: TimelineItem.Event) -> Unit,
onUserDataClicked: (UserId) -> Unit,
onPreviewAttachments: (ImmutableList<Attachment>) -> Unit,
modifier: Modifier = Modifier,
) {
@ -153,11 +155,6 @@ fun MessagesView(
}
}
fun onExpandGroupClick(event: TimelineItem.GroupedEvents) {
Timber.v("onExpandGroupClick= ${event.id}")
state.timelineState.eventSink(TimelineEvents.ToggleExpandGroup(event))
}
fun onActionSelected(action: TimelineItemAction, event: TimelineItem.Event) {
state.eventSink(MessagesEvents.HandleAction(action, event))
}
@ -208,7 +205,7 @@ fun MessagesView(
.consumeWindowInsets(padding),
onMessageClicked = ::onMessageClicked,
onMessageLongClicked = ::onMessageLongClicked,
onExpandGroupClick = ::onExpandGroupClick,
onUserDataClicked = onUserDataClicked,
)
},
snackbarHost = {
@ -246,8 +243,8 @@ fun MessagesViewContent(
state: MessagesState,
modifier: Modifier = Modifier,
onMessageClicked: (TimelineItem.Event) -> Unit = {},
onUserDataClicked: (UserId) -> Unit = {},
onMessageLongClicked: (TimelineItem.Event) -> Unit = {},
onExpandGroupClick: (TimelineItem.GroupedEvents) -> Unit = {},
) {
Column(
modifier = modifier
@ -262,7 +259,7 @@ fun MessagesViewContent(
modifier = Modifier.weight(1f),
onMessageClicked = onMessageClicked,
onMessageLongClicked = onMessageLongClicked,
onExpandGroupClick = onExpandGroupClick,
onUserDataClicked = onUserDataClicked,
)
}
MessageComposerView(
@ -362,6 +359,7 @@ private fun ContentToPreview(state: MessagesState) {
onBackPressed = {},
onRoomDetailsClicked = {},
onEventClicked = {},
onPreviewAttachments = {}
onPreviewAttachments = {},
onUserDataClicked = {},
)
}

View file

@ -16,11 +16,9 @@
package io.element.android.features.messages.impl.timeline
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.matrix.api.core.EventId
sealed interface TimelineEvents {
object LoadMore : TimelineEvents
data class SetHighlightedEvent(val eventId: EventId?) : TimelineEvents
data class ToggleExpandGroup(val event: TimelineItem.GroupedEvents) : TimelineEvents
}

View file

@ -20,19 +20,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@ -45,7 +40,6 @@ private const val backPaginationPageSize = 50
class TimelinePresenter @Inject constructor(
private val timelineItemsFactory: TimelineItemsFactory,
private val timelineItemGrouper: TimelineItemGrouper,
room: MatrixRoom,
) : Presenter<TimelineState> {
@ -57,7 +51,6 @@ class TimelinePresenter @Inject constructor(
val highlightedEventId: MutableState<EventId?> = rememberSaveable {
mutableStateOf(null)
}
val expandedGroups = remember { mutableStateMapOf<String, Boolean>() }
val timelineItems = timelineItemsFactory
.flow()
@ -71,9 +64,6 @@ class TimelinePresenter @Inject constructor(
when (event) {
TimelineEvents.LoadMore -> localCoroutineScope.loadMore(paginationState.value)
is TimelineEvents.SetHighlightedEvent -> highlightedEventId.value = event.eventId
is TimelineEvents.ToggleExpandGroup -> {
expandedGroups[event.event.identifier()] = expandedGroups[event.event.identifier()].orFalse().not()
}
}
}
@ -92,7 +82,7 @@ class TimelinePresenter @Inject constructor(
return TimelineState(
highlightedEventId = highlightedEventId.value,
paginationState = paginationState.value,
timelineItems = timelineItemGrouper.group(timelineItems.value, expandedGroups).toImmutableList(),
timelineItems = timelineItems.value,
eventSink = ::handleEvents
)
}

View file

@ -94,7 +94,7 @@ internal fun aTimelineItemEvent(
eventId: EventId = EventId("\$" + Random.nextInt().toString()),
isMine: Boolean = false,
content: TimelineItemEventContent = aTimelineItemTextContent(),
groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.First,
groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
sendState: EventSendState = EventSendState.Sent(eventId),
): TimelineItem.Event {
return TimelineItem.Event(

View file

@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
@ -47,8 +48,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -85,6 +88,7 @@ import io.element.android.libraries.designsystem.theme.LocalColors
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
@ -93,9 +97,9 @@ import kotlinx.coroutines.launch
fun TimelineView(
state: TimelineState,
modifier: Modifier = Modifier,
onUserDataClicked: (UserId) -> Unit = {},
onMessageClicked: (TimelineItem.Event) -> Unit = {},
onMessageLongClicked: (TimelineItem.Event) -> Unit = {},
onExpandGroupClick: (TimelineItem.GroupedEvents) -> Unit = {},
) {
fun onReachedLoadMore() {
@ -119,7 +123,7 @@ fun TimelineView(
highlightedItem = state.highlightedEventId?.value,
onClick = onMessageClicked,
onLongClick = onMessageLongClicked,
onExpandGroupClick = onExpandGroupClick,
onUserDataClick = onUserDataClicked,
)
if (index == state.timelineItems.lastIndex) {
onReachedLoadMore()
@ -139,9 +143,9 @@ fun TimelineView(
fun TimelineItemRow(
timelineItem: TimelineItem,
highlightedItem: String?,
onUserDataClick: (UserId) -> Unit,
onClick: (TimelineItem.Event) -> Unit,
onLongClick: (TimelineItem.Event) -> Unit,
onExpandGroupClick: (TimelineItem.GroupedEvents) -> Unit,
modifier: Modifier = Modifier
) {
when (timelineItem) {
@ -174,13 +178,16 @@ fun TimelineItemRow(
isHighlighted = highlightedItem == timelineItem.identifier(),
onClick = ::onClick,
onLongClick = ::onLongClick,
onUserDataClick = onUserDataClick,
modifier = modifier,
)
}
}
is TimelineItem.GroupedEvents -> {
val isExpanded = rememberSaveable(key = timelineItem.identifier()) { mutableStateOf(false) }
fun onExpandGroupClick() {
onExpandGroupClick(timelineItem)
isExpanded.value = !isExpanded.value
}
Column(modifier = modifier.animateContentSize()) {
@ -190,11 +197,11 @@ fun TimelineItemRow(
count = timelineItem.events.size,
timelineItem.events.size
),
isExpanded = timelineItem.expanded,
isHighlighted = !timelineItem.expanded && timelineItem.events.any { it.identifier() == highlightedItem },
isExpanded = isExpanded.value,
isHighlighted = !isExpanded.value && timelineItem.events.any { it.identifier() == highlightedItem },
onClick = ::onExpandGroupClick,
)
if (timelineItem.expanded) {
if (isExpanded.value) {
Column {
timelineItem.events.forEach { subGroupEvent ->
TimelineItemRow(
@ -202,7 +209,7 @@ fun TimelineItemRow(
highlightedItem = highlightedItem,
onClick = onClick,
onLongClick = onLongClick,
onExpandGroupClick = {}
onUserDataClick = onUserDataClick,
)
}
}
@ -230,15 +237,21 @@ fun TimelineItemEventRow(
isHighlighted: Boolean,
onClick: () -> Unit,
onLongClick: () -> Unit,
onUserDataClick: (UserId) -> Unit,
modifier: Modifier = Modifier
) {
val interactionSource = remember { MutableInteractionSource() }
fun onUserDataClicked() {
onUserDataClick(event.senderId)
}
val (parentAlignment, contentAlignment) = if (event.isMine) {
Pair(Alignment.CenterEnd, Alignment.End)
} else {
Pair(Alignment.CenterStart, Alignment.Start)
}
Box(
modifier = modifier
.fillMaxWidth()
@ -247,14 +260,17 @@ fun TimelineItemEventRow(
) {
Row {
if (!event.isMine) {
Spacer(modifier = Modifier.width(16.dp))
Spacer(modifier = Modifier.width(4.dp))
}
Column(horizontalAlignment = contentAlignment) {
if (event.showSenderInformation) {
MessageSenderInformation(
event.safeSenderName,
event.senderAvatar,
Modifier.zIndex(1f)
Modifier
.zIndex(1f)
.offset(y = 12.dp)
.clickable(onClick = ::onUserDataClicked)
)
}
val bubbleState = BubbleState(
@ -282,7 +298,7 @@ fun TimelineItemEventRow(
reactionsState = event.reactionsState,
modifier = Modifier
.zIndex(1f)
.offset(x = if (event.isMine) 0.dp else 20.dp, y = -(16.dp))
.offset(x = if (event.isMine) 0.dp else 20.dp, y = -(4.dp))
)
}
if (event.isMine) {

View file

@ -20,7 +20,6 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -35,13 +34,16 @@ import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState
import io.element.android.features.messages.impl.timeline.model.bubble.BubbleStateProvider
import io.element.android.libraries.core.extensions.to01
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.ElementTheme
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
private val BUBBLE_RADIUS = 16.dp
@ -84,10 +86,9 @@ fun MessageEventBubble(
fun Modifier.offsetForItem(): Modifier {
return if (state.isMine) {
// FIXME setting y offset to -12.dp can overlap a state event displayed above.
offset(y = -(12.dp))
this
} else {
offset(x = 20.dp, y = -(12.dp))
offset(x = 20.dp)
}
}
@ -130,7 +131,7 @@ internal fun MessageEventBubbleDarkPreview(@PreviewParameter(BubbleStateProvider
@Composable
private fun ContentToPreview(state: BubbleState) {
// Due to y offset, surround with a Box
// Due to position offset, surround with a Box
Box(
modifier = Modifier
.size(width = 240.dp, height = 64.dp)
@ -141,7 +142,18 @@ private fun ContentToPreview(state: BubbleState) {
state = state,
interactionSource = MutableInteractionSource(),
) {
Spacer(modifier = Modifier.size(width = 120.dp, height = 32.dp))
// Render the state as a text to better understand the previews
Box(
modifier = Modifier
.size(width = 120.dp, height = 32.dp)
.padding(horizontal = 12.dp, vertical = 6.dp),
contentAlignment = Alignment.Center,
) {
Text(
fontSize = 10.sp,
text = "${state.groupPosition.javaClass.simpleName} m:${state.isMine.to01()} h:${state.isHighlighted.to01()}"
)
}
}
}
}

View file

@ -25,13 +25,18 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Error
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.designsystem.ElementTextStyles
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.ElementTheme
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
@ -48,7 +53,10 @@ fun TimelineEventTimestampView(
val hasMessageSendingFailed = event.sendState is EventSendState.SendingFailed
val isMessageEdited = (event.content as? TimelineItemTextBasedContent)?.isEdited.orFalse()
val tint = if (hasMessageSendingFailed) ElementTheme.colors.textActionCritical else null
Row(modifier = modifier.clickable(onClick = onClick)) {
Row(
modifier = modifier.clickable(onClick = onClick),
verticalAlignment = Alignment.CenterVertically,
) {
if (isMessageEdited) {
Text(
stringResource(R.string.common_edited_suffix),
@ -68,3 +76,21 @@ fun TimelineEventTimestampView(
}
}
}
@Preview
@Composable
internal fun TimelineEventTimestampViewLightPreview(@PreviewParameter(TimelineItemEventForTimestampViewProvider::class) event: TimelineItem.Event) =
ElementPreviewLight { ContentToPreview(event) }
@Preview
@Composable
internal fun TimelineEventTimestampViewDarkPreview(@PreviewParameter(TimelineItemEventForTimestampViewProvider::class) event: TimelineItem.Event) =
ElementPreviewDark { ContentToPreview(event) }
@Composable
private fun ContentToPreview(event: TimelineItem.Event) {
TimelineEventTimestampView(
event = event,
onClick = {}
)
}

View file

@ -0,0 +1,39 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.timeline.components
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
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.libraries.matrix.api.timeline.item.event.EventSendState
class TimelineItemEventForTimestampViewProvider : PreviewParameterProvider<TimelineItem.Event> {
override val values: Sequence<TimelineItem.Event>
get() = sequenceOf(
aTimelineItemEvent(),
// Sending failed
aTimelineItemEvent().copy(sendState = EventSendState.SendingFailed("AN_ERROR")),
// Edited
aTimelineItemEvent().copy(content = aTimelineItemTextContent().copy(isEdited = true)),
// Sending failed + Edited (not sure this is possible IRL, but should be covered by test)
aTimelineItemEvent().copy(
sendState = EventSendState.SendingFailed("AN_ERROR"),
content = aTimelineItemTextContent().copy(isEdited = true),
),
)
}

View file

@ -21,9 +21,13 @@ import io.element.android.features.messages.impl.timeline.diff.CacheInvalidator
import io.element.android.features.messages.impl.timeline.diff.MatrixTimelineItemsDiffCallback
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemEventFactory
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory
import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -38,9 +42,10 @@ class TimelineItemsFactory @Inject constructor(
private val dispatchers: CoroutineDispatchers,
private val eventItemFactory: TimelineItemEventFactory,
private val virtualItemFactory: TimelineItemVirtualFactory,
private val timelineItemGrouper: TimelineItemGrouper,
) {
private val timelineItems = MutableStateFlow<List<TimelineItem>>(emptyList())
private val timelineItems = MutableStateFlow(emptyList<TimelineItem>().toImmutableList())
private val timelineItemsCache = arrayListOf<TimelineItem?>()
// Items from rust sdk, used for diffing
@ -49,7 +54,7 @@ class TimelineItemsFactory @Inject constructor(
private val lock = Mutex()
private val cacheInvalidator = CacheInvalidator(timelineItemsCache)
fun flow(): StateFlow<List<TimelineItem>> = timelineItems.asStateFlow()
fun flow(): StateFlow<ImmutableList<TimelineItem>> = timelineItems.asStateFlow()
suspend fun replaceWith(
timelineItems: List<MatrixTimelineItem>,
@ -72,7 +77,8 @@ class TimelineItemsFactory @Inject constructor(
newTimelineItemStates.add(cacheItem)
}
}
this.timelineItems.emit(newTimelineItemStates)
val result = timelineItemGrouper.group(newTimelineItemStates).toPersistentList()
this.timelineItems.emit(result)
}
private fun calculateAndApplyDiff(newTimelineItems: List<MatrixTimelineItem>) {

View file

@ -16,10 +16,12 @@
package io.element.android.features.messages.impl.timeline.factories.event
import io.element.android.features.messages.impl.timeline.groups.canBeDisplayedInBubbleBlock
import io.element.android.features.messages.impl.timeline.model.AggregatedReaction
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.TimelineItemReactions
import io.element.android.libraries.core.bool.orTrue
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.timeline.MatrixTimelineItem
@ -102,10 +104,39 @@ class TimelineItemEventFactory @Inject constructor(
val previousSender = prevTimelineItem?.event?.sender
val nextSender = nextTimelineItem?.event?.sender
val previousIsGroupable = prevTimelineItem?.canBeDisplayedInBubbleBlock().orTrue()
val nextIsGroupable = nextTimelineItem?.canBeDisplayedInBubbleBlock().orTrue()
return when {
previousSender != currentSender && nextSender == currentSender -> TimelineItemGroupPosition.First
previousSender == currentSender && nextSender == currentSender -> TimelineItemGroupPosition.Middle
previousSender == currentSender && nextSender != currentSender -> TimelineItemGroupPosition.Last
previousSender != currentSender && nextSender == currentSender -> {
if (nextIsGroupable) {
TimelineItemGroupPosition.First
} else {
TimelineItemGroupPosition.None
}
}
previousSender == currentSender && nextSender == currentSender -> {
if (previousIsGroupable) {
if (nextIsGroupable) {
TimelineItemGroupPosition.Middle
} else {
TimelineItemGroupPosition.Last
}
} else {
if (nextIsGroupable) {
TimelineItemGroupPosition.First
} else {
TimelineItemGroupPosition.None
}
}
}
previousSender == currentSender /* && nextSender != currentSender (== true) */ -> {
if (previousIsGroupable) {
TimelineItemGroupPosition.Last
} else {
TimelineItemGroupPosition.None
}
}
else -> TimelineItemGroupPosition.None
}
}

View file

@ -0,0 +1,80 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.timeline.groups
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemProfileChangeContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRoomMembershipContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateEventContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
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.MessageContent
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
/**
* Return true if the Event can be grouped in a collapse/expand block
* When [canBeGrouped] returns a value, [canBeDisplayedInBubbleBlock] MUST return the opposite value.
* Since the receiving type are not the same, the two functions exist.
*/
internal fun TimelineItem.Event.canBeGrouped(): Boolean {
return when (content) {
is TimelineItemTextBasedContent,
is TimelineItemEncryptedContent,
is TimelineItemImageContent,
is TimelineItemFileContent,
is TimelineItemVideoContent,
TimelineItemRedactedContent,
TimelineItemUnknownContent -> false
is TimelineItemProfileChangeContent,
is TimelineItemRoomMembershipContent,
is TimelineItemStateEventContent -> true
}
}
/**
* Return true if the Event can be grouped in a block of message bubbles.
* When [canBeDisplayedInBubbleBlock] returns a value, [canBeGrouped] MUST return the opposite value.
* Since the receiving type are not the same, the two functions exist.
*/
internal fun MatrixTimelineItem.Event.canBeDisplayedInBubbleBlock(): Boolean {
return when (event.content) {
is FailedToParseMessageLikeContent,
is MessageContent,
RedactedContent,
is StickerContent,
is UnableToDecryptContent -> true
is FailedToParseStateContent,
is ProfileChangeContent,
is RoomMembershipContent,
UnknownContent,
is StateContent -> false
}
}

View file

@ -17,28 +17,14 @@
package io.element.android.features.messages.impl.timeline.groups
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEmoteContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemNoticeContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemProfileChangeContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRoomMembershipContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateEventContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
import io.element.android.libraries.core.bool.orFalse
import kotlinx.collections.immutable.toImmutableList
import javax.inject.Inject
class TimelineItemGrouper @Inject constructor() {
/**
* Create a new list of [TimelineItem] by grouping some of them into [TimelineItem.GroupedEvents].
*/
fun group(from: List<TimelineItem>, expandedGroups: Map<String, Boolean>): List<TimelineItem> {
fun group(from: List<TimelineItem>): List<TimelineItem> {
val result = mutableListOf<TimelineItem>()
val currentGroup = mutableListOf<TimelineItem.Event>()
from.forEach { timelineItem ->
@ -48,42 +34,24 @@ class TimelineItemGrouper @Inject constructor() {
// timelineItem cannot be grouped
if (currentGroup.isNotEmpty()) {
// There is a pending group, create a TimelineItem.GroupedEvents if there is more than 1 Event in the pending group.
result.addGroup(currentGroup, expandedGroups)
result.addGroup(currentGroup)
currentGroup.clear()
}
result.add(timelineItem)
}
}
if (currentGroup.isNotEmpty()) {
result.addGroup(currentGroup, expandedGroups)
result.addGroup(currentGroup)
}
return result
}
private fun TimelineItem.Event.canBeGrouped(): Boolean {
return when (content) {
is TimelineItemEncryptedContent,
is TimelineItemImageContent,
TimelineItemRedactedContent,
is TimelineItemEmoteContent,
is TimelineItemNoticeContent,
is TimelineItemTextContent,
is TimelineItemFileContent,
is TimelineItemVideoContent,
TimelineItemUnknownContent -> false
is TimelineItemProfileChangeContent,
is TimelineItemRoomMembershipContent,
is TimelineItemStateEventContent -> true
}
}
}
/**
* Will add a group if there is more than 1 item, else add the item to the list.
*/
private fun MutableList<TimelineItem>.addGroup(
group: MutableList<TimelineItem.Event>,
expandedGroups: Map<String, Boolean>,
group: MutableList<TimelineItem.Event>
) {
if (group.size == 1) {
// Do not create a group with just 1 item, just add the item to the result
@ -91,7 +59,6 @@ private fun MutableList<TimelineItem>.addGroup(
} else {
add(
TimelineItem.GroupedEvents(
expanded = expandedGroups[group.first().id + "_group"].orFalse(),
events = group.toImmutableList()
)
)

View file

@ -68,10 +68,9 @@ sealed interface TimelineItem {
@Immutable
data class GroupedEvents(
val expanded: Boolean,
val events: ImmutableList<Event>,
) : TimelineItem {
// use first id with a suffix
val id = events.first().id + "_group"
// use last id with a suffix. Last will not change in cas of new event from backpagination.
val id = events.last().id + "_group"
}
}

View file

@ -18,13 +18,48 @@ package io.element.android.features.messages.impl.timeline.model
import androidx.compose.runtime.Immutable
/**
* Attribute for a TimelineItem, used to render successive events from the same sender differently.
*
* Possible sequences in the timeline will be:
*
* Only one Event:
* - [None]
*
* Two Events
* - [First]
* - [Last]
*
* Many Events:
* - [First]
* - [Middle] (repeated if necessary)
* - [Last]
*/
@Immutable
sealed interface TimelineItemGroupPosition {
/**
* The event is part of a group of events from the same sender and is the first sent Event.
*/
object First : TimelineItemGroupPosition
/**
* The event is part of a group of events from the same sender and is neither the first nor the last sent Event.
*/
object Middle : TimelineItemGroupPosition
/**
* The event is part of a group of events from the same sender and is the last sent Event.
*/
object Last : TimelineItemGroupPosition
/**
* The event is not part of a group of events. Sender of previous event is different, and sender of next event is different.
*/
object None : TimelineItemGroupPosition
/**
* Return true if the previous sender of the event is a different sender.
*/
fun isNew(): Boolean = when (this) {
First, None -> true
else -> false

View file

@ -25,6 +25,7 @@ open class BubbleStateProvider : PreviewParameterProvider<BubbleState> {
TimelineItemGroupPosition.First,
TimelineItemGroupPosition.Middle,
TimelineItemGroupPosition.Last,
TimelineItemGroupPosition.None,
).map { groupPosition ->
sequenceOf(false, true).map { isMine ->
sequenceOf(false, true).map { isHighlighted ->

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_room_attachment_source_camera">"Fotoaparát"</string>
<string name="screen_room_attachment_source_camera_photo">"Vyfotit"</string>
<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>
</resources>

View file

@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="room_timeline_state_changes">
<item quantity="one">"%1$d Raumänderung"</item>
<item quantity="other">"%1$d Raumänderungen"</item>
</plurals>
<string name="screen_room_attachment_source_camera">"Kamera"</string>
<string name="screen_room_attachment_source_camera_photo">"Foto aufnehmen"</string>
<string name="screen_room_attachment_source_camera_video">"Video aufnehmen"</string>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="room_timeline_state_changes">
<item quantity="one">"%1$d changement dans la conversation"</item>
<item quantity="other">"%1$d changements dans la conversation"</item>
</plurals>
<string name="screen_room_attachment_source_camera_photo">"Prendre une photo"</string>
</resources>

View file

@ -5,4 +5,10 @@
<item quantity="few">"%1$d schimbări ale camerei"</item>
<item quantity="other">"%1$d schimbări ale camerei"</item>
</plurals>
<string name="screen_room_attachment_source_camera">"Cameră foto"</string>
<string name="screen_room_attachment_source_camera_photo">"Faceți o fotografie"</string>
<string name="screen_room_attachment_source_camera_video">"Înregistrați un videoclip"</string>
<string name="screen_room_attachment_source_files">"Atașament"</string>
<string name="screen_room_attachment_source_gallery">"Bibliotecă foto și video"</string>
<string name="screen_room_error_failed_processing_media">"Procesarea datelor media a eșuat, vă rugăm să încercați din nou."</string>
</resources>

View file

@ -16,6 +16,7 @@
package io.element.android.features.messages
import android.net.Uri
import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
@ -28,7 +29,6 @@ import io.element.android.features.messages.impl.actionlist.ActionListPresenter
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
import io.element.android.features.messages.impl.timeline.TimelinePresenter
import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper
import io.element.android.features.messages.media.FakeLocalMediaFactory
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
@ -41,11 +41,15 @@ import io.element.android.libraries.mediapickers.test.FakePickerProvider
import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
import io.element.android.libraries.textcomposer.MessageComposerMode
import io.mockk.mockk
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
class MessagesPresenterTest {
private val mockMediaUrl: Uri = mockk("localMediaUri")
@Test
fun `present - initial state`() = runTest {
val presenter = createMessagePresenter()
@ -136,13 +140,12 @@ class MessagesPresenterTest {
room = matrixRoom,
mediaPickerProvider = FakePickerProvider(),
featureFlagService = FakeFeatureFlagService(),
localMediaFactory = FakeLocalMediaFactory(),
localMediaFactory = FakeLocalMediaFactory(mockMediaUrl),
mediaSender = MediaSender(FakeMediaPreProcessor(),matrixRoom),
snackbarDispatcher = SnackbarDispatcher(),
)
val timelinePresenter = TimelinePresenter(
timelineItemsFactory = aTimelineItemsFactory(),
timelineItemGrouper = TimelineItemGrouper(),
room = matrixRoom,
)
val actionListPresenter = ActionListPresenter()

View file

@ -14,8 +14,11 @@
* limitations under the License.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.features.messages.attachments
import android.net.Uri
import androidx.media3.common.MimeTypes
import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
@ -33,12 +36,15 @@ import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
class AttachmentsPreviewPresenterTest {
private val mediaPreProcessor = FakeMediaPreProcessor()
private val mockMediaUrl: Uri = mockk("localMediaUri")
@Test
fun `present - send media success scenario`() = runTest {
@ -84,7 +90,10 @@ class AttachmentsPreviewPresenterTest {
}
private fun anAttachmentsPreviewPresenter(
localMedia: LocalMedia = aLocalMedia(mimeType = MimeTypes.IMAGE_JPEG),
localMedia: LocalMedia = aLocalMedia(
uri = mockMediaUrl,
mimeType = MimeTypes.IMAGE_JPEG
),
room: MatrixRoom = FakeMatrixRoom()
): AttachmentsPreviewPresenter {
return AttachmentsPreviewPresenter(

View file

@ -23,7 +23,7 @@ import io.element.android.features.messages.impl.media.local.LocalMedia
import io.mockk.mockk
fun aLocalMedia(
uri: Uri = mockk("localMediaUri"),
uri: Uri,
mimeType: String = MimeTypes.IMAGE_JPEG,
name: String = "a media",
size: Long = 1000,

View file

@ -30,6 +30,7 @@ import io.element.android.features.messages.impl.timeline.factories.event.Timeli
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemEventFactory
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemDaySeparatorFactory
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory
import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
@ -56,7 +57,8 @@ internal fun aTimelineItemsFactory(): TimelineItemsFactory {
daySeparatorFactory = TimelineItemDaySeparatorFactory(
FakeDaySeparatorFormatter()
),
)
),
timelineItemGrouper = TimelineItemGrouper(),
)
}

View file

@ -23,12 +23,12 @@ import io.element.android.features.messages.impl.media.local.LocalMediaFactory
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.media.MediaFile
class FakeLocalMediaFactory : LocalMediaFactory {
class FakeLocalMediaFactory(private val localMediaUri: Uri) : LocalMediaFactory {
var fallbackMimeType: String = MimeTypes.OctetStream
override fun createFromMediaFile(mediaFile: MediaFile, mimeType: String?): LocalMedia {
return aLocalMedia(mimeType = mimeType ?: fallbackMimeType)
return aLocalMedia(uri = localMediaUri, mimeType = mimeType ?: fallbackMimeType)
}
override fun createFromUri(uri: Uri, mimeType: String?): LocalMedia {

View file

@ -14,8 +14,11 @@
* limitations under the License.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.features.messages.media.viewer
import android.net.Uri
import androidx.media3.common.MimeTypes
import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
@ -29,6 +32,8 @@ import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS
import io.element.android.libraries.matrix.test.media.FakeMediaLoader
import io.element.android.libraries.matrix.test.media.aMediaSource
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -37,7 +42,8 @@ private const val TESTED_MEDIA_NAME = "MediaName"
class MediaViewerPresenterTest {
private val localMediaFactory = FakeLocalMediaFactory()
private val mockMediaUrl: Uri = mockk("localMediaUri")
private val localMediaFactory = FakeLocalMediaFactory(mockMediaUrl)
private val mediaLoader = FakeMediaLoader()
@Test

View file

@ -18,6 +18,7 @@
package io.element.android.features.messages.textcomposer
import android.net.Uri
import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.ReceiveTurbine
@ -69,7 +70,8 @@ class MessageComposerPresenterTest {
)
private val mediaPreProcessor = FakeMediaPreProcessor()
private val snackbarDispatcher = SnackbarDispatcher()
private val localMediaFactory = FakeLocalMediaFactory()
private val mockMediaUrl: Uri = mockk("localMediaUri")
private val localMediaFactory = FakeLocalMediaFactory(mockMediaUrl)
@Test
fun `present - initial state`() = runTest {

View file

@ -23,13 +23,8 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.fixtures.aTimelineItemsFactory
import io.element.android.features.messages.impl.timeline.TimelineEvents
import io.element.android.features.messages.impl.timeline.TimelinePresenter
import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.anEventTimelineItem
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -38,7 +33,6 @@ class TimelinePresenterTest {
fun `present - initial state`() = runTest {
val presenter = TimelinePresenter(
timelineItemsFactory = aTimelineItemsFactory(),
timelineItemGrouper = TimelineItemGrouper(),
room = FakeMatrixRoom(),
)
moleculeFlow(RecompositionClock.Immediate) {
@ -55,7 +49,6 @@ class TimelinePresenterTest {
fun `present - load more`() = runTest {
val presenter = TimelinePresenter(
timelineItemsFactory = aTimelineItemsFactory(),
timelineItemGrouper = TimelineItemGrouper(),
room = FakeMatrixRoom(),
)
moleculeFlow(RecompositionClock.Immediate) {
@ -78,7 +71,6 @@ class TimelinePresenterTest {
fun `present - set highlighted event`() = runTest {
val presenter = TimelinePresenter(
timelineItemsFactory = aTimelineItemsFactory(),
timelineItemGrouper = TimelineItemGrouper(),
room = FakeMatrixRoom(),
)
moleculeFlow(RecompositionClock.Immediate) {
@ -95,37 +87,4 @@ class TimelinePresenterTest {
assertThat(withoutHighlightedState.highlightedEventId).isNull()
}
}
@Test
fun `present - expand and collapse grouped events`() = runTest {
val fakeTimeline = FakeMatrixTimeline(
initialTimelineItems = listOf(
MatrixTimelineItem.Event(anEventTimelineItem() /* This is a groupable event */),
MatrixTimelineItem.Event(anEventTimelineItem() /* This is a groupable event */),
)
)
val fakeRoom = FakeMatrixRoom(matrixTimeline = fakeTimeline)
val presenter = TimelinePresenter(
timelineItemsFactory = aTimelineItemsFactory(),
timelineItemGrouper = TimelineItemGrouper(),
room = fakeRoom,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
fakeTimeline.updateTimelineItems { it }
val loadedState = awaitItem()
val group1 = loadedState.timelineItems.first() as TimelineItem.GroupedEvents
assertThat(group1.expanded).isFalse()
loadedState.eventSink.invoke(TimelineEvents.ToggleExpandGroup(group1))
val withExpandedGroup = awaitItem()
val group2 = withExpandedGroup.timelineItems.first() as TimelineItem.GroupedEvents
assertThat(group2.expanded).isTrue()
withExpandedGroup.eventSink.invoke(TimelineEvents.ToggleExpandGroup(group2))
val withCollapsedGroup = awaitItem()
val group3 = withCollapsedGroup.timelineItems.first() as TimelineItem.GroupedEvents
assertThat(group3.expanded).isFalse()
}
}
}

View file

@ -49,7 +49,7 @@ class TimelineItemGrouperTest {
@Test
fun `test empty`() {
val result = sut.group(emptyList(), emptyMap())
val result = sut.group(emptyList())
assertThat(result).isEmpty()
}
@ -60,7 +60,6 @@ class TimelineItemGrouperTest {
aNonGroupableItem,
aNonGroupableItem,
),
emptyMap()
)
assertThat(result).isEqualTo(
listOf(
@ -77,12 +76,10 @@ class TimelineItemGrouperTest {
aGroupableItem.copy(id = AN_EVENT_ID_2.value),
aGroupableItem,
),
emptyMap()
)
assertThat(result).isEqualTo(
listOf(
TimelineItem.GroupedEvents(
expanded = false,
events = listOf(
aGroupableItem,
aGroupableItem.copy(id = AN_EVENT_ID_2.value),
@ -92,28 +89,6 @@ class TimelineItemGrouperTest {
)
}
@Test
fun `test groupables expanded`() {
val result = sut.group(
listOf(
aGroupableItem,
aGroupableItem.copy(id = AN_EVENT_ID_2.value),
),
mapOf("${AN_EVENT_ID_2.value}_group" to true)
)
assertThat(result).isEqualTo(
listOf(
TimelineItem.GroupedEvents(
expanded = true,
events = listOf(
aGroupableItem.copy(id = AN_EVENT_ID_2.value),
aGroupableItem,
).toImmutableList()
),
)
)
}
@Test
fun `test 1 groupable, not group must be created`() {
val listsToTest = listOf(
@ -130,7 +105,7 @@ class TimelineItemGrouperTest {
listOf(aNonGroupableItemNoEvent),
)
listsToTest.forEach { listToTest ->
val result = sut.group(listToTest, emptyMap())
val result = sut.group(listToTest)
assertThat(result).isEqualTo(listToTest)
}
}
@ -146,12 +121,10 @@ class TimelineItemGrouperTest {
aGroupableItem,
aGroupableItem,
),
emptyMap()
)
assertThat(result).isEqualTo(
listOf(
TimelineItem.GroupedEvents(
expanded = false,
events = listOf(
aGroupableItem,
aGroupableItem,
@ -159,7 +132,6 @@ class TimelineItemGrouperTest {
),
aNonGroupableItem,
TimelineItem.GroupedEvents(
expanded = false,
events = listOf(
aGroupableItem,
aGroupableItem,