Remove AttachmentsState and use the MessagesNavigator

This commit is contained in:
Benoit Marty 2024-11-21 17:07:34 +01:00
parent b37cabd703
commit 5ec0d5a5f9
12 changed files with 86 additions and 116 deletions

View file

@ -7,13 +7,16 @@
package io.element.android.features.messages.impl package io.element.android.features.messages.impl
import io.element.android.features.messages.impl.attachments.Attachment
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 io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import kotlinx.collections.immutable.ImmutableList
interface MessagesNavigator { interface MessagesNavigator {
fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo)
fun onForwardEventClick(eventId: EventId) fun onForwardEventClick(eventId: EventId)
fun onReportContentClick(eventId: EventId, senderId: UserId) fun onReportContentClick(eventId: EventId, senderId: UserId)
fun onEditPollClick(eventId: EventId) fun onEditPollClick(eventId: EventId)
fun onPreviewAttachment(attachments: ImmutableList<Attachment>)
} }

View file

@ -30,6 +30,7 @@ import io.element.android.anvilannotations.ContributesNode
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelineEvents
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories
@ -60,12 +61,16 @@ class MessagesNode @AssistedInject constructor(
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val room: MatrixRoom, private val room: MatrixRoom,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
messageComposerPresenterFactory: MessageComposerPresenter.Factory,
presenterFactory: MessagesPresenter.Factory, presenterFactory: MessagesPresenter.Factory,
private val timelineItemPresenterFactories: TimelineItemPresenterFactories, private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
private val mediaPlayer: MediaPlayer, private val mediaPlayer: MediaPlayer,
private val permalinkParser: PermalinkParser, private val permalinkParser: PermalinkParser,
) : Node(buildContext, plugins = plugins), MessagesNavigator { ) : Node(buildContext, plugins = plugins), MessagesNavigator {
private val presenter = presenterFactory.create(this) private val presenter = presenterFactory.create(
navigator = this,
composerPresenter = messageComposerPresenterFactory.create(this),
)
private val callbacks = plugins<Callback>() private val callbacks = plugins<Callback>()
data class Inputs(val focusedEventId: EventId?) : NodeInputs data class Inputs(val focusedEventId: EventId?) : NodeInputs
@ -114,10 +119,6 @@ class MessagesNode @AssistedInject constructor(
.orFalse() .orFalse()
} }
private fun onPreviewAttachments(attachments: ImmutableList<Attachment>) {
callbacks.forEach { it.onPreviewAttachments(attachments) }
}
private fun onUserDataClick(userId: UserId) { private fun onUserDataClick(userId: UserId) {
callbacks.forEach { it.onUserDataClick(userId) } callbacks.forEach { it.onUserDataClick(userId) }
} }
@ -178,6 +179,10 @@ class MessagesNode @AssistedInject constructor(
callbacks.forEach { it.onEditPollClick(eventId) } callbacks.forEach { it.onEditPollClick(eventId) }
} }
override fun onPreviewAttachment(attachments: ImmutableList<Attachment>) {
callbacks.forEach { it.onPreviewAttachments(attachments) }
}
private fun onViewAllPinnedMessagesClick() { private fun onViewAllPinnedMessagesClick() {
callbacks.forEach { it.onViewAllPinnedEvents() } callbacks.forEach { it.onViewAllPinnedEvents() }
} }
@ -213,7 +218,6 @@ class MessagesNode @AssistedInject constructor(
onBackClick = this::navigateUp, onBackClick = this::navigateUp,
onRoomDetailsClick = this::onRoomDetailsClick, onRoomDetailsClick = this::onRoomDetailsClick,
onEventContentClick = this::onEventClick, onEventContentClick = this::onEventClick,
onPreviewAttachments = this::onPreviewAttachments,
onUserDataClick = this::onUserDataClick, onUserDataClick = this::onUserDataClick,
onLinkClick = { url -> onLinkClick(activity, isDark, url, state.timelineState.eventSink) }, onLinkClick = { url -> onLinkClick(activity, isDark, url, state.timelineState.eventSink) },
onSendLocationClick = this::onSendLocationClick, onSendLocationClick = this::onSendLocationClick,

View file

@ -89,12 +89,12 @@ import timber.log.Timber
class MessagesPresenter @AssistedInject constructor( class MessagesPresenter @AssistedInject constructor(
@Assisted private val navigator: MessagesNavigator, @Assisted private val navigator: MessagesNavigator,
private val room: MatrixRoom, private val room: MatrixRoom,
private val composerPresenter: Presenter<MessageComposerState>, @Assisted private val composerPresenter: Presenter<MessageComposerState>,
private val voiceMessageComposerPresenter: Presenter<VoiceMessageComposerState>, private val voiceMessageComposerPresenter: Presenter<VoiceMessageComposerState>,
timelinePresenterFactory: TimelinePresenter.Factory, timelinePresenterFactory: TimelinePresenter.Factory,
private val timelineProtectionPresenter: Presenter<TimelineProtectionState>, private val timelineProtectionPresenter: Presenter<TimelineProtectionState>,
private val identityChangeStatePresenter: Presenter<IdentityChangeState>, private val identityChangeStatePresenter: Presenter<IdentityChangeState>,
private val actionListPresenterFactory: ActionListPresenter.Factory, actionListPresenterFactory: ActionListPresenter.Factory,
private val customReactionPresenter: Presenter<CustomReactionState>, private val customReactionPresenter: Presenter<CustomReactionState>,
private val reactionSummaryPresenter: Presenter<ReactionSummaryState>, private val reactionSummaryPresenter: Presenter<ReactionSummaryState>,
private val readReceiptBottomSheetPresenter: Presenter<ReadReceiptBottomSheetState>, private val readReceiptBottomSheetPresenter: Presenter<ReadReceiptBottomSheetState>,
@ -116,7 +116,10 @@ class MessagesPresenter @AssistedInject constructor(
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
fun create(navigator: MessagesNavigator): MessagesPresenter fun create(
navigator: MessagesNavigator,
composerPresenter: Presenter<MessageComposerState>,
): MessagesPresenter
} }
@Composable @Composable

View file

@ -32,10 +32,8 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -55,10 +53,8 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListEvents
import io.element.android.features.messages.impl.actionlist.ActionListView import io.element.android.features.messages.impl.actionlist.ActionListView
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeStateView import io.element.android.features.messages.impl.crypto.identity.IdentityChangeStateView
import io.element.android.features.messages.impl.messagecomposer.AttachmentsBottomSheet import io.element.android.features.messages.impl.messagecomposer.AttachmentsBottomSheet
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
import io.element.android.features.messages.impl.messagecomposer.MessageComposerView import io.element.android.features.messages.impl.messagecomposer.MessageComposerView
import io.element.android.features.messages.impl.messagecomposer.suggestions.SuggestionsPickerView import io.element.android.features.messages.impl.messagecomposer.suggestions.SuggestionsPickerView
@ -115,7 +111,6 @@ fun MessagesView(
onEventContentClick: (event: TimelineItem.Event) -> Boolean, onEventContentClick: (event: TimelineItem.Event) -> Boolean,
onUserDataClick: (UserId) -> Unit, onUserDataClick: (UserId) -> Unit,
onLinkClick: (String) -> Unit, onLinkClick: (String) -> Unit,
onPreviewAttachments: (ImmutableList<Attachment>) -> Unit,
onSendLocationClick: () -> Unit, onSendLocationClick: () -> Unit,
onCreatePollClick: () -> Unit, onCreatePollClick: () -> Unit,
onJoinCallClick: () -> Unit, onJoinCallClick: () -> Unit,
@ -129,11 +124,6 @@ fun MessagesView(
KeepScreenOn(state.voiceMessageComposerState.keepScreenOn) KeepScreenOn(state.voiceMessageComposerState.keepScreenOn)
AttachmentStateView(
state = state.composerState.attachmentsState,
onPreviewAttachments = onPreviewAttachments,
)
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
// This is needed because the composer is inside an AndroidView that can't be affected by the FocusManager in Compose // This is needed because the composer is inside an AndroidView that can't be affected by the FocusManager in Compose
@ -273,22 +263,6 @@ private fun ReinviteDialog(state: MessagesState) {
} }
} }
@Composable
private fun AttachmentStateView(
state: AttachmentsState,
onPreviewAttachments: (ImmutableList<Attachment>) -> Unit,
) {
when (state) {
AttachmentsState.None -> Unit
is AttachmentsState.Previewing -> {
val latestOnPreviewAttachments by rememberUpdatedState(onPreviewAttachments)
LaunchedEffect(state) {
latestOnPreviewAttachments(state.attachments)
}
}
}
}
@Composable @Composable
private fun MessagesViewContent( private fun MessagesViewContent(
state: MessagesState, state: MessagesState,
@ -557,7 +531,6 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class)
onEventContentClick = { false }, onEventContentClick = { false },
onUserDataClick = {}, onUserDataClick = {},
onLinkClick = {}, onLinkClick = {},
onPreviewAttachments = {},
onSendLocationClick = {}, onSendLocationClick = {},
onCreatePollClick = {}, onCreatePollClick = {},
onJoinCallClick = {}, onJoinCallClick = {},

View file

@ -36,7 +36,6 @@ internal fun MessagesViewWithIdentityChangePreview(
onEventContentClick = { false }, onEventContentClick = { false },
onUserDataClick = {}, onUserDataClick = {},
onLinkClick = {}, onLinkClick = {},
onPreviewAttachments = {},
onSendLocationClick = {}, onSendLocationClick = {},
onCreatePollClick = {}, onCreatePollClick = {},
onJoinCallClick = {}, onJoinCallClick = {},

View file

@ -14,8 +14,6 @@ import io.element.android.features.messages.impl.crypto.identity.IdentityChangeS
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeStatePresenter import io.element.android.features.messages.impl.crypto.identity.IdentityChangeStatePresenter
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailurePresenter import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailurePresenter
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureState import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureState
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerPresenter import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerPresenter
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter
@ -48,9 +46,6 @@ interface MessagesModule {
@Binds @Binds
fun bindTimelineProtectionPresenter(presenter: TimelineProtectionPresenter): Presenter<TimelineProtectionState> fun bindTimelineProtectionPresenter(presenter: TimelineProtectionPresenter): Presenter<TimelineProtectionState>
@Binds
fun bindMessageComposerPresenter(presenter: MessageComposerPresenter): Presenter<MessageComposerState>
@Binds @Binds
fun bindVoiceMessageComposerPresenter(presenter: VoiceMessageComposerPresenter): Presenter<VoiceMessageComposerState> fun bindVoiceMessageComposerPresenter(presenter: VoiceMessageComposerPresenter): Presenter<VoiceMessageComposerState>

View file

@ -14,7 +14,6 @@ import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
@ -26,8 +25,12 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.media3.common.MimeTypes import androidx.media3.common.MimeTypes
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.Composer import im.vector.app.features.analytics.plan.Composer
import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.analytics.plan.Interaction
import io.element.android.features.messages.impl.MessagesNavigator
import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError
import io.element.android.features.messages.impl.draft.ComposerDraftService import io.element.android.features.messages.impl.draft.ComposerDraftService
@ -38,8 +41,6 @@ import io.element.android.features.messages.impl.utils.TextPillificationHelper
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
@ -89,12 +90,11 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
import io.element.android.libraries.core.mimetype.MimeTypes.Any as AnyMimeTypes import io.element.android.libraries.core.mimetype.MimeTypes.Any as AnyMimeTypes
@SingleIn(RoomScope::class) class MessageComposerPresenter @AssistedInject constructor(
class MessageComposerPresenter @Inject constructor( @Assisted private val navigator: MessagesNavigator,
private val appCoroutineScope: CoroutineScope, private val appCoroutineScope: CoroutineScope,
private val room: MatrixRoom, private val room: MatrixRoom,
private val mediaPickerProvider: PickerProvider, private val mediaPickerProvider: PickerProvider,
@ -117,6 +117,11 @@ class MessageComposerPresenter @Inject constructor(
private val roomMemberProfilesCache: RoomMemberProfilesCache, private val roomMemberProfilesCache: RoomMemberProfilesCache,
private val suggestionsProcessor: SuggestionsProcessor, private val suggestionsProcessor: SuggestionsProcessor,
) : Presenter<MessageComposerState> { ) : Presenter<MessageComposerState> {
@AssistedFactory
interface Factory {
fun create(navigator: MessagesNavigator): MessageComposerPresenter
}
private val cameraPermissionPresenter = permissionsPresenterFactory.create(Manifest.permission.CAMERA) private val cameraPermissionPresenter = permissionsPresenterFactory.create(Manifest.permission.CAMERA)
private var pendingEvent: MessageComposerEvents? = null private var pendingEvent: MessageComposerEvents? = null
private val suggestionSearchTrigger = MutableStateFlow<Suggestion?>(null) private val suggestionSearchTrigger = MutableStateFlow<Suggestion?>(null)
@ -147,9 +152,6 @@ class MessageComposerPresenter @Inject constructor(
} }
val cameraPermissionState = cameraPermissionPresenter.present() val cameraPermissionState = cameraPermissionPresenter.present()
val attachmentsState = remember {
mutableStateOf<AttachmentsState>(AttachmentsState.None)
}
val canShareLocation = remember { mutableStateOf(false) } val canShareLocation = remember { mutableStateOf(false) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@ -162,16 +164,16 @@ class MessageComposerPresenter @Inject constructor(
} }
val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker { uri, mimeType -> val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker { uri, mimeType ->
handlePickedMedia(attachmentsState, uri, mimeType) handlePickedMedia(uri, mimeType)
} }
val filesPicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes) { uri -> val filesPicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes) { uri ->
handlePickedMedia(attachmentsState, uri) handlePickedMedia(uri)
} }
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker { uri -> val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker { uri ->
handlePickedMedia(attachmentsState, uri, MimeTypes.IMAGE_JPEG) handlePickedMedia(uri, MimeTypes.IMAGE_JPEG)
} }
val cameraVideoPicker = mediaPickerProvider.registerCameraVideoPicker { uri -> val cameraVideoPicker = mediaPickerProvider.registerCameraVideoPicker { uri ->
handlePickedMedia(attachmentsState, uri, MimeTypes.VIDEO_MP4) handlePickedMedia(uri, MimeTypes.VIDEO_MP4)
} }
val isFullScreen = rememberSaveable { val isFullScreen = rememberSaveable {
mutableStateOf(false) mutableStateOf(false)
@ -277,7 +279,6 @@ class MessageComposerPresenter @Inject constructor(
formattedFileSize = null formattedFileSize = null
), ),
), ),
attachmentState = attachmentsState,
) )
is MessageComposerEvents.SetMode -> { is MessageComposerEvents.SetMode -> {
localCoroutineScope.setMode(event.composerMode, markdownTextEditorState, richTextEditorState) localCoroutineScope.setMode(event.composerMode, markdownTextEditorState, richTextEditorState)
@ -396,7 +397,6 @@ class MessageComposerPresenter @Inject constructor(
showTextFormatting = showTextFormatting, showTextFormatting = showTextFormatting,
canShareLocation = canShareLocation.value, canShareLocation = canShareLocation.value,
canCreatePoll = canCreatePoll.value, canCreatePoll = canCreatePoll.value,
attachmentsState = attachmentsState.value,
suggestions = suggestions.toPersistentList(), suggestions = suggestions.toPersistentList(),
resolveMentionDisplay = resolveMentionDisplay, resolveMentionDisplay = resolveMentionDisplay,
eventSink = { handleEvents(it) }, eventSink = { handleEvents(it) },
@ -459,14 +459,12 @@ class MessageComposerPresenter @Inject constructor(
private fun CoroutineScope.sendAttachment( private fun CoroutineScope.sendAttachment(
attachment: Attachment, attachment: Attachment,
attachmentState: MutableState<AttachmentsState>,
) = when (attachment) { ) = when (attachment) {
is Attachment.Media -> { is Attachment.Media -> {
launch { launch {
sendMedia( sendMedia(
uri = attachment.localMedia.uri, uri = attachment.localMedia.uri,
mimeType = attachment.localMedia.info.mimeType, mimeType = attachment.localMedia.info.mimeType,
attachmentState = attachmentState,
) )
} }
} }
@ -474,14 +472,10 @@ class MessageComposerPresenter @Inject constructor(
@UnstableApi @UnstableApi
private fun handlePickedMedia( private fun handlePickedMedia(
attachmentsState: MutableState<AttachmentsState>,
uri: Uri?, uri: Uri?,
mimeType: String? = null, mimeType: String? = null,
) { ) {
if (uri == null) { uri ?: return
attachmentsState.value = AttachmentsState.None
return
}
val localMedia = localMediaFactory.createFromUri( val localMedia = localMediaFactory.createFromUri(
uri = uri, uri = uri,
mimeType = mimeType, mimeType = mimeType,
@ -489,13 +483,12 @@ class MessageComposerPresenter @Inject constructor(
formattedFileSize = null formattedFileSize = null
) )
val mediaAttachment = Attachment.Media(localMedia) val mediaAttachment = Attachment.Media(localMedia)
attachmentsState.value = AttachmentsState.Previewing(persistentListOf(mediaAttachment)) navigator.onPreviewAttachment(persistentListOf(mediaAttachment))
} }
private suspend fun sendMedia( private suspend fun sendMedia(
uri: Uri, uri: Uri,
mimeType: String, mimeType: String,
attachmentState: MutableState<AttachmentsState>,
) = runCatching { ) = runCatching {
mediaSender.sendMedia( mediaSender.sendMedia(
uri = uri, uri = uri,
@ -503,12 +496,8 @@ class MessageComposerPresenter @Inject constructor(
progressCallback = null, progressCallback = null,
).getOrThrow() ).getOrThrow()
} }
.onSuccess {
attachmentState.value = AttachmentsState.None
}
.onFailure { cause -> .onFailure { cause ->
Timber.e(cause, "Failed to send attachment") Timber.e(cause, "Failed to send attachment")
attachmentState.value = AttachmentsState.None
if (cause is CancellationException) { if (cause is CancellationException) {
throw cause throw cause
} else { } else {

View file

@ -7,9 +7,7 @@
package io.element.android.features.messages.impl.messagecomposer package io.element.android.features.messages.impl.messagecomposer
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.textcomposer.model.TextEditorState import io.element.android.libraries.textcomposer.model.TextEditorState
@ -25,14 +23,7 @@ data class MessageComposerState(
val showTextFormatting: Boolean, val showTextFormatting: Boolean,
val canShareLocation: Boolean, val canShareLocation: Boolean,
val canCreatePoll: Boolean, val canCreatePoll: Boolean,
val attachmentsState: AttachmentsState,
val suggestions: ImmutableList<ResolvedSuggestion>, val suggestions: ImmutableList<ResolvedSuggestion>,
val resolveMentionDisplay: (String, String) -> TextDisplay, val resolveMentionDisplay: (String, String) -> TextDisplay,
val eventSink: (MessageComposerEvents) -> Unit, val eventSink: (MessageComposerEvents) -> Unit,
) )
@Immutable
sealed interface AttachmentsState {
data object None : AttachmentsState
data class Previewing(val attachments: ImmutableList<Attachment>) : AttachmentsState
}

View file

@ -31,7 +31,6 @@ fun aMessageComposerState(
showAttachmentSourcePicker: Boolean = false, showAttachmentSourcePicker: Boolean = false,
canShareLocation: Boolean = true, canShareLocation: Boolean = true,
canCreatePoll: Boolean = true, canCreatePoll: Boolean = true,
attachmentsState: AttachmentsState = AttachmentsState.None,
suggestions: ImmutableList<ResolvedSuggestion> = persistentListOf(), suggestions: ImmutableList<ResolvedSuggestion> = persistentListOf(),
eventSink: (MessageComposerEvents) -> Unit = {}, eventSink: (MessageComposerEvents) -> Unit = {},
) = MessageComposerState( ) = MessageComposerState(
@ -42,7 +41,6 @@ fun aMessageComposerState(
showAttachmentSourcePicker = showAttachmentSourcePicker, showAttachmentSourcePicker = showAttachmentSourcePicker,
canShareLocation = canShareLocation, canShareLocation = canShareLocation,
canCreatePoll = canCreatePoll, canCreatePoll = canCreatePoll,
attachmentsState = attachmentsState,
suggestions = suggestions, suggestions = suggestions,
resolveMentionDisplay = { _, _ -> TextDisplay.Plain }, resolveMentionDisplay = { _, _ -> TextDisplay.Plain },
eventSink = eventSink, eventSink = eventSink,

View file

@ -7,9 +7,11 @@
package io.element.android.features.messages.impl package io.element.android.features.messages.impl
import io.element.android.features.messages.impl.attachments.Attachment
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 io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import kotlinx.collections.immutable.ImmutableList
class FakeMessagesNavigator : MessagesNavigator { class FakeMessagesNavigator : MessagesNavigator {
var onShowEventDebugInfoClickedCount = 0 var onShowEventDebugInfoClickedCount = 0
@ -24,6 +26,9 @@ class FakeMessagesNavigator : MessagesNavigator {
var onEditPollClickedCount = 0 var onEditPollClickedCount = 0
private set private set
var onPreviewAttachmentCount = 0
private set
override fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) { override fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
onShowEventDebugInfoClickedCount++ onShowEventDebugInfoClickedCount++
} }
@ -39,4 +44,8 @@ class FakeMessagesNavigator : MessagesNavigator {
override fun onEditPollClick(eventId: EventId) { override fun onEditPollClick(eventId: EventId) {
onEditPollClickedCount++ onEditPollClickedCount++
} }
override fun onPreviewAttachment(attachments: ImmutableList<Attachment>) {
onPreviewAttachmentCount++
}
} }

View file

@ -514,7 +514,6 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMessa
onEventClick: (event: TimelineItem.Event) -> Boolean = EnsureNeverCalledWithParamAndResult(), onEventClick: (event: TimelineItem.Event) -> Boolean = EnsureNeverCalledWithParamAndResult(),
onUserDataClick: (UserId) -> Unit = EnsureNeverCalledWithParam(), onUserDataClick: (UserId) -> Unit = EnsureNeverCalledWithParam(),
onLinkClick: (String) -> Unit = EnsureNeverCalledWithParam(), onLinkClick: (String) -> Unit = EnsureNeverCalledWithParam(),
onPreviewAttachments: (ImmutableList<Attachment>) -> Unit = EnsureNeverCalledWithParam(),
onSendLocationClick: () -> Unit = EnsureNeverCalled(), onSendLocationClick: () -> Unit = EnsureNeverCalled(),
onCreatePollClick: () -> Unit = EnsureNeverCalled(), onCreatePollClick: () -> Unit = EnsureNeverCalled(),
onJoinCallClick: () -> Unit = EnsureNeverCalled(), onJoinCallClick: () -> Unit = EnsureNeverCalled(),
@ -532,7 +531,6 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMessa
onEventContentClick = onEventClick, onEventContentClick = onEventClick,
onUserDataClick = onUserDataClick, onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick, onLinkClick = onLinkClick,
onPreviewAttachments = onPreviewAttachments,
onSendLocationClick = onSendLocationClick, onSendLocationClick = onSendLocationClick,
onCreatePollClick = onCreatePollClick, onCreatePollClick = onCreatePollClick,
onJoinCallClick = onJoinCallClick, onJoinCallClick = onJoinCallClick,

View file

@ -18,6 +18,8 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.Composer import im.vector.app.features.analytics.plan.Composer
import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.analytics.plan.Interaction
import io.element.android.features.messages.impl.FakeMessagesNavigator
import io.element.android.features.messages.impl.MessagesNavigator
import io.element.android.features.messages.impl.draft.ComposerDraftService import io.element.android.features.messages.impl.draft.ComposerDraftService
import io.element.android.features.messages.impl.draft.FakeComposerDraftService import io.element.android.features.messages.impl.draft.FakeComposerDraftService
import io.element.android.features.messages.impl.messagecomposer.suggestions.SuggestionsProcessor import io.element.android.features.messages.impl.messagecomposer.suggestions.SuggestionsProcessor
@ -133,7 +135,6 @@ class MessageComposerPresenterTest {
assertThat(initialState.mode).isEqualTo(MessageComposerMode.Normal) assertThat(initialState.mode).isEqualTo(MessageComposerMode.Normal)
assertThat(initialState.showAttachmentSourcePicker).isFalse() assertThat(initialState.showAttachmentSourcePicker).isFalse()
assertThat(initialState.canShareLocation).isTrue() assertThat(initialState.canShareLocation).isTrue()
assertThat(initialState.attachmentsState).isEqualTo(AttachmentsState.None)
} }
} }
@ -685,7 +686,12 @@ class MessageComposerPresenterTest {
val room = FakeMatrixRoom( val room = FakeMatrixRoom(
typingNoticeResult = { Result.success(Unit) } typingNoticeResult = { Result.success(Unit) }
) )
val presenter = createPresenter(this, room = room) val navigator = FakeMessagesNavigator()
val presenter = createPresenter(
coroutineScope = this,
room = room,
navigator = navigator,
)
pickerProvider.givenMimeType(MimeTypes.Images) pickerProvider.givenMimeType(MimeTypes.Images)
mediaPreProcessor.givenResult( mediaPreProcessor.givenResult(
Result.success( Result.success(
@ -709,9 +715,7 @@ class MessageComposerPresenterTest {
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery) initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery)
val previewingState = awaitItem() assertThat(navigator.onPreviewAttachmentCount).isEqualTo(1)
assertThat(previewingState.showAttachmentSourcePicker).isFalse()
assertThat(previewingState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java)
} }
} }
@ -720,7 +724,12 @@ class MessageComposerPresenterTest {
val room = FakeMatrixRoom( val room = FakeMatrixRoom(
typingNoticeResult = { Result.success(Unit) } typingNoticeResult = { Result.success(Unit) }
) )
val presenter = createPresenter(this, room = room) val navigator = FakeMessagesNavigator()
val presenter = createPresenter(
coroutineScope = this,
room = room,
navigator = navigator,
)
pickerProvider.givenMimeType(MimeTypes.Videos) pickerProvider.givenMimeType(MimeTypes.Videos)
mediaPreProcessor.givenResult( mediaPreProcessor.givenResult(
Result.success( Result.success(
@ -745,9 +754,7 @@ class MessageComposerPresenterTest {
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery) initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery)
val previewingState = awaitItem() assertThat(navigator.onPreviewAttachmentCount).isEqualTo(1)
assertThat(previewingState.showAttachmentSourcePicker).isFalse()
assertThat(previewingState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java)
} }
} }
@ -772,15 +779,18 @@ class MessageComposerPresenterTest {
val room = FakeMatrixRoom( val room = FakeMatrixRoom(
typingNoticeResult = { Result.success(Unit) } typingNoticeResult = { Result.success(Unit) }
) )
val presenter = createPresenter(this, room = room) val navigator = FakeMessagesNavigator()
val presenter = createPresenter(
coroutineScope = this,
room = room,
navigator = navigator,
)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles) initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles)
val sendingState = awaitItem() assertThat(navigator.onPreviewAttachmentCount).isEqualTo(1)
assertThat(sendingState.showAttachmentSourcePicker).isFalse()
assertThat(sendingState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java)
} }
} }
@ -828,19 +838,19 @@ class MessageComposerPresenterTest {
typingNoticeResult = { Result.success(Unit) } typingNoticeResult = { Result.success(Unit) }
) )
val permissionPresenter = FakePermissionsPresenter().apply { setPermissionGranted() } val permissionPresenter = FakePermissionsPresenter().apply { setPermissionGranted() }
val navigator = FakeMessagesNavigator()
val presenter = createPresenter( val presenter = createPresenter(
this, coroutineScope = this,
room = room, room = room,
permissionPresenter = permissionPresenter, permissionPresenter = permissionPresenter,
navigator = navigator,
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.PhotoFromCamera) initialState.eventSink(MessageComposerEvents.PickAttachmentSource.PhotoFromCamera)
val finalState = awaitItem() assertThat(navigator.onPreviewAttachmentCount).isEqualTo(1)
assertThat(finalState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java)
cancelAndIgnoreRemainingEvents()
} }
} }
@ -850,23 +860,20 @@ class MessageComposerPresenterTest {
typingNoticeResult = { Result.success(Unit) } typingNoticeResult = { Result.success(Unit) }
) )
val permissionPresenter = FakePermissionsPresenter() val permissionPresenter = FakePermissionsPresenter()
val navigator = FakeMessagesNavigator()
val presenter = createPresenter( val presenter = createPresenter(
this, coroutineScope = this,
room = room, room = room,
permissionPresenter = permissionPresenter, permissionPresenter = permissionPresenter,
navigator = navigator,
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.PhotoFromCamera) initialState.eventSink(MessageComposerEvents.PickAttachmentSource.PhotoFromCamera)
val permissionState = awaitItem()
assertThat(permissionState.showAttachmentSourcePicker).isFalse()
assertThat(permissionState.attachmentsState).isInstanceOf(AttachmentsState.None::class.java)
permissionPresenter.setPermissionGranted() permissionPresenter.setPermissionGranted()
skipItems(1) assertThat(navigator.onPreviewAttachmentCount).isEqualTo(1)
val finalState = awaitItem()
assertThat(finalState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java)
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
} }
@ -877,19 +884,19 @@ class MessageComposerPresenterTest {
typingNoticeResult = { Result.success(Unit) } typingNoticeResult = { Result.success(Unit) }
) )
val permissionPresenter = FakePermissionsPresenter().apply { setPermissionGranted() } val permissionPresenter = FakePermissionsPresenter().apply { setPermissionGranted() }
val navigator = FakeMessagesNavigator()
val presenter = createPresenter( val presenter = createPresenter(
this, coroutineScope = this,
room = room, room = room,
permissionPresenter = permissionPresenter, permissionPresenter = permissionPresenter,
navigator = navigator,
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.VideoFromCamera) initialState.eventSink(MessageComposerEvents.PickAttachmentSource.VideoFromCamera)
val finalState = awaitItem() assertThat(navigator.onPreviewAttachmentCount).isEqualTo(1)
assertThat(finalState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java)
cancelAndIgnoreRemainingEvents()
} }
} }
@ -899,10 +906,12 @@ class MessageComposerPresenterTest {
typingNoticeResult = { Result.success(Unit) } typingNoticeResult = { Result.success(Unit) }
) )
val permissionPresenter = FakePermissionsPresenter() val permissionPresenter = FakePermissionsPresenter()
val navigator = FakeMessagesNavigator()
val presenter = createPresenter( val presenter = createPresenter(
this, coroutineScope = this,
room = room, room = room,
permissionPresenter = permissionPresenter, permissionPresenter = permissionPresenter,
navigator = navigator,
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -911,12 +920,9 @@ class MessageComposerPresenterTest {
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.VideoFromCamera) initialState.eventSink(MessageComposerEvents.PickAttachmentSource.VideoFromCamera)
val permissionState = awaitItem() val permissionState = awaitItem()
assertThat(permissionState.showAttachmentSourcePicker).isFalse() assertThat(permissionState.showAttachmentSourcePicker).isFalse()
assertThat(permissionState.attachmentsState).isInstanceOf(AttachmentsState.None::class.java)
permissionPresenter.setPermissionGranted() permissionPresenter.setPermissionGranted()
skipItems(1) skipItems(1)
val finalState = awaitItem() assertThat(navigator.onPreviewAttachmentCount).isEqualTo(1)
assertThat(finalState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java)
cancelAndIgnoreRemainingEvents()
} }
} }
@ -1500,6 +1506,7 @@ class MessageComposerPresenterTest {
room: MatrixRoom = FakeMatrixRoom( room: MatrixRoom = FakeMatrixRoom(
typingNoticeResult = { Result.success(Unit) } typingNoticeResult = { Result.success(Unit) }
), ),
navigator: MessagesNavigator = FakeMessagesNavigator(),
pickerProvider: PickerProvider = this.pickerProvider, pickerProvider: PickerProvider = this.pickerProvider,
featureFlagService: FeatureFlagService = this.featureFlagService, featureFlagService: FeatureFlagService = this.featureFlagService,
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(), sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
@ -1514,6 +1521,7 @@ class MessageComposerPresenterTest {
isRichTextEditorEnabled: Boolean = true, isRichTextEditorEnabled: Boolean = true,
draftService: ComposerDraftService = FakeComposerDraftService(), draftService: ComposerDraftService = FakeComposerDraftService(),
) = MessageComposerPresenter( ) = MessageComposerPresenter(
navigator = navigator,
appCoroutineScope = coroutineScope, appCoroutineScope = coroutineScope,
room = room, room = room,
mediaPickerProvider = pickerProvider, mediaPickerProvider = pickerProvider,