Merge pull request #3023 from element-hq/feature/fga/update_rust_sdk_0.2.25

Feature/fga/update rust sdk 0.2.25
This commit is contained in:
ganfra 2024-06-13 14:07:10 +02:00 committed by GitHub
commit 5b18f9cddd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
184 changed files with 656 additions and 1047 deletions

View file

@ -46,7 +46,6 @@ import io.element.android.features.messages.impl.timeline.TimelineState
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryPresenter
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetPresenter
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
@ -107,7 +106,6 @@ class MessagesPresenter @AssistedInject constructor(
private val actionListPresenter: ActionListPresenter,
private val customReactionPresenter: CustomReactionPresenter,
private val reactionSummaryPresenter: ReactionSummaryPresenter,
private val retrySendMenuPresenter: RetrySendMenuPresenter,
private val readReceiptBottomSheetPresenter: ReadReceiptBottomSheetPresenter,
private val networkMonitor: NetworkMonitor,
private val snackbarDispatcher: SnackbarDispatcher,
@ -140,7 +138,6 @@ class MessagesPresenter @AssistedInject constructor(
val actionListState = actionListPresenter.present()
val customReactionState = customReactionPresenter.present()
val reactionSummaryState = reactionSummaryPresenter.present()
val retryState = retrySendMenuPresenter.present()
val readReceiptBottomSheetState = readReceiptBottomSheetPresenter.present()
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
@ -231,7 +228,6 @@ class MessagesPresenter @AssistedInject constructor(
actionListState = actionListState,
customReactionState = customReactionState,
reactionSummaryState = reactionSummaryState,
retrySendMenuState = retryState,
readReceiptBottomSheetState = readReceiptBottomSheetState,
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
snackbarMessage = snackbarMessage,
@ -309,11 +305,9 @@ class MessagesPresenter @AssistedInject constructor(
}
private suspend fun handleActionRedact(event: TimelineItem.Event) {
if (event.failedToSend) {
// If the message hasn't been sent yet, just cancel it
event.transactionId?.let { room.cancelSend(it) }
} else if (event.eventId != null) {
room.redactEvent(event.eventId)
timelineController.invokeOnCurrentTimeline {
redactEvent(eventId = event.eventId, transactionId = event.transactionId, reason = null)
.onFailure { Timber.e(it) }
}
}

View file

@ -23,7 +23,6 @@ import io.element.android.features.messages.impl.timeline.TimelineState
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
import io.element.android.features.messages.impl.typing.TypingNotificationState
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
import io.element.android.libraries.architecture.AsyncData
@ -47,7 +46,6 @@ data class MessagesState(
val actionListState: ActionListState,
val customReactionState: CustomReactionState,
val reactionSummaryState: ReactionSummaryState,
val retrySendMenuState: RetrySendMenuState,
val readReceiptBottomSheetState: ReadReceiptBottomSheetState,
val hasNetworkConnection: Boolean,
val snackbarMessage: SnackbarMessage?,

View file

@ -31,8 +31,6 @@ import io.element.android.features.messages.impl.timeline.components.reactionsum
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvents
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.aRetrySendMenuState
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.features.messages.impl.typing.aTypingNotificationState
@ -110,7 +108,6 @@ fun aMessagesState(
// Render a focused event for an event with sender information displayed
focusedEventIndex = 2,
),
retrySendMenuState: RetrySendMenuState = aRetrySendMenuState(),
readReceiptBottomSheetState: ReadReceiptBottomSheetState = aReadReceiptBottomSheetState(),
actionListState: ActionListState = anActionListState(),
customReactionState: CustomReactionState = aCustomReactionState(),
@ -132,7 +129,6 @@ fun aMessagesState(
voiceMessageComposerState = voiceMessageComposerState,
typingNotificationState = aTypingNotificationState(),
timelineState = timelineState,
retrySendMenuState = retrySendMenuState,
readReceiptBottomSheetState = readReceiptBottomSheetState,
actionListState = actionListState,
customReactionState = customReactionState,

View file

@ -74,8 +74,6 @@ import io.element.android.features.messages.impl.timeline.components.reactionsum
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryView
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheet
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvents
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuEvents
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMessageMenu
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerEvents
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessagePermissionRationaleDialog
@ -103,7 +101,6 @@ import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
import timber.log.Timber
@ -212,11 +209,6 @@ fun MessagesView(
onMessageLongClick = ::onMessageLongClick,
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,
onTimestampClick = { event ->
if (event.localSendState is LocalEventSendState.SendingFailed) {
state.retrySendMenuState.eventSink(RetrySendMenuEvents.EventSelected(event))
}
},
onReactionClick = ::onEmojiReactionClick,
onReactionLongClick = ::onEmojiReactionLongClick,
onMoreReactionsClick = ::onMoreReactionsClick,
@ -258,7 +250,6 @@ fun MessagesView(
)
ReactionSummaryView(state = state.reactionSummaryState)
RetrySendMessageMenu(state = state.retrySendMenuState)
ReadReceiptBottomSheet(
state = state.readReceiptBottomSheetState,
onUserDataClick = onUserDataClick,
@ -319,7 +310,6 @@ private fun MessagesViewContent(
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
onReadReceiptClick: (TimelineItem.Event) -> Unit,
onMessageLongClick: (TimelineItem.Event) -> Unit,
onTimestampClick: (TimelineItem.Event) -> Unit,
onSendLocationClick: () -> Unit,
onCreatePollClick: () -> Unit,
onJoinCallClick: () -> Unit,
@ -387,7 +377,6 @@ private fun MessagesViewContent(
onLinkClick = onLinkClick,
onMessageClick = onMessageClick,
onMessageLongClick = onMessageLongClick,
onTimestampClick = onTimestampClick,
onSwipeToReply = onSwipeToReply,
onReactionClick = onReactionClick,
onReactionLongClick = onReactionLongClick,

View file

@ -181,7 +181,7 @@ class ActionListPresenter @Inject constructor(
add(TimelineItemAction.Forward)
}
}
if (timelineItem.isMine && timelineItem.isTextMessage) {
if (timelineItem.isEditable) {
add(TimelineItemAction.Edit)
}
if (timelineItem.content.canBeCopied()) {

View file

@ -83,7 +83,6 @@ fun TimelineView(
onLinkClick: (String) -> Unit,
onMessageClick: (TimelineItem.Event) -> Unit,
onMessageLongClick: (TimelineItem.Event) -> Unit,
onTimestampClick: (TimelineItem.Event) -> Unit,
onSwipeToReply: (TimelineItem.Event) -> Unit,
onReactionClick: (emoji: String, TimelineItem.Event) -> Unit,
onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit,
@ -148,7 +147,6 @@ fun TimelineView(
onReactionLongClick = onReactionLongClick,
onMoreReactionsClick = onMoreReactionsClick,
onReadReceiptClick = onReadReceiptClick,
onTimestampClick = onTimestampClick,
eventSink = state.eventSink,
onSwipeToReply = onSwipeToReply,
onJoinCallClick = onJoinCallClick,
@ -245,8 +243,8 @@ private fun BoxScope.TimelineScrollHelper(
// Use inverse of canAutoScroll otherwise we might briefly see the before the scroll animation is triggered
isVisible = !canAutoScroll || forceJumpToBottomVisibility || !isLive,
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(end = 24.dp, bottom = 12.dp),
.align(Alignment.BottomEnd)
.padding(end = 24.dp, bottom = 12.dp),
onClick = { jumpToBottom() },
)
}
@ -273,8 +271,8 @@ private fun JumpToBottomButton(
) {
Icon(
modifier = Modifier
.size(24.dp)
.rotate(90f),
.size(24.dp)
.rotate(90f),
imageVector = CompoundIcons.ArrowRight(),
contentDescription = stringResource(id = CommonStrings.a11y_jump_to_bottom)
)
@ -301,7 +299,6 @@ internal fun TimelineViewPreview(
onLinkClick = {},
onMessageClick = {},
onMessageLongClick = {},
onTimestampClick = {},
onSwipeToReply = {},
onReactionClick = { _, _ -> },
onReactionLongClick = { _, _ -> },

View file

@ -45,6 +45,5 @@ internal fun ATimelineItemEventRow(
onMoreReactionsClick = {},
onReadReceiptClick = {},
onSwipeToReply = {},
onTimestampClick = {},
eventSink = {},
)

View file

@ -16,61 +16,34 @@
package io.element.android.features.messages.impl.timeline.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.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.material.ripple.rememberRipple
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TimelineEventTimestampView(
event: TimelineItem.Event,
onClick: () -> Unit,
onLongClick: () -> Unit,
formattedTime: String,
isMessageEdited: Boolean,
modifier: Modifier = Modifier,
) {
val formattedTime = event.sentTime
val hasMessageSendingFailed = event.localSendState is LocalEventSendState.SendingFailed
val isMessageEdited = event.content.isEdited()
val tint = if (hasMessageSendingFailed) MaterialTheme.colorScheme.error else null
val clickModifier = if (hasMessageSendingFailed) {
Modifier.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
indication = rememberRipple(bounded = false),
interactionSource = remember { MutableInteractionSource() }
)
} else {
Modifier
}
Row(
modifier = Modifier
.then(clickModifier)
// Add extra padding for touch target size
.padding(PaddingValues(start = TimelineEventTimestampViewDefaults.spacing))
.then(modifier),
verticalAlignment = Alignment.CenterVertically,
@ -79,35 +52,22 @@ fun TimelineEventTimestampView(
Text(
stringResource(CommonStrings.common_edited_suffix),
style = ElementTheme.typography.fontBodyXsRegular,
color = tint ?: MaterialTheme.colorScheme.secondary,
color = MaterialTheme.colorScheme.secondary,
)
Spacer(modifier = Modifier.width(4.dp))
}
Text(
formattedTime,
style = ElementTheme.typography.fontBodyXsRegular,
color = tint ?: MaterialTheme.colorScheme.secondary,
color = MaterialTheme.colorScheme.secondary,
)
if (hasMessageSendingFailed && tint != null) {
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(
event = event,
onClick = {},
onLongClick = {},
)
TimelineEventTimestampView(formattedTime = event.sentTime, isMessageEdited = event.content.isEdited())
}
object TimelineEventTimestampViewDefaults {

View file

@ -92,6 +92,7 @@ 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
@ -130,7 +131,6 @@ fun TimelineItemEventRow(
onLinkClick: (String) -> Unit,
onUserDataClick: (UserId) -> Unit,
inReplyToClick: (EventId) -> Unit,
onTimestampClick: (TimelineItem.Event) -> Unit,
onReactionClick: (emoji: String, eventId: TimelineItem.Event) -> Unit,
onReactionLongClick: (emoji: String, eventId: TimelineItem.Event) -> Unit,
onMoreReactionsClick: (eventId: TimelineItem.Event) -> Unit,
@ -190,7 +190,6 @@ fun TimelineItemEventRow(
interactionSource = interactionSource,
onClick = onClick,
onLongClick = onLongClick,
onTimestampClick = onTimestampClick,
inReplyToClick = ::inReplyToClick,
onUserDataClick = ::onUserDataClick,
onReactionClick = { emoji -> onReactionClick(emoji, event) },
@ -209,7 +208,6 @@ fun TimelineItemEventRow(
interactionSource = interactionSource,
onClick = onClick,
onLongClick = onLongClick,
onTimestampClick = onTimestampClick,
inReplyToClick = ::inReplyToClick,
onUserDataClick = ::onUserDataClick,
onReactionClick = { emoji -> onReactionClick(emoji, event) },
@ -265,7 +263,6 @@ private fun TimelineItemEventRowContent(
interactionSource: MutableInteractionSource,
onClick: () -> Unit,
onLongClick: () -> Unit,
onTimestampClick: (TimelineItem.Event) -> Unit,
inReplyToClick: () -> Unit,
onUserDataClick: () -> Unit,
onReactionClick: (emoji: String) -> Unit,
@ -337,9 +334,6 @@ private fun TimelineItemEventRowContent(
event = event,
onMessageLongClick = onLongClick,
inReplyToClick = inReplyToClick,
onTimestampClick = {
onTimestampClick(event)
},
onLinkClick = onLinkClick,
eventSink = eventSink,
)
@ -419,7 +413,6 @@ private fun MessageEventBubbleContent(
event: TimelineItem.Event,
onMessageLongClick: () -> Unit,
inReplyToClick: () -> Unit,
onTimestampClick: () -> Unit,
onLinkClick: (String) -> Unit,
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
@SuppressLint("ModifierParameter")
@ -467,9 +460,8 @@ private fun MessageEventBubbleContent(
Box(modifier, contentAlignment = Alignment.Center) {
content {}
TimelineEventTimestampView(
event = event,
onClick = onTimestampClick,
onLongClick = ::onTimestampLongClick,
formattedTime = event.sentTime,
isMessageEdited = event.content.isEdited(),
modifier = Modifier
// Outer padding
.padding(horizontal = 4.dp, vertical = 4.dp)
@ -489,9 +481,8 @@ private fun MessageEventBubbleContent(
content = { content(this::onContentLayoutChange) },
overlay = {
TimelineEventTimestampView(
event = event,
onClick = onTimestampClick,
onLongClick = ::onTimestampLongClick,
formattedTime = event.sentTime,
isMessageEdited = event.content.isEdited(),
modifier = Modifier
.padding(horizontal = 8.dp, vertical = 4.dp)
)
@ -501,9 +492,8 @@ private fun MessageEventBubbleContent(
Column(modifier) {
content {}
TimelineEventTimestampView(
event = event,
onClick = onTimestampClick,
onLongClick = ::onTimestampLongClick,
formattedTime = event.sentTime,
isMessageEdited = event.content.isEdited(),
modifier = Modifier
.align(Alignment.End)
.padding(horizontal = 8.dp, vertical = 4.dp)

View file

@ -49,7 +49,6 @@ fun TimelineItemGroupedEventsRow(
inReplyToClick: (EventId) -> Unit,
onUserDataClick: (UserId) -> Unit,
onLinkClick: (String) -> Unit,
onTimestampClick: (TimelineItem.Event) -> Unit,
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
@ -76,7 +75,6 @@ fun TimelineItemGroupedEventsRow(
inReplyToClick = inReplyToClick,
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,
onTimestampClick = onTimestampClick,
onReactionClick = onReactionClick,
onReactionLongClick = onReactionLongClick,
onMoreReactionsClick = onMoreReactionsClick,
@ -100,7 +98,6 @@ private fun TimelineItemGroupedEventsRowContent(
inReplyToClick: (EventId) -> Unit,
onUserDataClick: (UserId) -> Unit,
onLinkClick: (String) -> Unit,
onTimestampClick: (TimelineItem.Event) -> Unit,
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
@ -133,7 +130,6 @@ private fun TimelineItemGroupedEventsRowContent(
inReplyToClick = inReplyToClick,
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,
onTimestampClick = onTimestampClick,
onReactionClick = onReactionClick,
onReactionLongClick = onReactionLongClick,
onMoreReactionsClick = onMoreReactionsClick,
@ -175,7 +171,6 @@ internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPrevi
inReplyToClick = {},
onUserDataClick = {},
onLinkClick = {},
onTimestampClick = {},
onReactionClick = { _, _ -> },
onReactionLongClick = { _, _ -> },
onMoreReactionsClick = {},
@ -200,7 +195,6 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi
inReplyToClick = {},
onUserDataClick = {},
onLinkClick = {},
onTimestampClick = {},
onReactionClick = { _, _ -> },
onReactionLongClick = { _, _ -> },
onMoreReactionsClick = {},

View file

@ -54,7 +54,6 @@ internal fun TimelineItemRow(
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
onReadReceiptClick: (TimelineItem.Event) -> Unit,
onTimestampClick: (TimelineItem.Event) -> Unit,
onSwipeToReply: (TimelineItem.Event) -> Unit,
onJoinCallClick: () -> Unit,
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
@ -118,7 +117,6 @@ internal fun TimelineItemRow(
onReactionLongClick = onReactionLongClick,
onMoreReactionsClick = onMoreReactionsClick,
onReadReceiptClick = onReadReceiptClick,
onTimestampClick = onTimestampClick,
onSwipeToReply = { onSwipeToReply(timelineItem) },
eventSink = eventSink,
)
@ -137,7 +135,6 @@ internal fun TimelineItemRow(
inReplyToClick = inReplyToClick,
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,
onTimestampClick = onTimestampClick,
onReactionClick = onReactionClick,
onReactionLongClick = onReactionLongClick,
onMoreReactionsClick = onMoreReactionsClick,

View file

@ -81,7 +81,8 @@ fun TimelineItemReadReceiptView(
}
} else {
when (state.sendState) {
LocalEventSendState.NotSentYet -> {
is LocalEventSendState.SendingFailed,
is LocalEventSendState.NotSentYet -> {
ReadReceiptsRow(modifier) {
Icon(
modifier = Modifier.padding(2.dp),
@ -91,10 +92,6 @@ fun TimelineItemReadReceiptView(
)
}
}
LocalEventSendState.Canceled -> Unit
is LocalEventSendState.SendingFailed -> {
// Error? The timestamp is already displayed in red
}
null,
is LocalEventSendState.Sent -> {
if (state.isLastOutgoingMessage) {

View file

@ -123,7 +123,6 @@ private fun ReadReceiptBottomSheetContent(
@PreviewsDayNight
@Composable
internal fun ReadReceiptBottomSheetPreview(@PreviewParameter(ReadReceiptBottomSheetStateProvider::class) state: ReadReceiptBottomSheetState) = ElementPreview {
// TODO restore RetrySendMessageMenuBottomSheet once the issue with bottom sheet not being previewable is fixed
Column {
ReadReceiptBottomSheetContent(
state = state,

View file

@ -1,26 +0,0 @@
/*
* 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.retrysendmenu
import io.element.android.features.messages.impl.timeline.model.TimelineItem
sealed interface RetrySendMenuEvents {
data class EventSelected(val event: TimelineItem.Event) : RetrySendMenuEvents
data object Retry : RetrySendMenuEvents
data object Remove : RetrySendMenuEvents
data object Dismiss : RetrySendMenuEvents
}

View file

@ -1,71 +0,0 @@
/*
* 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.retrysendmenu
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.room.MatrixRoom
import kotlinx.coroutines.launch
import javax.inject.Inject
class RetrySendMenuPresenter @Inject constructor(
private val room: MatrixRoom,
) : Presenter<RetrySendMenuState> {
@Composable
override fun present(): RetrySendMenuState {
val coroutineScope = rememberCoroutineScope()
var selectedEvent: TimelineItem.Event? by remember { mutableStateOf(null) }
fun handleEvent(event: RetrySendMenuEvents) {
when (event) {
is RetrySendMenuEvents.EventSelected -> {
selectedEvent = event.event
}
RetrySendMenuEvents.Retry -> {
coroutineScope.launch {
selectedEvent?.transactionId?.let { transactionId ->
room.retrySendMessage(transactionId)
}
selectedEvent = null
}
}
RetrySendMenuEvents.Remove -> {
coroutineScope.launch {
selectedEvent?.transactionId?.let { transactionId ->
room.cancelSend(transactionId)
}
selectedEvent = null
}
}
RetrySendMenuEvents.Dismiss -> {
selectedEvent = null
}
}
}
return RetrySendMenuState(
selectedEvent = selectedEvent,
eventSink = { handleEvent(it) },
)
}
}

View file

@ -1,26 +0,0 @@
/*
* 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.retrysendmenu
import androidx.compose.runtime.Immutable
import io.element.android.features.messages.impl.timeline.model.TimelineItem
@Immutable
data class RetrySendMenuState(
val selectedEvent: TimelineItem.Event?,
val eventSink: (RetrySendMenuEvents) -> Unit,
)

View file

@ -1,36 +0,0 @@
/*
* 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.retrysendmenu
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
class RetrySendMenuStateProvider : PreviewParameterProvider<RetrySendMenuState> {
override val values: Sequence<RetrySendMenuState> = sequenceOf(
aRetrySendMenuState(),
aRetrySendMenuState(event = aTimelineItemEvent()),
)
}
fun aRetrySendMenuState(
event: TimelineItem.Event? = null,
eventSink: (RetrySendMenuEvents) -> Unit = {},
) = RetrySendMenuState(
selectedEvent = event,
eventSink = eventSink,
)

View file

@ -1,159 +0,0 @@
/*
* 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.retrysendmenu
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SheetState
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
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.features.messages.impl.R
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.launch
@Composable
internal fun RetrySendMessageMenu(
state: RetrySendMenuState,
modifier: Modifier = Modifier,
) {
val isVisible = state.selectedEvent != null
fun onDismiss() {
state.eventSink(RetrySendMenuEvents.Dismiss)
}
fun onRetry() {
state.eventSink(RetrySendMenuEvents.Retry)
}
fun onRemove() {
state.eventSink(RetrySendMenuEvents.Remove)
}
RetrySendMessageMenuBottomSheet(
modifier = modifier,
isVisible = isVisible,
onRetry = ::onRetry,
onRemove = ::onRemove,
onDismiss = ::onDismiss
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun RetrySendMessageMenuBottomSheet(
isVisible: Boolean,
onRetry: () -> Unit,
onRemove: () -> Unit,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
) {
val sheetState = rememberModalBottomSheetState()
val coroutineScope = rememberCoroutineScope()
if (isVisible) {
ModalBottomSheet(
modifier = modifier,
// modifier = modifier.navigationBarsPadding() - FIXME after https://issuetracker.google.com/issues/275849044
// .imePadding()
sheetState = sheetState,
onDismissRequest = {
coroutineScope.launch {
sheetState.hide()
onDismiss()
}
}
) {
RetrySendMenuContents(
onRetry = onRetry,
onRemove = onRemove,
)
// FIXME remove after https://issuetracker.google.com/issues/275849044
Spacer(modifier = Modifier.height(32.dp))
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ColumnScope.RetrySendMenuContents(
onRetry: () -> Unit,
onRemove: () -> Unit,
sheetState: SheetState = rememberModalBottomSheetState(),
) {
val coroutineScope = rememberCoroutineScope()
ListItem(headlineContent = {
Text(
text = stringResource(R.string.screen_room_retry_send_menu_title),
style = ElementTheme.typography.fontBodyLgMedium,
)
})
ListItem(
headlineContent = {
Text(
text = stringResource(R.string.screen_room_retry_send_menu_send_again_action),
style = ElementTheme.typography.fontBodyLgRegular,
)
},
modifier = Modifier.clickable {
coroutineScope.launch {
sheetState.hide()
onRetry()
}
}
)
ListItem(
headlineContent = {
Text(
text = stringResource(CommonStrings.action_remove),
style = ElementTheme.typography.fontBodyLgRegular,
)
},
colors = ListItemDefaults.colors(headlineColor = MaterialTheme.colorScheme.error),
modifier = Modifier.clickable {
coroutineScope.launch {
sheetState.hide()
onRemove()
}
}
)
}
@PreviewsDayNight
@Composable
internal fun RetrySendMessageMenuPreview(@PreviewParameter(RetrySendMenuStateProvider::class) state: RetrySendMenuState) = ElementPreview {
RetrySendMessageMenu(
state = state,
)
}

View file

@ -38,7 +38,6 @@ import io.element.android.features.messages.impl.timeline.components.customreact
import io.element.android.features.messages.impl.timeline.components.customreaction.FakeEmojibaseProvider
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryPresenter
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetPresenter
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter
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.TimelineItemTextContent
@ -64,19 +63,18 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatch
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
import io.element.android.libraries.matrix.test.A_TRANSACTION_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
@ -445,41 +443,25 @@ class MessagesPresenterTest {
@Test
fun `present - handle action redact`() = runTest {
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
val matrixRoom = FakeMatrixRoom()
val presenter = createMessagesPresenter(matrixRoom = matrixRoom, coroutineDispatchers = coroutineDispatchers)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Redact, aMessageEvent()))
assertThat(matrixRoom.redactEventEventIdParam).isEqualTo(AN_EVENT_ID)
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
}
}
@Test
fun `present - handle action redact message in error, in this case the message is just cancelled`() = runTest {
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
val matrixRoom = FakeMatrixRoom()
val liveTimeline = FakeTimeline()
val matrixRoom = FakeMatrixRoom(liveTimeline = liveTimeline)
val redactEventLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String? -> Result.success(true) }
liveTimeline.redactEventLambda = redactEventLambda
val presenter = createMessagesPresenter(matrixRoom = matrixRoom, coroutineDispatchers = coroutineDispatchers)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
initialState.eventSink.invoke(
MessagesEvents.HandleAction(
action = TimelineItemAction.Redact,
event = aMessageEvent(
transactionId = A_TRANSACTION_ID,
sendState = LocalEventSendState.SendingFailed("Failed to send message")
)
)
)
assertThat(matrixRoom.cancelSendCount).isEqualTo(1)
assertThat(matrixRoom.redactEventEventIdParam).isNull()
val messageEvent = aMessageEvent()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Redact, messageEvent))
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
assert(redactEventLambda)
.isCalledOnce()
.with(value(messageEvent.eventId), value(messageEvent.transactionId), value(null))
}
}
@ -839,7 +821,6 @@ class MessagesPresenterTest {
val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter()
val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
val retrySendMenuPresenter = RetrySendMenuPresenter(room = matrixRoom)
return MessagesPresenter(
room = matrixRoom,
composerPresenter = messageComposerPresenter,
@ -849,7 +830,6 @@ class MessagesPresenterTest {
actionListPresenter = actionListPresenter,
customReactionPresenter = customReactionPresenter,
reactionSummaryPresenter = reactionSummaryPresenter,
retrySendMenuPresenter = retrySendMenuPresenter,
readReceiptBottomSheetPresenter = readReceiptBottomSheetPresenter,
networkMonitor = FakeNetworkMonitor(),
snackbarDispatcher = SnackbarDispatcher(),

View file

@ -52,8 +52,6 @@ import io.element.android.features.messages.impl.timeline.components.customreact
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvents
import io.element.android.features.messages.impl.timeline.components.receipt.aReadReceiptData
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvents
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuEvents
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.aRetrySendMenuState
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.core.UserId
@ -74,6 +72,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class)
class MessagesViewTest {
@ -145,22 +144,6 @@ class MessagesViewTest {
callback.assertSuccess()
}
@Test
fun `clicking on an Event timestamp in error emits the expected Event`() {
val eventsRecorder = EventsRecorder<RetrySendMenuEvents>()
val state = aMessagesState(
retrySendMenuState = aRetrySendMenuState(
eventSink = eventsRecorder
),
)
val timelineItem = state.timelineState.timelineItems[1] as TimelineItem.Event
rule.setMessagesView(
state = state,
)
rule.onAllNodesWithText(timelineItem.sentTime)[1].performClick()
eventsRecorder.assertSingle(RetrySendMenuEvents.EventSelected(timelineItem))
}
@Test
fun `long clicking on an Event emits the expected Event userHasPermissionToSendMessage`() {
`long clicking on an Event emits the expected Event`(userHasPermissionToSendMessage = true)
@ -313,6 +296,7 @@ class MessagesViewTest {
}
@Test
@Config(qualifiers = "h1024dp")
fun `clicking on the sender of an Event invoke expected callback`() {
val eventsRecorder = EventsRecorder<MessagesEvents>(expectEvents = false)
val state = aMessagesState(

View file

@ -62,7 +62,7 @@ class ActionListPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
val messageEvent = aMessageEvent(isMine = true, content = TimelineItemRedactedContent)
val messageEvent = aMessageEvent(isMine = true, isEditable = false, content = TimelineItemRedactedContent)
initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage(
event = messageEvent,
@ -96,7 +96,11 @@ class ActionListPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
val messageEvent = aMessageEvent(isMine = false, content = TimelineItemRedactedContent)
val messageEvent = aMessageEvent(
isMine = false,
isEditable = false,
content = TimelineItemRedactedContent
)
initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage(
event = messageEvent,
@ -132,6 +136,7 @@ class ActionListPresenterTest {
val initialState = awaitItem()
val messageEvent = aMessageEvent(
isMine = false,
isEditable = false,
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null)
)
initialState.eventSink.invoke(
@ -174,6 +179,7 @@ class ActionListPresenterTest {
val initialState = awaitItem()
val messageEvent = aMessageEvent(
isMine = false,
isEditable = false,
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null)
)
initialState.eventSink.invoke(
@ -215,6 +221,7 @@ class ActionListPresenterTest {
val initialState = awaitItem()
val messageEvent = aMessageEvent(
isMine = false,
isEditable = false,
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null)
)
initialState.eventSink.invoke(
@ -256,6 +263,7 @@ class ActionListPresenterTest {
val initialState = awaitItem()
val messageEvent = aMessageEvent(
isMine = false,
isEditable = false,
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null)
)
initialState.eventSink.invoke(
@ -382,6 +390,7 @@ class ActionListPresenterTest {
val initialState = awaitItem()
val messageEvent = aMessageEvent(
isMine = true,
isEditable = false,
content = aTimelineItemImageContent(),
)
initialState.eventSink.invoke(

View file

@ -106,7 +106,6 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setTimel
onLinkClick: (String) -> Unit = EnsureNeverCalledWithParam(),
onMessageClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
onMessageLongClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
onTimestampClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
onSwipeToReply: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
onReactionClick: (emoji: String, TimelineItem.Event) -> Unit = EnsureNeverCalledWithTwoParams(),
onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit = EnsureNeverCalledWithTwoParams(),
@ -123,7 +122,6 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setTimel
onLinkClick = onLinkClick,
onMessageClick = onMessageClick,
onMessageLongClick = onMessageLongClick,
onTimestampClick = onTimestampClick,
onSwipeToReply = onSwipeToReply,
onReactionClick = onReactionClick,
onReactionLongClick = onReactionLongClick,

View file

@ -1,163 +0,0 @@
/*
* 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.retrysendmenu
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
import io.element.android.libraries.matrix.test.A_TRANSACTION_ID
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class RetrySendMenuPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
private val room = FakeMatrixRoom()
private val presenter = RetrySendMenuPresenter(room)
@Test
fun `present - handle event selected`() = runTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
val selectedEvent = aTimelineItemEvent()
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
assertThat(awaitItem().selectedEvent).isSameInstanceAs(selectedEvent)
}
}
@Test
fun `present - handle dismiss`() = runTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
val selectedEvent = aTimelineItemEvent()
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
skipItems(1)
initialState.eventSink(RetrySendMenuEvents.Dismiss)
assertThat(room.cancelSendCount).isEqualTo(0)
assertThat(room.retrySendMessageCount).isEqualTo(0)
assertThat(awaitItem().selectedEvent).isNull()
}
}
@Test
fun `present - handle resend with transactionId`() = runTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID)
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
skipItems(1)
initialState.eventSink(RetrySendMenuEvents.Retry)
assertThat(room.cancelSendCount).isEqualTo(0)
assertThat(room.retrySendMessageCount).isEqualTo(1)
assertThat(awaitItem().selectedEvent).isNull()
}
}
@Test
fun `present - handle resend without transactionId`() = runTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
val selectedEvent = aTimelineItemEvent(transactionId = null)
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
skipItems(1)
initialState.eventSink(RetrySendMenuEvents.Retry)
assertThat(room.cancelSendCount).isEqualTo(0)
assertThat(room.retrySendMessageCount).isEqualTo(0)
assertThat(awaitItem().selectedEvent).isNull()
}
}
@Test
fun `present - handle resend with error`() = runTest {
room.givenRetrySendMessageResult(Result.failure(IllegalStateException("An error")))
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID)
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
skipItems(1)
initialState.eventSink(RetrySendMenuEvents.Retry)
assertThat(room.cancelSendCount).isEqualTo(0)
assertThat(room.retrySendMessageCount).isEqualTo(1)
assertThat(awaitItem().selectedEvent).isNull()
}
}
@Test
fun `present - handle remove failed message with transactionId`() = runTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID)
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
skipItems(1)
initialState.eventSink(RetrySendMenuEvents.Remove)
assertThat(room.cancelSendCount).isEqualTo(1)
assertThat(room.retrySendMessageCount).isEqualTo(0)
assertThat(awaitItem().selectedEvent).isNull()
}
}
@Test
fun `present - handle remove failed message without transactionId`() = runTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
val selectedEvent = aTimelineItemEvent(transactionId = null)
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
skipItems(1)
initialState.eventSink(RetrySendMenuEvents.Remove)
assertThat(room.cancelSendCount).isEqualTo(0)
assertThat(room.retrySendMessageCount).isEqualTo(0)
assertThat(awaitItem().selectedEvent).isNull()
}
}
@Test
fun `present - handle remove failed message with error`() = runTest {
room.givenRetrySendMessageResult(Result.failure(IllegalStateException("An error")))
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID)
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
skipItems(1)
initialState.eventSink(RetrySendMenuEvents.Remove)
assertThat(room.cancelSendCount).isEqualTo(1)
assertThat(room.retrySendMessageCount).isEqualTo(0)
assertThat(awaitItem().selectedEvent).isNull()
}
}
}

View file

@ -1,90 +0,0 @@
/*
* Copyright (c) 2024 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.retrysendmenu
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.messages.impl.R
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.pressBackKey
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class)
class RetrySendMessageMenuTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `dismiss the bottom sheet emits the expected event`() {
val eventsRecorder = EventsRecorder<RetrySendMenuEvents>()
rule.setRetrySendMessageMenu(
aRetrySendMenuState(
event = aTimelineItemEvent(),
eventSink = eventsRecorder
),
)
rule.pressBackKey()
// Cannot test this for now.
// eventsRecorder.assertSingle(RetrySendMenuEvents.Dismiss)
}
@Config(qualifiers = "h1024dp")
@Test
fun `retry to send the event emits the expected event`() {
val eventsRecorder = EventsRecorder<RetrySendMenuEvents>()
rule.setRetrySendMessageMenu(
aRetrySendMenuState(
event = aTimelineItemEvent(),
eventSink = eventsRecorder
),
)
rule.clickOn(R.string.screen_room_retry_send_menu_send_again_action)
eventsRecorder.assertSingle(RetrySendMenuEvents.Retry)
}
@Config(qualifiers = "h1024dp")
@Test
fun `remove the event emits the expected event`() {
val eventsRecorder = EventsRecorder<RetrySendMenuEvents>()
rule.setRetrySendMessageMenu(
aRetrySendMenuState(
event = aTimelineItemEvent(),
eventSink = eventsRecorder
),
)
rule.clickOn(CommonStrings.action_remove)
eventsRecorder.assertSingle(RetrySendMenuEvents.Remove)
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRetrySendMessageMenu(
state: RetrySendMenuState,
) {
setContent {
RetrySendMessageMenu(
state = state,
)
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2024 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.migration.impl.migrations
import com.squareup.anvil.annotations.ContributesMultibinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.sessionstorage.api.SessionStore
import java.io.File
import javax.inject.Inject
@ContributesMultibinding(AppScope::class)
class AppMigration05 @Inject constructor(
private val sessionStore: SessionStore,
private val baseDirectory: File,
) : AppMigration {
override val order: Int = 5
override suspend fun migrate() {
val allSessions = sessionStore.getAllSessions()
for (session in allSessions) {
if (session.sessionPath.isEmpty()) {
val sessionPath = File(baseDirectory, session.userId.replace(':', '_')).absolutePath
sessionStore.updateData(session.copy(sessionPath = sessionPath))
}
}
}
}

View file

@ -68,8 +68,12 @@ class PollRepository @Inject constructor(
suspend fun deletePoll(
pollStartId: EventId,
): Result<Unit> =
room.redactEvent(
eventId = pollStartId,
)
): Result<Boolean> =
timelineProvider
.getActiveTimeline()
.redactEvent(
eventId = pollStartId,
transactionId = null,
reason = null,
)
}

View file

@ -29,6 +29,7 @@ import io.element.android.features.poll.impl.aPollTimelineItems
import io.element.android.features.poll.impl.anOngoingPollContent
import io.element.android.features.poll.impl.data.PollRepository
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
@ -39,6 +40,7 @@ import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.matrix.test.timeline.LiveTimelineProvider
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.any
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
@ -455,47 +457,53 @@ import org.junit.Test
@Test
fun `delete confirms`() = runTest {
val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId))
val redactEventLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String? -> Result.success(true) }
timeline.redactEventLambda = redactEventLambda
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitDefaultItem()
awaitPollLoaded().eventSink(CreatePollEvents.Delete(confirmed = false))
awaitDeleteConfirmation()
assertThat(fakeMatrixRoom.redactEventEventIdParam).isNull()
assert(redactEventLambda).isNeverCalled()
}
}
@Test
fun `delete can be cancelled`() = runTest {
val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId))
val redactEventLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String? -> Result.success(true) }
timeline.redactEventLambda = redactEventLambda
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitDefaultItem()
awaitPollLoaded().eventSink(CreatePollEvents.Delete(confirmed = false))
assertThat(fakeMatrixRoom.redactEventEventIdParam).isNull()
awaitDeleteConfirmation().eventSink(CreatePollEvents.HideConfirmation)
awaitPollLoaded().apply {
assertThat(showDeleteConfirmation).isFalse()
}
assertThat(fakeMatrixRoom.redactEventEventIdParam).isNull()
assert(redactEventLambda).isNeverCalled()
}
}
@Test
fun `delete can be confirmed`() = runTest {
val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId))
val redactEventLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String? -> Result.success(true) }
timeline.redactEventLambda = redactEventLambda
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitDefaultItem()
awaitPollLoaded().eventSink(CreatePollEvents.Delete(confirmed = false))
assertThat(fakeMatrixRoom.redactEventEventIdParam).isNull()
awaitDeleteConfirmation().eventSink(CreatePollEvents.Delete(confirmed = true))
awaitPollLoaded().apply {
assertThat(showDeleteConfirmation).isFalse()
}
assertThat(fakeMatrixRoom.redactEventEventIdParam).isEqualTo(pollEventId)
assert(redactEventLambda)
.isCalledOnce()
.with(value(pollEventId), value(null), any())
}
}

View file

@ -287,7 +287,8 @@ class DefaultBugReporterTest {
oidcData = null,
refreshToken = null,
slidingSyncProxy = null,
passphrase = null
passphrase = null,
sessionPath = "session",
)
@Test
fun `test sendBugReport error`() = runTest {

View file

@ -51,5 +51,6 @@ fun aSessionData(
isTokenValid = isTokenValid,
loginType = LoginType.UNKNOWN,
passphrase = null,
sessionPath = "/a/path/to/a/session",
)
}