Group some state events in the timeline
This commit is contained in:
parent
e886312ff6
commit
f0550e354a
18 changed files with 332 additions and 32 deletions
|
|
@ -63,6 +63,7 @@ import io.element.android.features.messages.impl.actionlist.model.TimelineItemAc
|
||||||
import io.element.android.features.messages.impl.textcomposer.AttachmentSourcePicker
|
import io.element.android.features.messages.impl.textcomposer.AttachmentSourcePicker
|
||||||
import io.element.android.features.messages.impl.textcomposer.MessageComposerEvents
|
import io.element.android.features.messages.impl.textcomposer.MessageComposerEvents
|
||||||
import io.element.android.features.messages.impl.textcomposer.MessageComposerView
|
import io.element.android.features.messages.impl.textcomposer.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.TimelineView
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
|
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
|
||||||
|
|
@ -139,6 +140,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) {
|
fun onActionSelected(action: TimelineItemAction, event: TimelineItem.Event) {
|
||||||
state.eventSink(MessagesEvents.HandleAction(action, event))
|
state.eventSink(MessagesEvents.HandleAction(action, event))
|
||||||
}
|
}
|
||||||
|
|
@ -189,7 +195,8 @@ fun MessagesView(
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.consumeWindowInsets(padding),
|
.consumeWindowInsets(padding),
|
||||||
onMessageClicked = ::onMessageClicked,
|
onMessageClicked = ::onMessageClicked,
|
||||||
onMessageLongClicked = ::onMessageLongClicked
|
onMessageLongClicked = ::onMessageLongClicked,
|
||||||
|
onExpandGroupClick = ::onExpandGroupClick,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
snackbarHost = {
|
snackbarHost = {
|
||||||
|
|
@ -214,6 +221,7 @@ fun MessagesViewContent(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onMessageClicked: (TimelineItem.Event) -> Unit = {},
|
onMessageClicked: (TimelineItem.Event) -> Unit = {},
|
||||||
onMessageLongClicked: (TimelineItem.Event) -> Unit = {},
|
onMessageLongClicked: (TimelineItem.Event) -> Unit = {},
|
||||||
|
onExpandGroupClick: (TimelineItem.GroupedEvents) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
|
@ -227,7 +235,8 @@ fun MessagesViewContent(
|
||||||
state = state.timelineState,
|
state = state.timelineState,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
onMessageClicked = onMessageClicked,
|
onMessageClicked = onMessageClicked,
|
||||||
onMessageLongClicked = onMessageLongClicked
|
onMessageLongClicked = onMessageLongClicked,
|
||||||
|
onExpandGroupClick = onExpandGroupClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
MessageComposerView(
|
MessageComposerView(
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,11 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline
|
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
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
||||||
sealed interface TimelineEvents {
|
sealed interface TimelineEvents {
|
||||||
object LoadMore : TimelineEvents
|
object LoadMore : TimelineEvents
|
||||||
data class SetHighlightedEvent(val eventId: EventId?) : 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
|
package io.element.android.features.messages.impl.timeline
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
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.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.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||||
|
|
@ -42,6 +45,7 @@ private const val backPaginationPageSize = 50
|
||||||
|
|
||||||
class TimelinePresenter @Inject constructor(
|
class TimelinePresenter @Inject constructor(
|
||||||
private val timelineItemsFactory: TimelineItemsFactory,
|
private val timelineItemsFactory: TimelineItemsFactory,
|
||||||
|
private val timelineItemGrouper: TimelineItemGrouper,
|
||||||
room: MatrixRoom,
|
room: MatrixRoom,
|
||||||
) : Presenter<TimelineState> {
|
) : Presenter<TimelineState> {
|
||||||
|
|
||||||
|
|
@ -53,6 +57,8 @@ class TimelinePresenter @Inject constructor(
|
||||||
val highlightedEventId: MutableState<EventId?> = rememberSaveable {
|
val highlightedEventId: MutableState<EventId?> = rememberSaveable {
|
||||||
mutableStateOf(null)
|
mutableStateOf(null)
|
||||||
}
|
}
|
||||||
|
val expandedGroups = remember { mutableStateMapOf<String, Boolean>() }
|
||||||
|
|
||||||
val timelineItems = timelineItemsFactory
|
val timelineItems = timelineItemsFactory
|
||||||
.flow()
|
.flow()
|
||||||
.collectAsState()
|
.collectAsState()
|
||||||
|
|
@ -65,6 +71,9 @@ class TimelinePresenter @Inject constructor(
|
||||||
when (event) {
|
when (event) {
|
||||||
TimelineEvents.LoadMore -> localCoroutineScope.loadMore(paginationState.value)
|
TimelineEvents.LoadMore -> localCoroutineScope.loadMore(paginationState.value)
|
||||||
is TimelineEvents.SetHighlightedEvent -> highlightedEventId.value = event.eventId
|
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(
|
return TimelineState(
|
||||||
highlightedEventId = highlightedEventId.value,
|
highlightedEventId = highlightedEventId.value,
|
||||||
paginationState = paginationState.value,
|
paginationState = paginationState.value,
|
||||||
timelineItems = timelineItems.value.toImmutableList(),
|
timelineItems = timelineItemGrouper.group(timelineItems.value, expandedGroups).toImmutableList(),
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,14 +50,17 @@ import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.layout.LastBaseline
|
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.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.zIndex
|
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.MessageEventBubble
|
||||||
import io.element.android.features.messages.impl.timeline.components.MessageStateEventContainer
|
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.TimelineItemReactionsView
|
||||||
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
|
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.TimelineItemDaySeparatorView
|
||||||
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineLoadingMoreIndicator
|
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.TimelineItem
|
||||||
|
|
@ -84,6 +87,7 @@ fun TimelineView(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onMessageClicked: (TimelineItem.Event) -> Unit = {},
|
onMessageClicked: (TimelineItem.Event) -> Unit = {},
|
||||||
onMessageLongClicked: (TimelineItem.Event) -> Unit = {},
|
onMessageLongClicked: (TimelineItem.Event) -> Unit = {},
|
||||||
|
onExpandGroupClick: (TimelineItem.GroupedEvents) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun onReachedLoadMore() {
|
fun onReachedLoadMore() {
|
||||||
|
|
@ -106,9 +110,10 @@ fun TimelineView(
|
||||||
) { index, timelineItem ->
|
) { index, timelineItem ->
|
||||||
TimelineItemRow(
|
TimelineItemRow(
|
||||||
timelineItem = timelineItem,
|
timelineItem = timelineItem,
|
||||||
isHighlighted = timelineItem.identifier() == state.highlightedEventId?.value,
|
highlightedItem = state.highlightedEventId?.value,
|
||||||
onClick = onMessageClicked,
|
onClick = onMessageClicked,
|
||||||
onLongClick = onMessageLongClicked
|
onLongClick = onMessageLongClicked,
|
||||||
|
onExpandGroupClick = onExpandGroupClick,
|
||||||
)
|
)
|
||||||
if (index == state.timelineItems.lastIndex) {
|
if (index == state.timelineItems.lastIndex) {
|
||||||
onReachedLoadMore()
|
onReachedLoadMore()
|
||||||
|
|
@ -127,9 +132,10 @@ fun TimelineView(
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemRow(
|
fun TimelineItemRow(
|
||||||
timelineItem: TimelineItem,
|
timelineItem: TimelineItem,
|
||||||
isHighlighted: Boolean,
|
highlightedItem: String?,
|
||||||
onClick: (TimelineItem.Event) -> Unit,
|
onClick: (TimelineItem.Event) -> Unit,
|
||||||
onLongClick: (TimelineItem.Event) -> Unit,
|
onLongClick: (TimelineItem.Event) -> Unit,
|
||||||
|
onExpandGroupClick: (TimelineItem.GroupedEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
when (timelineItem) {
|
when (timelineItem) {
|
||||||
is TimelineItem.Virtual -> {
|
is TimelineItem.Virtual -> {
|
||||||
|
|
@ -149,19 +155,48 @@ fun TimelineItemRow(
|
||||||
if (timelineItem.content is TimelineItemStateContent) {
|
if (timelineItem.content is TimelineItemStateContent) {
|
||||||
TimelineItemStateEventRow(
|
TimelineItemStateEventRow(
|
||||||
event = timelineItem,
|
event = timelineItem,
|
||||||
isHighlighted = isHighlighted,
|
isHighlighted = highlightedItem == timelineItem.identifier(),
|
||||||
onClick = ::onClick,
|
onClick = ::onClick,
|
||||||
onLongClick = ::onLongClick
|
onLongClick = ::onLongClick
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
TimelineItemEventRow(
|
TimelineItemEventRow(
|
||||||
event = timelineItem,
|
event = timelineItem,
|
||||||
isHighlighted = isHighlighted,
|
isHighlighted = highlightedItem == timelineItem.identifier(),
|
||||||
onClick = ::onClick,
|
onClick = ::onClick,
|
||||||
onLongClick = ::onLongClick
|
onLongClick = ::onLongClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is TimelineItem.GroupedEvents -> {
|
||||||
|
fun onExpandGroupClick() {
|
||||||
|
onExpandGroupClick(timelineItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timelineItem.expanded) {
|
||||||
|
Column {
|
||||||
|
timelineItem.events.forEach { subGroupEvent ->
|
||||||
|
TimelineItemRow(
|
||||||
|
timelineItem = subGroupEvent,
|
||||||
|
highlightedItem = highlightedItem,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
onExpandGroupClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.ArrowDropDown
|
||||||
|
import androidx.compose.material.icons.filled.ArrowRight
|
||||||
|
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.ArrowDropDown
|
||||||
|
} else {
|
||||||
|
Icons.Default.ArrowRight
|
||||||
|
}
|
||||||
|
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 = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new list of [TimelineItem] by grouping some of them into [TimelineItem.GroupedEvents].
|
||||||
|
*/
|
||||||
|
class TimelineItemGrouper @Inject constructor() {
|
||||||
|
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.
|
||||||
|
if (currentGroup.size == 1) {
|
||||||
|
// Do not create a group with just 1 item, just add the item to the result
|
||||||
|
result.add(currentGroup.first())
|
||||||
|
} else {
|
||||||
|
result.add(
|
||||||
|
TimelineItem.GroupedEvents(
|
||||||
|
expanded = expandedGroups[currentGroup.first().id + "_group"].orFalse(),
|
||||||
|
events = currentGroup.toImmutableList()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
currentGroup.clear()
|
||||||
|
}
|
||||||
|
result.add(timelineItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed interface TimelineItem {
|
sealed interface TimelineItem {
|
||||||
|
|
@ -29,11 +30,13 @@ sealed interface TimelineItem {
|
||||||
fun identifier(): String = when (this) {
|
fun identifier(): String = when (this) {
|
||||||
is Event -> id
|
is Event -> id
|
||||||
is Virtual -> id
|
is Virtual -> id
|
||||||
|
is GroupedEvents -> id
|
||||||
}
|
}
|
||||||
|
|
||||||
fun contentType(): String = when (this) {
|
fun contentType(): String = when (this) {
|
||||||
is Event -> content.type
|
is Event -> content.type
|
||||||
is Virtual -> model.type
|
is Virtual -> model.type
|
||||||
|
is GroupedEvents -> "groupedEvent"
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|
@ -60,4 +63,13 @@ sealed interface TimelineItem {
|
||||||
|
|
||||||
val safeSenderName: String = senderDisplayName ?: senderId.value
|
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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<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">"Camera"</string>
|
||||||
<string name="screen_room_attachment_source_camera_photo">"Take photo"</string>
|
<string name="screen_room_attachment_source_camera_photo">"Take photo"</string>
|
||||||
<string name="screen_room_attachment_source_camera_video">"Record a video"</string>
|
<string name="screen_room_attachment_source_camera_video">"Record a video"</string>
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
<string name="action_leave_room">"Raum verlassen"</string>
|
<string name="action_leave_room">"Raum verlassen"</string>
|
||||||
<string name="action_next">"Weiter"</string>
|
<string name="action_next">"Weiter"</string>
|
||||||
<string name="action_no">"Nein"</string>
|
<string name="action_no">"Nein"</string>
|
||||||
|
<string name="action_not_now">"Nicht jetzt"</string>
|
||||||
<string name="action_ok">"OK"</string>
|
<string name="action_ok">"OK"</string>
|
||||||
<string name="action_quick_reply">"Schnellantwort"</string>
|
<string name="action_quick_reply">"Schnellantwort"</string>
|
||||||
<string name="action_quote">"Zitieren"</string>
|
<string name="action_quote">"Zitieren"</string>
|
||||||
|
|
@ -41,10 +42,11 @@
|
||||||
<string name="action_share">"Teilen"</string>
|
<string name="action_share">"Teilen"</string>
|
||||||
<string name="action_share_link">"Link teilen"</string>
|
<string name="action_share_link">"Link teilen"</string>
|
||||||
<string name="action_skip">"Überspringen"</string>
|
<string name="action_skip">"Überspringen"</string>
|
||||||
|
<string name="action_start_chat">"Chat starten"</string>
|
||||||
<string name="action_take_photo">"Foto aufnehmen"</string>
|
<string name="action_take_photo">"Foto aufnehmen"</string>
|
||||||
<string name="action_yes">"Ja"</string>
|
<string name="action_yes">"Ja"</string>
|
||||||
<string name="common_about">"Über"</string>
|
<string name="common_about">"Über"</string>
|
||||||
<string name="common_analytics">"Analytik"</string>
|
<string name="common_analytics">"Analyse"</string>
|
||||||
<string name="common_audio">"Audio"</string>
|
<string name="common_audio">"Audio"</string>
|
||||||
<string name="common_bubbles">"Blasen"</string>
|
<string name="common_bubbles">"Blasen"</string>
|
||||||
<string name="common_decryption_error">"Entschlüsselungsfehler"</string>
|
<string name="common_decryption_error">"Entschlüsselungsfehler"</string>
|
||||||
|
|
@ -61,6 +63,7 @@
|
||||||
<string name="common_offline">"Offline"</string>
|
<string name="common_offline">"Offline"</string>
|
||||||
<string name="common_password">"Passwort"</string>
|
<string name="common_password">"Passwort"</string>
|
||||||
<string name="common_reactions">"Reaktionen"</string>
|
<string name="common_reactions">"Reaktionen"</string>
|
||||||
|
<string name="common_report_a_bug">"Fehler melden"</string>
|
||||||
<string name="common_search_results">"Suchergebnisse"</string>
|
<string name="common_search_results">"Suchergebnisse"</string>
|
||||||
<string name="common_security">"Sicherheit"</string>
|
<string name="common_security">"Sicherheit"</string>
|
||||||
<string name="common_server_not_supported">"Server wird nicht unterstützt"</string>
|
<string name="common_server_not_supported">"Server wird nicht unterstützt"</string>
|
||||||
|
|
@ -87,6 +90,7 @@
|
||||||
<string name="emoji_picker_category_places">"Reisen & Orte"</string>
|
<string name="emoji_picker_category_places">"Reisen & Orte"</string>
|
||||||
<string name="emoji_picker_category_symbols">"Symbole"</string>
|
<string name="emoji_picker_category_symbols">"Symbole"</string>
|
||||||
<string name="error_failed_loading_messages">"Fehler beim Laden der Nachrichten"</string>
|
<string name="error_failed_loading_messages">"Fehler beim Laden der Nachrichten"</string>
|
||||||
|
<string name="error_some_messages_have_not_been_sent">"Einige Nachrichten wurden nicht gesendet"</string>
|
||||||
<string name="error_unknown">"Entschuldigung, ein Fehler ist aufgetreten."</string>
|
<string name="error_unknown">"Entschuldigung, ein Fehler ist aufgetreten."</string>
|
||||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||||
<plurals name="common_member_count">
|
<plurals name="common_member_count">
|
||||||
|
|
@ -96,6 +100,9 @@
|
||||||
<string name="report_content_hint">"Grund für die Meldung dieses Inhalts"</string>
|
<string name="report_content_hint">"Grund für die Meldung dieses Inhalts"</string>
|
||||||
<string name="room_timeline_beginning_of_room">"Dies ist der Anfang von %1$s."</string>
|
<string name="room_timeline_beginning_of_room">"Dies ist der Anfang von %1$s."</string>
|
||||||
<string name="room_timeline_read_marker_title">"Neu"</string>
|
<string name="room_timeline_read_marker_title">"Neu"</string>
|
||||||
|
<string name="screen_analytics_prompt_data_usage">"Wir erfassen und analysieren "<b>"keine"</b>" Account-Daten"</string>
|
||||||
|
<string name="screen_analytics_prompt_settings">"Sie können die Analyse jederzeit in den Einstellungen deaktivieren"</string>
|
||||||
|
<string name="screen_analytics_prompt_third_party_sharing">"Wir geben "<b>"keine"</b>" Informationen an Dritte weiter"</string>
|
||||||
<string name="screen_analytics_settings_share_data">"Teile Analyse-Daten"</string>
|
<string name="screen_analytics_settings_share_data">"Teile Analyse-Daten"</string>
|
||||||
<string name="screen_media_picker_error_failed_selection">"Medienauswahl fehlgeschlagen, bitte versuche es erneut."</string>
|
<string name="screen_media_picker_error_failed_selection">"Medienauswahl fehlgeschlagen, bitte versuche es erneut."</string>
|
||||||
<string name="settings_rageshake_detection_threshold">"Erkennungsschwelle"</string>
|
<string name="settings_rageshake_detection_threshold">"Erkennungsschwelle"</string>
|
||||||
|
|
|
||||||
|
|
@ -116,10 +116,6 @@
|
||||||
<item quantity="one">"%1$d miembro"</item>
|
<item quantity="one">"%1$d miembro"</item>
|
||||||
<item quantity="other">"%1$d miembros"</item>
|
<item quantity="other">"%1$d miembros"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<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>
|
|
||||||
<string name="preference_rageshake">"Agitar con fuerza para informar de un error"</string>
|
<string name="preference_rageshake">"Agitar con fuerza para informar de un error"</string>
|
||||||
<string name="rageshake_dialog_content">"Parece que sacudes el teléfono con frustración. ¿Quieres abrir la pantalla de informe de errores?"</string>
|
<string name="rageshake_dialog_content">"Parece que sacudes el teléfono con frustración. ¿Quieres abrir la pantalla de informe de errores?"</string>
|
||||||
<string name="report_content_explanation">"Este mensaje se notificará al administrador de su homeserver. No podrán leer ningún mensaje cifrado."</string>
|
<string name="report_content_explanation">"Este mensaje se notificará al administrador de su homeserver. No podrán leer ningún mensaje cifrado."</string>
|
||||||
|
|
|
||||||
|
|
@ -116,10 +116,6 @@
|
||||||
<item quantity="one">"%1$d membro"</item>
|
<item quantity="one">"%1$d membro"</item>
|
||||||
<item quantity="other">"%1$d membri"</item>
|
<item quantity="other">"%1$d membri"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<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>
|
|
||||||
<string name="preference_rageshake">"Scuoti per segnalare un problema"</string>
|
<string name="preference_rageshake">"Scuoti per segnalare un problema"</string>
|
||||||
<string name="rageshake_dialog_content">"Sembra che tu stia scuotendo il telefono per la frustrazione. Vuoi aprire la schermata di segnalazione dei problemi?"</string>
|
<string name="rageshake_dialog_content">"Sembra che tu stia scuotendo il telefono per la frustrazione. Vuoi aprire la schermata di segnalazione dei problemi?"</string>
|
||||||
<string name="report_content_explanation">"Questo messaggio verrà segnalato all\'amministratore dell\'homeserver. Questi non sarà in grado di leggere i messaggi criptati."</string>
|
<string name="report_content_explanation">"Questo messaggio verrà segnalato all\'amministratore dell\'homeserver. Questi non sarà in grado di leggere i messaggi criptati."</string>
|
||||||
|
|
|
||||||
|
|
@ -125,11 +125,6 @@
|
||||||
<item quantity="few">"%1$d membri"</item>
|
<item quantity="few">"%1$d membri"</item>
|
||||||
<item quantity="other">"%1$d membri"</item>
|
<item quantity="other">"%1$d membri"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<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>
|
|
||||||
<string name="preference_rageshake">"Rageshake pentru a raporta erori"</string>
|
<string name="preference_rageshake">"Rageshake pentru a raporta erori"</string>
|
||||||
<string name="rageshake_dialog_content">"Se pare că scuturați telefonul de frustrare. Doriți să deschdeți ecranul de raportare a unei erori?"</string>
|
<string name="rageshake_dialog_content">"Se pare că scuturați telefonul de frustrare. Doriți să deschdeți ecranul de raportare a unei erori?"</string>
|
||||||
<string name="report_content_explanation">"Acest mesaj va fi raportat administratorilor homeserver-ului tau. Ei nu vor putea citi niciun mesaj criptat."</string>
|
<string name="report_content_explanation">"Acest mesaj va fi raportat administratorilor homeserver-ului tau. Ei nu vor putea citi niciun mesaj criptat."</string>
|
||||||
|
|
|
||||||
|
|
@ -132,10 +132,6 @@
|
||||||
<item quantity="one">"%1$d member"</item>
|
<item quantity="one">"%1$d member"</item>
|
||||||
<item quantity="other">"%1$d members"</item>
|
<item quantity="other">"%1$d members"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<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="preference_rageshake">"Rageshake to report bug"</string>
|
<string name="preference_rageshake">"Rageshake to report bug"</string>
|
||||||
<string name="rageshake_dialog_content">"You seem to be shaking the phone in frustration. Would you like to open the bug report screen?"</string>
|
<string name="rageshake_dialog_content">"You seem to be shaking the phone in frustration. Would you like to open the bug report screen?"</string>
|
||||||
<string name="report_content_explanation">"This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages."</string>
|
<string name="report_content_explanation">"This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages."</string>
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": ":libraries:messageformatter:impl",
|
"name": ":libraries:eventformatter:impl",
|
||||||
"includeRegex": [
|
"includeRegex": [
|
||||||
"state_event_.*"
|
"state_event_.*"
|
||||||
]
|
]
|
||||||
|
|
@ -95,7 +95,8 @@
|
||||||
"name": ":features:messages:impl",
|
"name": ":features:messages:impl",
|
||||||
"includeRegex": [
|
"includeRegex": [
|
||||||
"screen_room_.*",
|
"screen_room_.*",
|
||||||
"screen_dm_details_.*"
|
"screen_dm_details_.*",
|
||||||
|
"room_timeline_state_changes"
|
||||||
],
|
],
|
||||||
"excludeRegex": [
|
"excludeRegex": [
|
||||||
"screen_room_details_.*",
|
"screen_room_details_.*",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue