Merge branch 'develop' into feature/fga/image_loading
This commit is contained in:
commit
2c0771ecc0
165 changed files with 3727 additions and 1085 deletions
|
|
@ -19,9 +19,9 @@ package io.element.android.features.messages.impl
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.messages.impl.actionlist.anActionListState
|
||||
import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemContent
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemList
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineState
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||
import io.element.android.libraries.core.data.StableCharSequence
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
|
@ -46,7 +46,7 @@ fun aMessagesState() = MessagesState(
|
|||
mode = MessageComposerMode.Normal("Hello"),
|
||||
),
|
||||
timelineState = aTimelineState().copy(
|
||||
timelineItems = aTimelineItemList(aTimelineItemContent()),
|
||||
timelineItems = aTimelineItemList(aTimelineItemTextContent()),
|
||||
),
|
||||
actionListState = anActionListState(),
|
||||
hasNetworkConnection = true,
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ import androidx.compose.material.icons.Icons
|
|||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.AttachFile
|
||||
import androidx.compose.material.icons.filled.Collections
|
||||
import androidx.compose.material.icons.filled.LocalLibrary
|
||||
import androidx.compose.material.icons.filled.PhotoCamera
|
||||
import androidx.compose.material.icons.filled.Videocam
|
||||
import androidx.compose.material.rememberModalBottomSheetState
|
||||
|
|
@ -69,6 +68,7 @@ import io.element.android.features.messages.impl.attachments.Attachment
|
|||
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerView
|
||||
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||
import io.element.android.features.messages.impl.timeline.TimelineView
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
|
||||
|
|
@ -88,7 +88,6 @@ import io.element.android.libraries.designsystem.utils.LogCompositions
|
|||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
import io.element.android.libraries.ui.strings.R as StringsR
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalLayoutApi::class)
|
||||
|
|
@ -154,6 +153,11 @@ 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))
|
||||
}
|
||||
|
|
@ -203,7 +207,8 @@ fun MessagesView(
|
|||
.padding(padding)
|
||||
.consumeWindowInsets(padding),
|
||||
onMessageClicked = ::onMessageClicked,
|
||||
onMessageLongClicked = ::onMessageLongClicked
|
||||
onMessageLongClicked = ::onMessageLongClicked,
|
||||
onExpandGroupClick = ::onExpandGroupClick,
|
||||
)
|
||||
},
|
||||
snackbarHost = {
|
||||
|
|
@ -242,6 +247,7 @@ fun MessagesViewContent(
|
|||
modifier: Modifier = Modifier,
|
||||
onMessageClicked: (TimelineItem.Event) -> Unit = {},
|
||||
onMessageLongClicked: (TimelineItem.Event) -> Unit = {},
|
||||
onExpandGroupClick: (TimelineItem.GroupedEvents) -> Unit = {},
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
|
|
@ -255,7 +261,8 @@ fun MessagesViewContent(
|
|||
state = state.timelineState,
|
||||
modifier = Modifier.weight(1f),
|
||||
onMessageClicked = onMessageClicked,
|
||||
onMessageLongClicked = onMessageLongClicked
|
||||
onMessageLongClicked = onMessageLongClicked,
|
||||
onExpandGroupClick = onExpandGroupClick,
|
||||
)
|
||||
}
|
||||
MessageComposerView(
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -53,20 +54,25 @@ class ActionListPresenter @Inject constructor() : Presenter<ActionListState> {
|
|||
)
|
||||
}
|
||||
|
||||
fun CoroutineScope.computeForMessage(timelineItem: TimelineItem.Event, target: MutableState<ActionListState.Target>) = launch {
|
||||
private fun CoroutineScope.computeForMessage(timelineItem: TimelineItem.Event, target: MutableState<ActionListState.Target>) = launch {
|
||||
target.value = ActionListState.Target.Loading(timelineItem)
|
||||
val actions =
|
||||
if (timelineItem.content is TimelineItemRedactedContent) {
|
||||
emptyList()
|
||||
} else {
|
||||
mutableListOf(
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Copy,
|
||||
).also {
|
||||
if (timelineItem.isMine) {
|
||||
it.add(TimelineItemAction.Edit)
|
||||
it.add(TimelineItemAction.Redact)
|
||||
when (timelineItem.content) {
|
||||
is TimelineItemRedactedContent,
|
||||
is TimelineItemStateContent -> {
|
||||
// TODO Add Share action (also) here, and developer options
|
||||
emptyList()
|
||||
}
|
||||
else -> {
|
||||
mutableListOf(
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Copy,
|
||||
).also {
|
||||
if (timelineItem.isMine) {
|
||||
it.add(TimelineItemAction.Edit)
|
||||
it.add(TimelineItemAction.Redact)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,11 @@
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,15 +17,18 @@
|
|||
package io.element.android.features.messages.impl.timeline
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
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
|
||||
|
|
@ -42,6 +45,7 @@ private const val backPaginationPageSize = 50
|
|||
|
||||
class TimelinePresenter @Inject constructor(
|
||||
private val timelineItemsFactory: TimelineItemsFactory,
|
||||
private val timelineItemGrouper: TimelineItemGrouper,
|
||||
room: MatrixRoom,
|
||||
) : Presenter<TimelineState> {
|
||||
|
||||
|
|
@ -53,6 +57,8 @@ class TimelinePresenter @Inject constructor(
|
|||
val highlightedEventId: MutableState<EventId?> = rememberSaveable {
|
||||
mutableStateOf(null)
|
||||
}
|
||||
val expandedGroups = remember { mutableStateMapOf<String, Boolean>() }
|
||||
|
||||
val timelineItems = timelineItemsFactory
|
||||
.flow()
|
||||
.collectAsState()
|
||||
|
|
@ -65,6 +71,9 @@ 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -83,7 +92,7 @@ class TimelinePresenter @Inject constructor(
|
|||
return TimelineState(
|
||||
highlightedEventId = highlightedEventId.value,
|
||||
paginationState = paginationState.value,
|
||||
timelineItems = timelineItems.value.toImmutableList(),
|
||||
timelineItems = timelineItemGrouper.group(timelineItems.value, expandedGroups).toImmutableList(),
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ 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.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
|
@ -55,6 +56,12 @@ internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList
|
|||
content = content,
|
||||
groupPosition = TimelineItemGroupPosition.First
|
||||
),
|
||||
// A state event on top of it
|
||||
aTimelineItemEvent(
|
||||
isMine = false,
|
||||
content = aTimelineItemStateEventContent(),
|
||||
groupPosition = TimelineItemGroupPosition.None
|
||||
),
|
||||
// 3 items (First Middle Last) with isMine = true
|
||||
aTimelineItemEvent(
|
||||
isMine = true,
|
||||
|
|
@ -71,12 +78,18 @@ internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList
|
|||
content = content,
|
||||
groupPosition = TimelineItemGroupPosition.First
|
||||
),
|
||||
// A state event on top of it
|
||||
aTimelineItemEvent(
|
||||
isMine = true,
|
||||
content = aTimelineItemStateEventContent(),
|
||||
groupPosition = TimelineItemGroupPosition.None
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aTimelineItemEvent(
|
||||
isMine: Boolean = false,
|
||||
content: TimelineItemEventContent = aTimelineItemContent(),
|
||||
content: TimelineItemEventContent = aTimelineItemTextContent(),
|
||||
groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.First
|
||||
): TimelineItem.Event {
|
||||
val randomId = "\$" + Random.nextInt().toString()
|
||||
|
|
@ -96,10 +109,3 @@ internal fun aTimelineItemEvent(
|
|||
groupPosition = groupPosition,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aTimelineItemContent(): TimelineItemEventContent {
|
||||
return TimelineItemTextContent(
|
||||
body = "Text",
|
||||
htmlDocument = null
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@
|
|||
|
||||
package io.element.android.features.messages.impl.timeline
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -49,19 +49,24 @@ import androidx.compose.runtime.snapshotFlow
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.LastBaseline
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import io.element.android.features.messages.impl.R
|
||||
import io.element.android.features.messages.impl.timeline.components.MessageEventBubble
|
||||
import io.element.android.features.messages.impl.timeline.components.MessageStateEventContainer
|
||||
import io.element.android.features.messages.impl.timeline.components.TimelineItemReactionsView
|
||||
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
|
||||
import io.element.android.features.messages.impl.timeline.components.group.GroupHeaderView
|
||||
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemDaySeparatorView
|
||||
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineLoadingMoreIndicator
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
|
||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingModel
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
|
|
@ -81,6 +86,7 @@ fun TimelineView(
|
|||
modifier: Modifier = Modifier,
|
||||
onMessageClicked: (TimelineItem.Event) -> Unit = {},
|
||||
onMessageLongClicked: (TimelineItem.Event) -> Unit = {},
|
||||
onExpandGroupClick: (TimelineItem.GroupedEvents) -> Unit = {},
|
||||
) {
|
||||
|
||||
fun onReachedLoadMore() {
|
||||
|
|
@ -92,8 +98,6 @@ fun TimelineView(
|
|||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = lazyListState,
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Bottom,
|
||||
reverseLayout = true
|
||||
) {
|
||||
itemsIndexed(
|
||||
|
|
@ -103,9 +107,10 @@ fun TimelineView(
|
|||
) { index, timelineItem ->
|
||||
TimelineItemRow(
|
||||
timelineItem = timelineItem,
|
||||
isHighlighted = timelineItem.identifier() == state.highlightedEventId?.value,
|
||||
highlightedItem = state.highlightedEventId?.value,
|
||||
onClick = onMessageClicked,
|
||||
onLongClick = onMessageLongClicked
|
||||
onLongClick = onMessageLongClicked,
|
||||
onExpandGroupClick = onExpandGroupClick,
|
||||
)
|
||||
if (index == state.timelineItems.lastIndex) {
|
||||
onReachedLoadMore()
|
||||
|
|
@ -124,16 +129,20 @@ fun TimelineView(
|
|||
@Composable
|
||||
fun TimelineItemRow(
|
||||
timelineItem: TimelineItem,
|
||||
isHighlighted: Boolean,
|
||||
highlightedItem: String?,
|
||||
onClick: (TimelineItem.Event) -> Unit,
|
||||
onLongClick: (TimelineItem.Event) -> Unit,
|
||||
onExpandGroupClick: (TimelineItem.GroupedEvents) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
when (timelineItem) {
|
||||
is TimelineItem.Virtual -> TimelineItemVirtualRow(
|
||||
virtual = timelineItem
|
||||
)
|
||||
is TimelineItem.Virtual -> {
|
||||
TimelineItemVirtualRow(
|
||||
virtual = timelineItem,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
is TimelineItem.Event -> {
|
||||
|
||||
fun onClick() {
|
||||
onClick(timelineItem)
|
||||
}
|
||||
|
|
@ -142,12 +151,54 @@ fun TimelineItemRow(
|
|||
onLongClick(timelineItem)
|
||||
}
|
||||
|
||||
TimelineItemEventRow(
|
||||
event = timelineItem,
|
||||
isHighlighted = isHighlighted,
|
||||
onClick = ::onClick,
|
||||
onLongClick = ::onLongClick
|
||||
)
|
||||
if (timelineItem.content is TimelineItemStateContent) {
|
||||
TimelineItemStateEventRow(
|
||||
event = timelineItem,
|
||||
isHighlighted = highlightedItem == timelineItem.identifier(),
|
||||
onClick = ::onClick,
|
||||
onLongClick = ::onLongClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
TimelineItemEventRow(
|
||||
event = timelineItem,
|
||||
isHighlighted = highlightedItem == timelineItem.identifier(),
|
||||
onClick = ::onClick,
|
||||
onLongClick = ::onLongClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
is TimelineItem.GroupedEvents -> {
|
||||
fun onExpandGroupClick() {
|
||||
onExpandGroupClick(timelineItem)
|
||||
}
|
||||
|
||||
Column(modifier = modifier.animateContentSize()) {
|
||||
GroupHeaderView(
|
||||
text = pluralStringResource(
|
||||
id = R.plurals.room_timeline_state_changes,
|
||||
count = timelineItem.events.size,
|
||||
timelineItem.events.size
|
||||
),
|
||||
isExpanded = timelineItem.expanded,
|
||||
isHighlighted = !timelineItem.expanded && timelineItem.events.any { it.identifier() == highlightedItem },
|
||||
onClick = ::onExpandGroupClick,
|
||||
)
|
||||
if (timelineItem.expanded) {
|
||||
Column {
|
||||
timelineItem.events.forEach { subGroupEvent ->
|
||||
TimelineItemRow(
|
||||
timelineItem = subGroupEvent,
|
||||
highlightedItem = highlightedItem,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
onExpandGroupClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -236,6 +287,42 @@ fun TimelineItemEventRow(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TimelineItemStateEventRow(
|
||||
event: TimelineItem.Event,
|
||||
isHighlighted: Boolean,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
MessageStateEventContainer(
|
||||
isHighlighted = isHighlighted,
|
||||
interactionSource = interactionSource,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
modifier = Modifier
|
||||
.zIndex(-1f)
|
||||
.widthIn(max = 320.dp)
|
||||
) {
|
||||
val contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)
|
||||
TimelineItemEventContentView(
|
||||
content = event.content,
|
||||
interactionSource = interactionSource,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
modifier = contentModifier
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MessageSenderInformation(
|
||||
sender: String,
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ 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))
|
||||
} else {
|
||||
offset(x = 20.dp, y = -(12.dp))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
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
|
||||
|
||||
private val CORNER_RADIUS = 8.dp
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MessageStateEventContainer(
|
||||
isHighlighted: Boolean,
|
||||
interactionSource: MutableInteractionSource,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit = {},
|
||||
onLongClick: () -> Unit = {},
|
||||
content: @Composable () -> Unit = {},
|
||||
) {
|
||||
val backgroundColor = if (isHighlighted) {
|
||||
ElementTheme.colors.messageHighlightedBackground
|
||||
} else {
|
||||
Color.Companion.Transparent
|
||||
}
|
||||
val shape = RoundedCornerShape(CORNER_RADIUS)
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.widthIn(min = 80.dp)
|
||||
.clip(shape)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
indication = rememberRipple(),
|
||||
interactionSource = interactionSource
|
||||
),
|
||||
color = backgroundColor,
|
||||
shape = shape,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun MessageStateEventContainerLightPreview() =
|
||||
ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun MessageStateEventContainerDarkPreview() =
|
||||
ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
Column {
|
||||
MessageStateEventContainer(
|
||||
isHighlighted = false,
|
||||
interactionSource = MutableInteractionSource(),
|
||||
) {
|
||||
Spacer(modifier = Modifier.size(width = 120.dp, height = 32.dp))
|
||||
}
|
||||
MessageStateEventContainer(
|
||||
isHighlighted = true,
|
||||
interactionSource = MutableInteractionSource(),
|
||||
) {
|
||||
Spacer(modifier = Modifier.size(width = 120.dp, height = 32.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||
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.TimelineItemRedactedContent
|
||||
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.TimelineItemUnknownContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||
|
|
@ -74,5 +75,9 @@ fun TimelineItemEventContentView(
|
|||
content = content,
|
||||
modifier = modifier.defaultContentPadding()
|
||||
)
|
||||
is TimelineItemStateContent -> TimelineItemStateView(
|
||||
content = content,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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.event
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
||||
@Composable
|
||||
fun TimelineItemStateView(
|
||||
content: TimelineItemStateContent,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
fontSize = 13.sp,
|
||||
text = content.body,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineItemStateViewLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineItemStateViewDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
TimelineItemStateView(
|
||||
content = aTimelineItemStateEventContent(),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* 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.group
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
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.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ExpandLess
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
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.Surface
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
||||
private val CORNER_RADIUS = 8.dp
|
||||
|
||||
@Composable
|
||||
fun GroupHeaderView(
|
||||
text: String,
|
||||
isExpanded: Boolean,
|
||||
isHighlighted: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val backgroundColor = if (isHighlighted) {
|
||||
ElementTheme.colors.messageHighlightedBackground
|
||||
} else {
|
||||
Color.Companion.Transparent
|
||||
}
|
||||
val shape = RoundedCornerShape(CORNER_RADIUS)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.clip(shape)
|
||||
.clickable(onClick = onClick),
|
||||
color = backgroundColor,
|
||||
shape = shape,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
fontSize = 13.sp
|
||||
)
|
||||
val icon = if (isExpanded) {
|
||||
Icons.Default.ExpandLess
|
||||
} else {
|
||||
Icons.Default.ExpandMore
|
||||
}
|
||||
Icon(icon, "", tint = MaterialTheme.colorScheme.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun GroupHeaderViewLightPreview() =
|
||||
ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun GroupHeaderViewDarkPreview() =
|
||||
ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
GroupHeaderView(
|
||||
text = "8 room changes (expanded)",
|
||||
isExpanded = true,
|
||||
isHighlighted = false,
|
||||
onClick = {}
|
||||
)
|
||||
GroupHeaderView(
|
||||
text = "8 room changes (not expanded)",
|
||||
isExpanded = false,
|
||||
isHighlighted = false,
|
||||
onClick = {}
|
||||
)
|
||||
GroupHeaderView(
|
||||
text = "8 room changes (expanded/h)",
|
||||
isExpanded = true,
|
||||
isHighlighted = true,
|
||||
onClick = {}
|
||||
)
|
||||
GroupHeaderView(
|
||||
text = "8 room changes (not expanded/h)",
|
||||
isExpanded = false,
|
||||
isHighlighted = true,
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline.factories.event
|
|||
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
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
|
||||
|
|
@ -26,7 +27,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.RedactedConte
|
|||
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.EventContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
|
||||
import javax.inject.Inject
|
||||
|
|
@ -43,15 +43,15 @@ class TimelineItemContentFactory @Inject constructor(
|
|||
private val failedToParseStateFactory: TimelineItemContentFailedToParseStateFactory
|
||||
) {
|
||||
|
||||
fun create(itemContent: EventContent): TimelineItemEventContent {
|
||||
return when (itemContent) {
|
||||
fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent {
|
||||
return when (val itemContent = eventTimelineItem.content) {
|
||||
is FailedToParseMessageLikeContent -> failedToParseMessageFactory.create(itemContent)
|
||||
is FailedToParseStateContent -> failedToParseStateFactory.create(itemContent)
|
||||
is MessageContent -> messageFactory.create(itemContent)
|
||||
is ProfileChangeContent -> profileChangeFactory.create(itemContent)
|
||||
is ProfileChangeContent -> profileChangeFactory.create(eventTimelineItem)
|
||||
is RedactedContent -> redactedMessageFactory.create(itemContent)
|
||||
is RoomMembershipContent -> roomMembershipFactory.create(itemContent)
|
||||
is StateContent -> stateFactory.create(itemContent)
|
||||
is RoomMembershipContent -> roomMembershipFactory.create(eventTimelineItem)
|
||||
is StateContent -> stateFactory.create(eventTimelineItem)
|
||||
is StickerContent -> stickerFactory.create(itemContent)
|
||||
is UnableToDecryptContent -> utdFactory.create(itemContent)
|
||||
is UnknownContent -> TimelineItemUnknownContent
|
||||
|
|
|
|||
|
|
@ -17,13 +17,18 @@
|
|||
package io.element.android.features.messages.impl.timeline.factories.event
|
||||
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemProfileChangeContent
|
||||
import io.element.android.libraries.core.extensions.orEmpty
|
||||
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineItemContentProfileChangeFactory @Inject constructor() {
|
||||
class TimelineItemContentProfileChangeFactory @Inject constructor(
|
||||
private val timelineEventFormatter: TimelineEventFormatter,
|
||||
) {
|
||||
|
||||
fun create(content: ProfileChangeContent): TimelineItemEventContent {
|
||||
return TimelineItemUnknownContent
|
||||
fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent {
|
||||
val text = timelineEventFormatter.format(eventTimelineItem)
|
||||
return TimelineItemProfileChangeContent(text.orEmpty().toString())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,13 +17,18 @@
|
|||
package io.element.android.features.messages.impl.timeline.factories.event
|
||||
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRoomMembershipContent
|
||||
import io.element.android.libraries.core.extensions.orEmpty
|
||||
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineItemContentRoomMembershipFactory @Inject constructor() {
|
||||
class TimelineItemContentRoomMembershipFactory @Inject constructor(
|
||||
private val timelineEventFormatter: TimelineEventFormatter,
|
||||
) {
|
||||
|
||||
fun create(content: RoomMembershipContent): TimelineItemEventContent {
|
||||
return TimelineItemUnknownContent
|
||||
fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent {
|
||||
val text = timelineEventFormatter.format(eventTimelineItem)
|
||||
return TimelineItemRoomMembershipContent(text.orEmpty().toString())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,13 +17,18 @@
|
|||
package io.element.android.features.messages.impl.timeline.factories.event
|
||||
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateEventContent
|
||||
import io.element.android.libraries.core.extensions.orEmpty
|
||||
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineItemContentStateFactory @Inject constructor() {
|
||||
class TimelineItemContentStateFactory @Inject constructor(
|
||||
private val timelineEventFormatter: TimelineEventFormatter,
|
||||
) {
|
||||
|
||||
fun create(content: StateContent): TimelineItemEventContent {
|
||||
return TimelineItemUnknownContent
|
||||
fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent {
|
||||
val text = timelineEventFormatter.format(eventTimelineItem)
|
||||
return TimelineItemStateEventContent(text.orEmpty().toString())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ class TimelineItemEventFactory @Inject constructor(
|
|||
senderId = currentSender,
|
||||
senderDisplayName = senderDisplayName,
|
||||
senderAvatar = senderAvatarData,
|
||||
content = contentFactory.create(currentTimelineItem.event.content),
|
||||
content = contentFactory.create(currentTimelineItem.event),
|
||||
isMine = currentTimelineItem.event.isOwn,
|
||||
groupPosition = groupPosition,
|
||||
reactionsState = currentTimelineItem.computeReactionsState()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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.TimelineItemEmoteContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
|
||||
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.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> {
|
||||
val result = mutableListOf<TimelineItem>()
|
||||
val currentGroup = mutableListOf<TimelineItem.Event>()
|
||||
from.forEach { timelineItem ->
|
||||
if (timelineItem is TimelineItem.Event && timelineItem.canBeGrouped()) {
|
||||
currentGroup.add(0, timelineItem)
|
||||
} else {
|
||||
// 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)
|
||||
currentGroup.clear()
|
||||
}
|
||||
result.add(timelineItem)
|
||||
}
|
||||
}
|
||||
if (currentGroup.isNotEmpty()) {
|
||||
result.addGroup(currentGroup, expandedGroups)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun TimelineItem.Event.canBeGrouped(): Boolean {
|
||||
return when (content) {
|
||||
is TimelineItemEncryptedContent,
|
||||
is TimelineItemImageContent,
|
||||
TimelineItemRedactedContent,
|
||||
is TimelineItemEmoteContent,
|
||||
is TimelineItemNoticeContent,
|
||||
is TimelineItemTextContent,
|
||||
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>,
|
||||
) {
|
||||
if (group.size == 1) {
|
||||
// Do not create a group with just 1 item, just add the item to the result
|
||||
add(group.first())
|
||||
} else {
|
||||
add(
|
||||
TimelineItem.GroupedEvents(
|
||||
expanded = expandedGroups[group.first().id + "_group"].orFalse(),
|
||||
events = group.toImmutableList()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ import io.element.android.features.messages.impl.timeline.model.virtual.Timeline
|
|||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
sealed interface TimelineItem {
|
||||
|
|
@ -29,11 +30,13 @@ sealed interface TimelineItem {
|
|||
fun identifier(): String = when (this) {
|
||||
is Event -> id
|
||||
is Virtual -> id
|
||||
is GroupedEvents -> id
|
||||
}
|
||||
|
||||
fun contentType(): String = when (this) {
|
||||
is Event -> content.type
|
||||
is Virtual -> model.type
|
||||
is GroupedEvents -> "groupedEvent"
|
||||
}
|
||||
|
||||
@Immutable
|
||||
|
|
@ -60,4 +63,13 @@ sealed interface TimelineItem {
|
|||
|
||||
val safeSenderName: String = senderDisplayName ?: senderId.value
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class GroupedEvents(
|
||||
val expanded: Boolean,
|
||||
val events: ImmutableList<Event>,
|
||||
) : TimelineItem {
|
||||
// use first id with a suffix
|
||||
val id = events.first().id + "_group"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,3 +68,7 @@ fun aTimelineItemTextContent() = TimelineItemTextContent(
|
|||
)
|
||||
|
||||
fun aTimelineItemUnknownContent() = TimelineItemUnknownContent
|
||||
|
||||
fun aTimelineItemStateEventContent() = TimelineItemStateEventContent(
|
||||
body = "A state event",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (c) 2022 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.model.event
|
||||
|
||||
data class TimelineItemProfileChangeContent(
|
||||
override val body: String,
|
||||
) : TimelineItemStateContent {
|
||||
override val type: String = "TimelineItemProfileChangeContent"
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (c) 2022 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.model.event
|
||||
|
||||
data class TimelineItemRoomMembershipContent(
|
||||
override val body: String,
|
||||
) : TimelineItemStateContent {
|
||||
override val type: String = "TimelineItemRoomMembershipContent"
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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.model.event
|
||||
|
||||
sealed interface TimelineItemStateContent : TimelineItemEventContent {
|
||||
val body: String
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (c) 2022 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.model.event
|
||||
|
||||
data class TimelineItemStateEventContent(
|
||||
override val body: String,
|
||||
) : TimelineItemStateContent {
|
||||
override val type: String = "TimelineItemStateEventContent"
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?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 cambio en la sala"</item>
|
||||
<item quantity="other">"%1$d cambios en la sala"</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?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 modifica alla stanza"</item>
|
||||
<item quantity="other">"%1$d modifiche alla stanza"</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
|
@ -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 schimbare a camerii"</item>
|
||||
<item quantity="few">"%1$d schimbări ale camerei"</item>
|
||||
<item quantity="other">"%1$d schimbări ale camerei"</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
|
@ -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 room change"</item>
|
||||
<item quantity="other">"%1$d room changes"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_attachment_source_camera">"Camera"</string>
|
||||
<string name="screen_room_attachment_source_camera_photo">"Take photo"</string>
|
||||
<string name="screen_room_attachment_source_camera_video">"Record a video"</string>
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import io.element.android.features.messages.impl.actionlist.model.TimelineItemAc
|
|||
import io.element.android.features.messages.media.FakeLocalMediaFactory
|
||||
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.networkmonitor.test.FakeNetworkMonitor
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
|
|
@ -141,6 +142,7 @@ class MessagesPresenterTest {
|
|||
)
|
||||
val timelinePresenter = TimelinePresenter(
|
||||
timelineItemsFactory = aTimelineItemsFactory(),
|
||||
timelineItemGrouper = TimelineItemGrouper(),
|
||||
room = matrixRoom,
|
||||
)
|
||||
val actionListPresenter = ActionListPresenter()
|
||||
|
|
|
|||
|
|
@ -31,26 +31,39 @@ import io.element.android.features.messages.impl.timeline.factories.event.Timeli
|
|||
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.libraries.dateformatter.test.FakeDaySeparatorFormatter
|
||||
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
|
||||
internal fun aTimelineItemsFactory() = TimelineItemsFactory(
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
eventItemFactory = TimelineItemEventFactory(
|
||||
TimelineItemContentFactory(
|
||||
messageFactory = TimelineItemContentMessageFactory(),
|
||||
redactedMessageFactory = TimelineItemContentRedactedFactory(),
|
||||
stickerFactory = TimelineItemContentStickerFactory(),
|
||||
utdFactory = TimelineItemContentUTDFactory(),
|
||||
roomMembershipFactory = TimelineItemContentRoomMembershipFactory(),
|
||||
profileChangeFactory = TimelineItemContentProfileChangeFactory(),
|
||||
stateFactory = TimelineItemContentStateFactory(),
|
||||
failedToParseMessageFactory = TimelineItemContentFailedToParseMessageFactory(),
|
||||
failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory()
|
||||
)
|
||||
),
|
||||
virtualItemFactory = TimelineItemVirtualFactory(
|
||||
daySeparatorFactory = TimelineItemDaySeparatorFactory(
|
||||
FakeDaySeparatorFormatter()
|
||||
internal fun aTimelineItemsFactory(): TimelineItemsFactory {
|
||||
val timelineEventFormatter = aTimelineEventFormatter()
|
||||
return TimelineItemsFactory(
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
eventItemFactory = TimelineItemEventFactory(
|
||||
TimelineItemContentFactory(
|
||||
messageFactory = TimelineItemContentMessageFactory(),
|
||||
redactedMessageFactory = TimelineItemContentRedactedFactory(),
|
||||
stickerFactory = TimelineItemContentStickerFactory(),
|
||||
utdFactory = TimelineItemContentUTDFactory(),
|
||||
roomMembershipFactory = TimelineItemContentRoomMembershipFactory(timelineEventFormatter),
|
||||
profileChangeFactory = TimelineItemContentProfileChangeFactory(timelineEventFormatter),
|
||||
stateFactory = TimelineItemContentStateFactory(timelineEventFormatter),
|
||||
failedToParseMessageFactory = TimelineItemContentFailedToParseMessageFactory(),
|
||||
failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory()
|
||||
)
|
||||
),
|
||||
virtualItemFactory = TimelineItemVirtualFactory(
|
||||
daySeparatorFactory = TimelineItemDaySeparatorFactory(
|
||||
FakeDaySeparatorFormatter()
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aTimelineEventFormatter(): TimelineEventFormatter {
|
||||
return object : TimelineEventFormatter {
|
||||
override fun format(event: EventTimelineItem): CharSequence {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.features.messages.textcomposer
|
||||
|
||||
import app.cash.molecule.RecompositionClock
|
||||
|
|
@ -21,11 +23,11 @@ import app.cash.molecule.moleculeFlow
|
|||
import app.cash.turbine.ReceiveTurbine
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.messages.media.FakeLocalMediaFactory
|
||||
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
||||
import io.element.android.features.messages.media.FakeLocalMediaFactory
|
||||
import io.element.android.libraries.core.data.StableCharSequence
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||
|
|
@ -52,6 +54,7 @@ import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
|||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
|
|
|
|||
|
|
@ -23,8 +23,13 @@ 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
|
||||
|
||||
|
|
@ -33,6 +38,7 @@ class TimelinePresenterTest {
|
|||
fun `present - initial state`() = runTest {
|
||||
val presenter = TimelinePresenter(
|
||||
timelineItemsFactory = aTimelineItemsFactory(),
|
||||
timelineItemGrouper = TimelineItemGrouper(),
|
||||
room = FakeMatrixRoom(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
|
|
@ -49,6 +55,7 @@ class TimelinePresenterTest {
|
|||
fun `present - load more`() = runTest {
|
||||
val presenter = TimelinePresenter(
|
||||
timelineItemsFactory = aTimelineItemsFactory(),
|
||||
timelineItemGrouper = TimelineItemGrouper(),
|
||||
room = FakeMatrixRoom(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
|
|
@ -71,6 +78,7 @@ class TimelinePresenterTest {
|
|||
fun `present - set highlighted event`() = runTest {
|
||||
val presenter = TimelinePresenter(
|
||||
timelineItemsFactory = aTimelineItemsFactory(),
|
||||
timelineItemGrouper = TimelineItemGrouper(),
|
||||
room = FakeMatrixRoom(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
|
|
@ -87,4 +95,37 @@ 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* 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.timeline.groups
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.messages.fixtures.aMessageEvent
|
||||
import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper
|
||||
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.TimelineItemReactions
|
||||
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.designsystem.components.avatar.anAvatarData
|
||||
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 kotlinx.collections.immutable.toImmutableList
|
||||
import org.junit.Test
|
||||
|
||||
class TimelineItemGrouperTest {
|
||||
private val sut = TimelineItemGrouper()
|
||||
|
||||
private val aGroupableItem = TimelineItem.Event(
|
||||
id = AN_EVENT_ID.value,
|
||||
senderId = A_USER_ID,
|
||||
senderAvatar = anAvatarData(),
|
||||
senderDisplayName = "",
|
||||
content = TimelineItemStateEventContent(body = "a state event"),
|
||||
reactionsState = TimelineItemReactions(emptyList<AggregatedReaction>().toImmutableList())
|
||||
)
|
||||
private val aNonGroupableItem = aMessageEvent()
|
||||
private val aNonGroupableItemNoEvent = TimelineItem.Virtual("virtual", aTimelineItemDaySeparatorModel("Today"))
|
||||
|
||||
@Test
|
||||
fun `test empty`() {
|
||||
val result = sut.group(emptyList(), emptyMap())
|
||||
assertThat(result).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test non groupables`() {
|
||||
val result = sut.group(
|
||||
listOf(
|
||||
aNonGroupableItem,
|
||||
aNonGroupableItem,
|
||||
),
|
||||
emptyMap()
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
listOf(
|
||||
aNonGroupableItem,
|
||||
aNonGroupableItem,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test groupables and ensure reordering`() {
|
||||
val result = sut.group(
|
||||
listOf(
|
||||
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),
|
||||
).toImmutableList()
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@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(
|
||||
listOf(aGroupableItem),
|
||||
listOf(aGroupableItem, aNonGroupableItem),
|
||||
listOf(aGroupableItem, aNonGroupableItemNoEvent),
|
||||
listOf(aNonGroupableItem, aGroupableItem),
|
||||
listOf(aNonGroupableItemNoEvent, aGroupableItem),
|
||||
listOf(aNonGroupableItem, aGroupableItem, aNonGroupableItem),
|
||||
listOf(aNonGroupableItemNoEvent, aGroupableItem, aNonGroupableItemNoEvent),
|
||||
listOf(aGroupableItem, aNonGroupableItem, aGroupableItem),
|
||||
listOf(aGroupableItem, aNonGroupableItemNoEvent, aGroupableItem),
|
||||
listOf(aNonGroupableItem),
|
||||
listOf(aNonGroupableItemNoEvent),
|
||||
)
|
||||
listsToTest.forEach { listToTest ->
|
||||
val result = sut.group(listToTest, emptyMap())
|
||||
assertThat(result).isEqualTo(listToTest)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test 3 blocks`() {
|
||||
val result = sut.group(
|
||||
listOf(
|
||||
aGroupableItem,
|
||||
aGroupableItem,
|
||||
aNonGroupableItem,
|
||||
aGroupableItem,
|
||||
aGroupableItem,
|
||||
aGroupableItem,
|
||||
),
|
||||
emptyMap()
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
listOf(
|
||||
TimelineItem.GroupedEvents(
|
||||
expanded = false,
|
||||
events = listOf(
|
||||
aGroupableItem,
|
||||
aGroupableItem,
|
||||
).toImmutableList()
|
||||
),
|
||||
aNonGroupableItem,
|
||||
TimelineItem.GroupedEvents(
|
||||
expanded = false,
|
||||
events = listOf(
|
||||
aGroupableItem,
|
||||
aGroupableItem,
|
||||
aGroupableItem,
|
||||
).toImmutableList()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue