Merge pull request #3054 from element-hq/feature/fga/sending_queue_iteration
Feature/fga/sending queue iteration
This commit is contained in:
@@ -24,14 +24,14 @@ import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@VisibleForTesting
|
||||
const val SEND_QUEUES_RETRY_DELAY_MILLIS = 1500L
|
||||
const val SEND_QUEUES_RETRY_DELAY_MILLIS = 500L
|
||||
|
||||
@SingleIn(SessionScope::class)
|
||||
class SendQueues @Inject constructor(
|
||||
@@ -45,14 +45,12 @@ class SendQueues @Inject constructor(
|
||||
}
|
||||
.launchIn(coroutineScope)
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
matrixClient.sendQueueDisabledFlow()
|
||||
.onEach { roomId: RoomId ->
|
||||
Timber.d("Send queue disabled for room $roomId")
|
||||
.debounce(SEND_QUEUES_RETRY_DELAY_MILLIS)
|
||||
.onEach { _: RoomId ->
|
||||
if (networkMonitor.connectivity.value == NetworkStatus.Online) {
|
||||
delay(SEND_QUEUES_RETRY_DELAY_MILLIS)
|
||||
matrixClient.getRoom(roomId)?.use { room ->
|
||||
room.setSendQueueEnabled(enabled = true)
|
||||
}
|
||||
matrixClient.setAllSendQueuesEnabled(enabled = true)
|
||||
}
|
||||
}
|
||||
.launchIn(coroutineScope)
|
||||
|
||||
@@ -55,12 +55,13 @@ import org.junit.Test
|
||||
runCurrent()
|
||||
|
||||
assert(setAllSendQueuesEnabledLambda)
|
||||
.isCalledOnce()
|
||||
.with(value(true))
|
||||
.isCalledExactly(2)
|
||||
.withSequence(
|
||||
listOf(value(true)),
|
||||
listOf(value(true)),
|
||||
)
|
||||
|
||||
assert(setRoomSendQueueEnabledLambda)
|
||||
.isCalledOnce()
|
||||
.with(value(true))
|
||||
assert(setRoomSendQueueEnabledLambda).isNeverCalled()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -104,7 +104,9 @@ class ActionListPresenter @Inject constructor(
|
||||
is TimelineItemStateContent -> {
|
||||
buildList {
|
||||
add(TimelineItemAction.Copy)
|
||||
add(TimelineItemAction.CopyLink)
|
||||
if (timelineItem.isRemote) {
|
||||
add(TimelineItemAction.CopyLink)
|
||||
}
|
||||
if (isDeveloperModeEnabled) {
|
||||
add(TimelineItemAction.ViewSource)
|
||||
}
|
||||
@@ -128,7 +130,9 @@ class ActionListPresenter @Inject constructor(
|
||||
if (timelineItem.content.canBeCopied()) {
|
||||
add(TimelineItemAction.Copy)
|
||||
}
|
||||
add(TimelineItemAction.CopyLink)
|
||||
if (timelineItem.isRemote) {
|
||||
add(TimelineItemAction.CopyLink)
|
||||
}
|
||||
if (isDeveloperModeEnabled) {
|
||||
add(TimelineItemAction.ViewSource)
|
||||
}
|
||||
@@ -145,8 +149,8 @@ class ActionListPresenter @Inject constructor(
|
||||
if (timelineItem.isRemote) {
|
||||
add(TimelineItemAction.Reply)
|
||||
add(TimelineItemAction.Forward)
|
||||
add(TimelineItemAction.CopyLink)
|
||||
}
|
||||
add(TimelineItemAction.CopyLink)
|
||||
if (isDeveloperModeEnabled) {
|
||||
add(TimelineItemAction.ViewSource)
|
||||
}
|
||||
@@ -187,7 +191,9 @@ class ActionListPresenter @Inject constructor(
|
||||
if (timelineItem.content.canBeCopied()) {
|
||||
add(TimelineItemAction.Copy)
|
||||
}
|
||||
add(TimelineItemAction.CopyLink)
|
||||
if (timelineItem.isRemote) {
|
||||
add(TimelineItemAction.CopyLink)
|
||||
}
|
||||
if (isDeveloperModeEnabled) {
|
||||
add(TimelineItemAction.ViewSource)
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList
|
||||
isMine = false,
|
||||
content = content,
|
||||
groupPosition = TimelineItemGroupPosition.Middle,
|
||||
sendState = LocalEventSendState.SendingFailed("Message failed to send"),
|
||||
sendState = LocalEventSendState.SendingFailed.Unrecoverable("Message failed to send"),
|
||||
),
|
||||
aTimelineItemEvent(
|
||||
isMine = false,
|
||||
@@ -104,7 +104,7 @@ internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList
|
||||
isMine = true,
|
||||
content = content,
|
||||
groupPosition = TimelineItemGroupPosition.Middle,
|
||||
sendState = LocalEventSendState.SendingFailed("Message failed to send"),
|
||||
sendState = LocalEventSendState.SendingFailed.Unrecoverable("Message failed to send"),
|
||||
),
|
||||
aTimelineItemEvent(
|
||||
isMine = true,
|
||||
|
||||
@@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -29,45 +30,60 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.event.isEdited
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun TimelineEventTimestampView(
|
||||
formattedTime: String,
|
||||
isMessageEdited: Boolean,
|
||||
event: TimelineItem.Event,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val formattedTime = event.sentTime
|
||||
val hasUnrecoverableError = event.localSendState is LocalEventSendState.SendingFailed.Unrecoverable
|
||||
val isMessageEdited = event.content.isEdited()
|
||||
val tint = if (hasUnrecoverableError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.secondary
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(PaddingValues(start = TimelineEventTimestampViewDefaults.spacing))
|
||||
.then(modifier),
|
||||
.padding(PaddingValues(start = TimelineEventTimestampViewDefaults.spacing))
|
||||
.then(modifier),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (isMessageEdited) {
|
||||
Text(
|
||||
stringResource(CommonStrings.common_edited_suffix),
|
||||
style = ElementTheme.typography.fontBodyXsRegular,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
color = tint,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
}
|
||||
Text(
|
||||
formattedTime,
|
||||
style = ElementTheme.typography.fontBodyXsRegular,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
color = tint,
|
||||
)
|
||||
if (hasUnrecoverableError) {
|
||||
Spacer(modifier = Modifier.width(2.dp))
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Error(),
|
||||
contentDescription = stringResource(id = CommonStrings.common_sending_failed),
|
||||
tint = tint,
|
||||
modifier = Modifier.size(15.dp, 18.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun TimelineEventTimestampViewPreview(@PreviewParameter(TimelineItemEventForTimestampViewProvider::class) event: TimelineItem.Event) = ElementPreview {
|
||||
TimelineEventTimestampView(formattedTime = event.sentTime, isMessageEdited = event.content.isEdited())
|
||||
TimelineEventTimestampView(event = event)
|
||||
}
|
||||
|
||||
object TimelineEventTimestampViewDefaults {
|
||||
|
||||
@@ -26,13 +26,15 @@ class TimelineItemEventForTimestampViewProvider : PreviewParameterProvider<Timel
|
||||
override val values: Sequence<TimelineItem.Event>
|
||||
get() = sequenceOf(
|
||||
aTimelineItemEvent(),
|
||||
// Sending failed
|
||||
aTimelineItemEvent().copy(localSendState = LocalEventSendState.SendingFailed("AN_ERROR")),
|
||||
// Sending failed recoverable
|
||||
aTimelineItemEvent().copy(localSendState = LocalEventSendState.SendingFailed.Recoverable("AN_ERROR")),
|
||||
// Sending failed unrecoverable
|
||||
aTimelineItemEvent().copy(localSendState = LocalEventSendState.SendingFailed.Unrecoverable("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(
|
||||
localSendState = LocalEventSendState.SendingFailed("AN_ERROR"),
|
||||
localSendState = LocalEventSendState.SendingFailed.Unrecoverable("AN_ERROR"),
|
||||
content = aTimelineItemTextContent().copy(isEdited = true),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -92,7 +92,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.canBeRepliedTo
|
||||
import io.element.android.features.messages.impl.timeline.model.event.isEdited
|
||||
import io.element.android.features.messages.impl.timeline.model.eventId
|
||||
import io.element.android.features.messages.impl.timeline.model.metadata
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
|
||||
@@ -460,8 +459,7 @@ private fun MessageEventBubbleContent(
|
||||
Box(modifier, contentAlignment = Alignment.Center) {
|
||||
content {}
|
||||
TimelineEventTimestampView(
|
||||
formattedTime = event.sentTime,
|
||||
isMessageEdited = event.content.isEdited(),
|
||||
event = event,
|
||||
modifier = Modifier
|
||||
// Outer padding
|
||||
.padding(horizontal = 4.dp, vertical = 4.dp)
|
||||
@@ -481,8 +479,7 @@ private fun MessageEventBubbleContent(
|
||||
content = { content(this::onContentLayoutChange) },
|
||||
overlay = {
|
||||
TimelineEventTimestampView(
|
||||
formattedTime = event.sentTime,
|
||||
isMessageEdited = event.content.isEdited(),
|
||||
event = event,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
)
|
||||
@@ -492,8 +489,7 @@ private fun MessageEventBubbleContent(
|
||||
Column(modifier) {
|
||||
content {}
|
||||
TimelineEventTimestampView(
|
||||
formattedTime = event.sentTime,
|
||||
isMessageEdited = event.content.isEdited(),
|
||||
event = event,
|
||||
modifier = Modifier
|
||||
.align(Alignment.End)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
|
||||
@@ -81,8 +81,8 @@ fun TimelineItemReadReceiptView(
|
||||
}
|
||||
} else {
|
||||
when (state.sendState) {
|
||||
is LocalEventSendState.SendingFailed,
|
||||
is LocalEventSendState.NotSentYet -> {
|
||||
LocalEventSendState.NotSentYet,
|
||||
is LocalEventSendState.SendingFailed.Recoverable -> {
|
||||
ReadReceiptsRow(modifier) {
|
||||
Icon(
|
||||
modifier = Modifier.padding(2.dp),
|
||||
@@ -92,6 +92,9 @@ fun TimelineItemReadReceiptView(
|
||||
)
|
||||
}
|
||||
}
|
||||
is LocalEventSendState.SendingFailed.Unrecoverable -> {
|
||||
// Error? The timestamp is already displayed in red
|
||||
}
|
||||
null,
|
||||
is LocalEventSendState.Sent -> {
|
||||
if (state.isLastOutgoingMessage) {
|
||||
|
||||
@@ -615,7 +615,6 @@ class ActionListPresenterTest {
|
||||
actions = persistentListOf(
|
||||
TimelineItemAction.Edit,
|
||||
TimelineItemAction.Copy,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.Redact,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -22,11 +22,10 @@ import io.element.android.libraries.matrix.api.core.EventId
|
||||
@Immutable
|
||||
sealed interface LocalEventSendState {
|
||||
data object NotSentYet : LocalEventSendState
|
||||
|
||||
data class SendingFailed(
|
||||
val error: String
|
||||
) : LocalEventSendState
|
||||
|
||||
sealed class SendingFailed(open val error: String) : LocalEventSendState {
|
||||
data class Recoverable(override val error: String) : SendingFailed(error)
|
||||
data class Unrecoverable(override val error: String) : SendingFailed(error)
|
||||
}
|
||||
data class Sent(
|
||||
val eventId: EventId
|
||||
) : LocalEventSendState
|
||||
|
||||
@@ -77,7 +77,13 @@ fun RustEventSendState?.map(): LocalEventSendState? {
|
||||
return when (this) {
|
||||
null -> null
|
||||
RustEventSendState.NotSentYet -> LocalEventSendState.NotSentYet
|
||||
is RustEventSendState.SendingFailed -> LocalEventSendState.SendingFailed(error)
|
||||
is RustEventSendState.SendingFailed -> {
|
||||
if (this.isRecoverable) {
|
||||
LocalEventSendState.SendingFailed.Recoverable(this.error)
|
||||
} else {
|
||||
LocalEventSendState.SendingFailed.Unrecoverable(this.error)
|
||||
}
|
||||
}
|
||||
is RustEventSendState.Sent -> LocalEventSendState.Sent(EventId(eventId))
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user