Merge pull request #467 from vector-im/feature/bma/reworkGrouper
Improve timeline rendering for message and state event
This commit is contained in:
commit
08fdb40ef6
91 changed files with 431 additions and 264 deletions
|
|
@ -153,11 +153,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 +203,6 @@ fun MessagesView(
|
|||
.consumeWindowInsets(padding),
|
||||
onMessageClicked = ::onMessageClicked,
|
||||
onMessageLongClicked = ::onMessageLongClicked,
|
||||
onExpandGroupClick = ::onExpandGroupClick,
|
||||
)
|
||||
},
|
||||
snackbarHost = {
|
||||
|
|
@ -247,7 +241,6 @@ fun MessagesViewContent(
|
|||
modifier: Modifier = Modifier,
|
||||
onMessageClicked: (TimelineItem.Event) -> Unit = {},
|
||||
onMessageLongClicked: (TimelineItem.Event) -> Unit = {},
|
||||
onExpandGroupClick: (TimelineItem.GroupedEvents) -> Unit = {},
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
|
|
@ -262,7 +255,6 @@ fun MessagesViewContent(
|
|||
modifier = Modifier.weight(1f),
|
||||
onMessageClicked = onMessageClicked,
|
||||
onMessageLongClicked = onMessageLongClicked,
|
||||
onExpandGroupClick = onExpandGroupClick,
|
||||
)
|
||||
}
|
||||
MessageComposerView(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -47,8 +47,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
|
||||
|
|
@ -95,7 +97,6 @@ fun TimelineView(
|
|||
modifier: Modifier = Modifier,
|
||||
onMessageClicked: (TimelineItem.Event) -> Unit = {},
|
||||
onMessageLongClicked: (TimelineItem.Event) -> Unit = {},
|
||||
onExpandGroupClick: (TimelineItem.GroupedEvents) -> Unit = {},
|
||||
) {
|
||||
|
||||
fun onReachedLoadMore() {
|
||||
|
|
@ -119,7 +120,6 @@ fun TimelineView(
|
|||
highlightedItem = state.highlightedEventId?.value,
|
||||
onClick = onMessageClicked,
|
||||
onLongClick = onMessageLongClicked,
|
||||
onExpandGroupClick = onExpandGroupClick,
|
||||
)
|
||||
if (index == state.timelineItems.lastIndex) {
|
||||
onReachedLoadMore()
|
||||
|
|
@ -141,7 +141,6 @@ fun TimelineItemRow(
|
|||
highlightedItem: String?,
|
||||
onClick: (TimelineItem.Event) -> Unit,
|
||||
onLongClick: (TimelineItem.Event) -> Unit,
|
||||
onExpandGroupClick: (TimelineItem.GroupedEvents) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
when (timelineItem) {
|
||||
|
|
@ -179,8 +178,10 @@ fun TimelineItemRow(
|
|||
}
|
||||
}
|
||||
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 +191,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 +203,6 @@ fun TimelineItemRow(
|
|||
highlightedItem = highlightedItem,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
onExpandGroupClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -247,14 +247,16 @@ 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)
|
||||
)
|
||||
}
|
||||
val bubbleState = BubbleState(
|
||||
|
|
@ -282,7 +284,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) {
|
||||
|
|
|
|||
|
|
@ -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()}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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>) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -28,7 +28,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
|
||||
|
|
@ -142,7 +141,6 @@ class MessagesPresenterTest {
|
|||
)
|
||||
val timelinePresenter = TimelinePresenter(
|
||||
timelineItemsFactory = aTimelineItemsFactory(),
|
||||
timelineItemGrouper = TimelineItemGrouper(),
|
||||
room = matrixRoom,
|
||||
)
|
||||
val actionListPresenter = ActionListPresenter()
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.features.messages.attachments
|
||||
|
||||
import androidx.media3.common.MimeTypes
|
||||
|
|
@ -33,6 +35,7 @@ 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 kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.features.messages.media.viewer
|
||||
|
||||
import androidx.media3.common.MimeTypes
|
||||
|
|
@ -29,6 +31,7 @@ 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 kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.libraries.core.extensions
|
||||
|
||||
fun Boolean.toOnOff() = if (this) "ON" else "OFF"
|
||||
fun Boolean.to01() = if (this) "1" else "0"
|
||||
|
||||
inline fun <T> T.ooi(block: (T) -> Unit): T = also(block)
|
||||
|
||||
|
|
|
|||
|
|
@ -14,9 +14,12 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.tests.testutils
|
||||
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestCoroutineScheduler
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bb1648a000da198a30d3214adecf0b1266a150a226548ce77ac6d0eb001c65e2
|
||||
size 5238
|
||||
oid sha256:7ddcd892538faa691ade67de5145392f89eaab4c903cc8fed00b24726753ed4e
|
||||
size 6945
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:44487cb1f6115b388bb043081e15d904a090277fca6edea05ccb7b6c54d56de2
|
||||
size 5761
|
||||
oid sha256:cea39d32d858cd7b28885e634e746aea3652a56b665563e26f62346d9eaa1128
|
||||
size 7172
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:75ba23e1b82c3a5e4c3f4330a004b00d4eb2a2d3a27ce687ad6876c6e6e129be
|
||||
size 5547
|
||||
oid sha256:7eb000677f48efbd9dfbe373b221dbc98b2da65404e86aea42e79416cc032205
|
||||
size 7107
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:46067cba703ae02846a141dd53b888d48b6e55890cefed4cef4795c0b5910346
|
||||
size 5871
|
||||
oid sha256:1daab66cf9beb8a5bb9c4cdea0db76fdc690b850bde1ada9679bfaf0167e6982
|
||||
size 7150
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:37ba26495ac60e2857cfab755675993f44775c8c0bd36b5b7d69ccebf9a71a6d
|
||||
size 7269
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dcdb3be85af22f6997b372d268d54ca287ef282a8c1353d4fcb47d44ceffcf8a
|
||||
size 7595
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:65c434fc772482b1a884bc3c68447e228b470549ff72a510e43ba0aaae69387a
|
||||
size 7396
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:23f468e1737a6a3789fdfd56322e4b15551c1a0d74249f92aee92879d476ea91
|
||||
size 7485
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dbd968203eb9092f6ceab631da735a72bd15a44c3c8faaba45246a58bf3d9837
|
||||
size 5566
|
||||
oid sha256:7ab56f3950f19ed24fcd2592157046eb878a2dde9e9831b8a3d846f5f3207dc0
|
||||
size 7062
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d7f03a7e0ac8e63116d1c4806f07266ace800f52c8f089846c515ff12488918f
|
||||
size 5875
|
||||
oid sha256:2c6e69cd6e3687ef724ff2c8f5151fb6679fd6395d866d7f944507c130e938eb
|
||||
size 7084
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:492b8ec89b02bbba9abb849d6b28f61d77f448d66d1eef5eba5a6f66db725897
|
||||
size 5133
|
||||
oid sha256:d789e51e1f3c440f8577d465322c982a5a6aea54f7d1a7ab12e183ba841b0f14
|
||||
size 7274
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ccf5cf29bdcb6016203c849ea1013de2ab42ec21c9b202c56dea2d7b2b75434b
|
||||
size 5516
|
||||
oid sha256:15100ea369eab89e99c4252b88e33f362cca287e6ef0799e1e3e3a8f4be6e227
|
||||
size 7327
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e666a4e785be44883e945e5ab22766c96358381467fc6860c3e8733a359d6801
|
||||
size 5462
|
||||
oid sha256:92c2dc09ef213e2f55568f036d17e0ba81f9053a61774bd89ea27273c9a0aeb8
|
||||
size 7415
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6fc64090e9c685246a0adc2a2250cb80fcc64cbe7d6578d63bc4827eaa50591e
|
||||
size 5682
|
||||
oid sha256:a035a78264d396b78c2653b89ab0edbc3ee6400d380eccd587f80cafdd57f165
|
||||
size 7335
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b584ab84ca09c1fe9c2ce8433a92adb8f1f6c595ccc702f10b976b81951b9048
|
||||
size 5253
|
||||
oid sha256:012db6849c7458978d11f0c56b171dcc01993ac3bbc940639a609a04e5f591ad
|
||||
size 7093
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ab7a53108da1eda0c1f24360d4c4073dca110dd3b368cc1f117cd3f351a385dc
|
||||
size 5789
|
||||
oid sha256:d429ec724142f73fa7846ec26a21b62e1a3466bd1e85193bdf48e749bf188978
|
||||
size 7253
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ace5a91e960c6570647e2b8ad2de428ba474e48531a2f74ab267d07668d7a9e8
|
||||
size 4928
|
||||
oid sha256:87de1686f0e84c9f00f80dc02a63ad6d0b0c360762a9d4f996b315407aec37d6
|
||||
size 6700
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:aec5d74e6f4d316c1ea124dc50e2abbfd22d6ee94b6d82519b3f9cab7a191dfa
|
||||
size 5348
|
||||
oid sha256:ab4af3d37dc161ccba7366517f60cb50923539615909c4bf76ec667095a4293c
|
||||
size 6712
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dfaa4d6032641b332b111b20c81ec4bca917391cf122a06718fca7da849a7c94
|
||||
size 5124
|
||||
oid sha256:a5962c60a9072b96cbd72e8ea9cf6344f715d8469a45f51fcdebacfec5344544
|
||||
size 6693
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ac3eb3ebe9f2f30224389586f9b5901106b73cee9f5b4cdeb0f76b313ba0cd31
|
||||
size 5384
|
||||
oid sha256:3ed947a2df42bc4cd7b0c181d608c69763897383f4846b2878e3df2705dfae0e
|
||||
size 6715
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:30f04a04ecde63528df19021a9b43b1bb2d26aaf31fa021e4b83f4503fd73782
|
||||
size 6997
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a4e390e24a61c5ea42c2469bc71c0d7785de647991dbf5b8d5df360e76adcd0a
|
||||
size 7184
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:eba96060e90371916f2e62f550cc0fd4fa44e2872263a353dd3c37c2664c0f47
|
||||
size 7001
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:05f55e1df050651b48708dc3f40fe95a8ab68d1a0fea7838023cbadc2a21da57
|
||||
size 7078
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8e0eaf65f627b7be068311b0c8f52cfbe847571684a0c63dda3e7c0621eac42c
|
||||
size 5133
|
||||
oid sha256:544dac21d0af65c94f1e38ee2b0ff5a20dd3f12c40a5c41037c12f3af54df9ca
|
||||
size 6598
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8359444a65d74682106930a7bbc6fb171acb95446cc5ff67eefa0dadc51e5932
|
||||
size 5397
|
||||
oid sha256:dca857bfb1b6405488d96214b72e219f9aa4c4abd535cd1dd83b7efd7603c497
|
||||
size 6641
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d774fca7f28623d7e464c233e5e2282b5c7d608d7aa6084b2350a4f71765bc5a
|
||||
size 4809
|
||||
oid sha256:83613dcfe9968db44cabf918d6dc9ab011869c80d4bf089d5d4c46f7b6bedae4
|
||||
size 6968
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:32c81961535a22ce1fccc15865cd76bc9844168552a2205f7f7342398866b622
|
||||
size 5109
|
||||
oid sha256:8d44801312b785f5c595a988124de6d70d65de01fb18c74876b1f6328e84c222
|
||||
size 6914
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bd53dc603b4689eabe41e5d90ea0d8a70b769d60082ee57eee556ef9a7ea24ec
|
||||
size 4984
|
||||
oid sha256:e884fe9fd0c212caf0bfc745206ea277ff77037356e4e94b844e1cd1100a8861
|
||||
size 6955
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:43e08e7c243192571121595483ddc2dd44b56892afff5ae949ac224cad13e73f
|
||||
size 5165
|
||||
oid sha256:7abd5b98f55ca52bccd791196613029d2932520f062f08fd6356666b5fe1dbe8
|
||||
size 6878
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7c61846e0037aa7a4d6f146fa4f2b441654570db082b3f506bb6f49aa01f3999
|
||||
size 4944
|
||||
oid sha256:cfaed204b2cc5ee8d1ec54f84c446346c62e392ad2ae305359a552f5f9a427f1
|
||||
size 6755
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:137bfee9d776c8abff1a2428e3edc1c28cc7ca21d9849640e2aad2d945d7f7f2
|
||||
size 5361
|
||||
oid sha256:a801ced0c93a02e5fa22ca021f78d3ab1834169745353aaacbaad4bc41bbb112
|
||||
size 6834
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bdf7fe891aa40f4d626733deb130481d64b3315531712128d1b4073e5ccedf19
|
||||
size 5394
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0baf33f0ec99cd8e4b201d72b38208a02b0511da544276a38d17ff5653e2b754
|
||||
size 5906
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c8d9dc01ec07f9e43032e3ec7610eda5588145f3003cc3d329ddf4325e19f239
|
||||
size 6674
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5e0f86449d651c50966fe594a46e3a59ae9a5505013e3e36f2458be35ba1844e
|
||||
size 7174
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:621facc4799c0069fe83b4be9a5085ecafb042de1cdfbbf4ccf0c15548373aa9
|
||||
size 5332
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:986f40750d69639e771c9bf1aac30c49dbf122aa60dc0280164a8868671b638a
|
||||
size 5910
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6d4fb8d68603309963d434f3166821055d818836b1ebfb4fc4c637aba992277d
|
||||
size 6617
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1fa641ca8d5c796c858b54ad997052aabf817bc86e254d38d4b33038bc21eedf
|
||||
size 7285
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0e7e30c460d75815023ed02cd6b0fcff4a15881916f0056e47d9fd034bf3a181
|
||||
size 41125
|
||||
oid sha256:121e9eb7013123e5647e351eda7d331ade8032ae5aa56bd1be943ee6c6acad53
|
||||
size 41444
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:81b126a3d322d70f06db15622e560a146cea2c81f5873987550aba0a5c5efffa
|
||||
size 53314
|
||||
oid sha256:e19a24fa27305822af8ec3a4dfac727eb1297ccbbeffec726664c6bc29b1a2a4
|
||||
size 53421
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b7e8b8ff0aeeb517c47dfd987579b73a7e862e9a67a5100532f39087d42a4fe4
|
||||
size 46056
|
||||
oid sha256:9ab84b59725483b7187e91f3df18d1d55decbef7da37e186099027c6c98e3430
|
||||
size 46068
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b0ede4470399e2c7e6f3c99e59191a857c1e14254789307777359802980c59a1
|
||||
size 193735
|
||||
oid sha256:5b388b6664ea0e5a86d6506a954cbcdc36c39054204739365eb0228b2e1d4fd4
|
||||
size 197778
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c9fe22528aad68a5dffc8489fd1ec3138a8edde44fb23a1391f6e53a07d44fff
|
||||
size 193966
|
||||
oid sha256:dbeca10cc6a489dd20390bbc3bf5f2fa4f726342083621b8161a2a4448622ffc
|
||||
size 198122
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d559a454af6dfe367d1256dcd7ceeb32ae6fe168b76b1b5d8860d328e9752fb3
|
||||
size 51433
|
||||
oid sha256:23ed23fbc4a47152c4d9b6373111c7de5497900e9ccb3382f99364bde2444032
|
||||
size 51724
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1e45ec17c6353f96f0f777f8c638da010dbd062c680175d8ae80f6e700a6f928
|
||||
size 72941
|
||||
oid sha256:8a715939660ec1a2bea4c5a3e9d0a3b4b0bc45f137fae4c3210476145346a3a9
|
||||
size 73507
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:572d6f11354e2b1f02cb0979b35e2c798abe2d3b63f852ca6b868471ceecaed0
|
||||
size 42964
|
||||
oid sha256:f3094dab56dd2c46a5c34162e717afcf674823be080959ec5e852227946237ff
|
||||
size 42954
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b25f0497c08515125896756c89c5bc9e1757b25ec50478cd5cb67cdfd00c2dec
|
||||
size 55019
|
||||
oid sha256:44534d8513114e666e7faa341a81184420fd84e8c3723c2d17236949f31e2fdb
|
||||
size 55118
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3f22479bb2c6bd35c6f927c11ff19e644226df3d2a99b9c6b23f5bd679178d5a
|
||||
size 39151
|
||||
oid sha256:f988cd33ae74aa292f26f0c6f11b5fcec8ff298005423161921e912a5e416c97
|
||||
size 39341
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6920b50647111a70215c74ca27be736d6c208c0801eb4bcd323abcb33e903aea
|
||||
size 56229
|
||||
oid sha256:9a65842cd12943594ed28ffba0a958ff076d6c6492533bfc5ffd395b915b2b30
|
||||
size 56467
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a01bde6db3ce00e84e9bb5d6a8179c8d97af9dde939f6e31aa81c955dbdbf3a3
|
||||
size 40586
|
||||
oid sha256:f7822ba6ecc9e7cf7bfdb4446b6796dce685d2417f6ed29b6ef5015438e09ace
|
||||
size 40935
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3f12a13dd0fb3b569ce332ebc4e00fb55a6402bbf2b2a927b02abb4056c5e5c3
|
||||
size 53043
|
||||
oid sha256:d3d8fae3d49c61850eef15c762d1f9615a735ca1943e78aa4dc116eaa830c02a
|
||||
size 53182
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c50bb17bf530de6472d44232b0248c2a5761c67fde785b3aee6eb3f77fa4103e
|
||||
size 45948
|
||||
oid sha256:ebc4e13d35c918274f35445db57955780bfdc037d5bd85a98eddb9b3bac42d5e
|
||||
size 45846
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c0f6c785a1e20160355c72356744dedba65bda3d453fbd9772390350eaf6cc6e
|
||||
size 195504
|
||||
oid sha256:ed3f266ce286216deca3747ded2b46bd7ef37c707145d1ae676e8afe227850b2
|
||||
size 199651
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:37c702936170a0e4b37d608cf62b92c3b60b93950d4e9f32633af77e3ce4fdc9
|
||||
size 195775
|
||||
oid sha256:685a607ef2c644b8844a1a83bc9427040f3890b4b9f777aa80592c98f0a96d87
|
||||
size 199963
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e0b885810087916d3fefe699e5abc5125138ad74c6377f560e010e7fe68892da
|
||||
size 51388
|
||||
oid sha256:d5975a89ee4cf263317079be95b6f6cc199bfbbcf75ac88d622989bfd859c813
|
||||
size 51801
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:adcb71f4b9639287beabf35c9e91837bebee8f6b070fa77c0e6520fdf24faaae
|
||||
size 73679
|
||||
oid sha256:0375a01da0c470b8379d2edcfb282b4b55f5cee86dc9da3efbe66fbadf7019a5
|
||||
size 74079
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cb24bbba2e17417f4f3459353ee2c762517acfae2eada90c47b559828bad16a0
|
||||
size 42411
|
||||
oid sha256:f085dfb5b0147374af21b8df3f4cd2b9930e638188add6f3a4fa94008747c7ee
|
||||
size 42553
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d75c877c55c0f622aabee5ed8428b8a980cbc28daacb2127f2a17cc29db62a9d
|
||||
size 55057
|
||||
oid sha256:7ec8c64bb4500bce08725d74753f45db93a369095626064ecdf9a83122aea326
|
||||
size 55285
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a6edb33e04054aba49ea978d6b2c0d0c25332c4dd901cf2cadd6c368076e957d
|
||||
size 38687
|
||||
oid sha256:54046d58d89dd8f44e8e2fdb83306682fd6305a32ba91c06c9bcd042e99bbdd3
|
||||
size 38928
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:996016191220540fde3d8aaa3124a074a6eaaf6e87c43bf0d57d32992faf47fc
|
||||
size 56342
|
||||
oid sha256:a96004c7012bd1b83eb395511472a6c93f410f9504656f4e3621d7809eccca9f
|
||||
size 56483
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9c2c642f1595f503a1deb1c241f14543d7f15221baa1d0937595aa023f36a874
|
||||
size 44765
|
||||
oid sha256:49759fbb46a966d0e0f1c793545e320e7588ac482399a12ac45762474e6781f8
|
||||
size 45579
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1afd2c42012017f154fdf031d4a5fc7384fa5f239b56cf4567f42fd579806c9a
|
||||
size 45363
|
||||
oid sha256:cdca9e94c071c20753aa2076f6c3a7ed931a2c1e90a78c29e00f27f7af99dfe8
|
||||
size 45758
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9481f2994885a7649aa54d1c0043c44fe390004a7b58db4fc9d0e3e70bfc258a
|
||||
size 43537
|
||||
oid sha256:4e47966820a6a77efece51aaac5adbe3dd1dad1880b1feb7da2e86e36b386f7e
|
||||
size 43815
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:220b9ac66b74724887d7b0bbc082b4d0240d9fc25a068d33491eb83fe76e45fd
|
||||
size 43958
|
||||
oid sha256:bc3fc4c97a43c5786a75328e35828ed0e0cfd74ffab673832053ecc11db9b5e0
|
||||
size 44852
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9aa83ed14fb23495e79500a1e7d9dee8215efe67eec89e65e382d4f695eac68b
|
||||
size 44871
|
||||
oid sha256:7f82a51b6b6ed4ac030c4602da9da2629010169e998aaba16f9e05c652f6010d
|
||||
size 45283
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:aa4970f995c6ee618be3d868ff5a34442352a7f4493cad345093b889daf0d4c2
|
||||
size 42042
|
||||
oid sha256:5d18843f61267ae7385c23113be5c5c3d4376d95a843eef126ed670877f69b80
|
||||
size 42281
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue