Merge branch 'develop' into renovate/io.element.android-compound-android-0.x

This commit is contained in:
Benoit Marty
2024-11-22 07:50:38 +01:00
committed by GitHub
34 changed files with 282 additions and 223 deletions

View File

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

View File

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

View File

@@ -37,7 +37,6 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
import io.element.android.features.messages.impl.timeline.TimelineController
import io.element.android.features.messages.impl.timeline.TimelineEvents
import io.element.android.features.messages.impl.timeline.TimelinePresenter
import io.element.android.features.messages.impl.timeline.TimelineState
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
@@ -89,12 +88,12 @@ import timber.log.Timber
class MessagesPresenter @AssistedInject constructor(
@Assisted private val navigator: MessagesNavigator,
private val room: MatrixRoom,
private val composerPresenter: Presenter<MessageComposerState>,
@Assisted private val composerPresenter: Presenter<MessageComposerState>,
private val voiceMessageComposerPresenter: Presenter<VoiceMessageComposerState>,
timelinePresenterFactory: TimelinePresenter.Factory,
@Assisted private val timelinePresenter: Presenter<TimelineState>,
private val timelineProtectionPresenter: Presenter<TimelineProtectionState>,
private val identityChangeStatePresenter: Presenter<IdentityChangeState>,
private val actionListPresenterFactory: ActionListPresenter.Factory,
actionListPresenterFactory: ActionListPresenter.Factory,
private val customReactionPresenter: Presenter<CustomReactionState>,
private val reactionSummaryPresenter: Presenter<ReactionSummaryState>,
private val readReceiptBottomSheetPresenter: Presenter<ReadReceiptBottomSheetState>,
@@ -111,12 +110,15 @@ class MessagesPresenter @AssistedInject constructor(
private val permalinkParser: PermalinkParser,
private val analyticsService: AnalyticsService,
) : Presenter<MessagesState> {
private val timelinePresenter = timelinePresenterFactory.create(navigator = navigator)
private val actionListPresenter = actionListPresenterFactory.create(TimelineItemActionPostProcessor.Default)
@AssistedFactory
interface Factory {
fun create(navigator: MessagesNavigator): MessagesPresenter
fun create(
navigator: MessagesNavigator,
composerPresenter: Presenter<MessageComposerState>,
timelinePresenter: Presenter<TimelineState>,
): MessagesPresenter
}
@Composable

View File

@@ -32,10 +32,8 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.ActionListView
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.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.MessageComposerView
import io.element.android.features.messages.impl.messagecomposer.suggestions.SuggestionsPickerView
@@ -115,7 +111,6 @@ fun MessagesView(
onEventContentClick: (event: TimelineItem.Event) -> Boolean,
onUserDataClick: (UserId) -> Unit,
onLinkClick: (String) -> Unit,
onPreviewAttachments: (ImmutableList<Attachment>) -> Unit,
onSendLocationClick: () -> Unit,
onCreatePollClick: () -> Unit,
onJoinCallClick: () -> Unit,
@@ -129,11 +124,6 @@ fun MessagesView(
KeepScreenOn(state.voiceMessageComposerState.keepScreenOn)
AttachmentStateView(
state = state.composerState.attachmentsState,
onPreviewAttachments = onPreviewAttachments,
)
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
@@ -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
private fun MessagesViewContent(
state: MessagesState,
@@ -557,7 +531,6 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class)
onEventContentClick = { false },
onUserDataClick = {},
onLinkClick = {},
onPreviewAttachments = {},
onSendLocationClick = {},
onCreatePollClick = {},
onJoinCallClick = {},

View File

@@ -32,7 +32,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
import io.element.android.features.messages.impl.timeline.model.event.canBeCopied
import io.element.android.features.messages.impl.timeline.model.event.canBeForwarded
import io.element.android.features.messages.impl.timeline.model.event.canReact
@@ -155,18 +154,16 @@ class DefaultActionListPresenter @AssistedInject constructor(
add(TimelineItemAction.Forward)
}
if (timelineItem.isEditable) {
add(TimelineItemAction.Edit)
} else {
// Caption
if (timelineItem.isMine &&
timelineItem.content is TimelineItemEventContentWithAttachment &&
timelineItem.content !is TimelineItemVoiceContent) {
if (timelineItem.content is TimelineItemEventContentWithAttachment) {
// Caption
if (timelineItem.content.caption == null) {
add(TimelineItemAction.AddCaption)
} else {
add(TimelineItemAction.EditCaption)
add(TimelineItemAction.RemoveCaption)
}
} else {
add(TimelineItemAction.Edit)
}
}
if (canRedact && timelineItem.content is TimelineItemPollContent && !timelineItem.content.isEnded) {

View File

@@ -36,7 +36,6 @@ internal fun MessagesViewWithIdentityChangePreview(
onEventContentClick = { false },
onUserDataClick = {},
onLinkClick = {},
onPreviewAttachments = {},
onSendLocationClick = {},
onCreatePollClick = {},
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.sendfailure.resolve.ResolveVerifiedUserSendFailurePresenter
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.PinnedMessagesBannerState
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter
@@ -48,9 +46,6 @@ interface MessagesModule {
@Binds
fun bindTimelineProtectionPresenter(presenter: TimelineProtectionPresenter): Presenter<TimelineProtectionState>
@Binds
fun bindMessageComposerPresenter(presenter: MessageComposerPresenter): Presenter<MessageComposerState>
@Binds
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.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
@@ -26,8 +25,12 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.media3.common.MimeTypes
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.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.preview.error.sendAttachmentError
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.designsystem.utils.snackbar.SnackbarDispatcher
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.FeatureFlags
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.launch
import timber.log.Timber
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
import io.element.android.libraries.core.mimetype.MimeTypes.Any as AnyMimeTypes
@SingleIn(RoomScope::class)
class MessageComposerPresenter @Inject constructor(
class MessageComposerPresenter @AssistedInject constructor(
@Assisted private val navigator: MessagesNavigator,
private val appCoroutineScope: CoroutineScope,
private val room: MatrixRoom,
private val mediaPickerProvider: PickerProvider,
@@ -117,6 +117,11 @@ class MessageComposerPresenter @Inject constructor(
private val roomMemberProfilesCache: RoomMemberProfilesCache,
private val suggestionsProcessor: SuggestionsProcessor,
) : Presenter<MessageComposerState> {
@AssistedFactory
interface Factory {
fun create(navigator: MessagesNavigator): MessageComposerPresenter
}
private val cameraPermissionPresenter = permissionsPresenterFactory.create(Manifest.permission.CAMERA)
private var pendingEvent: MessageComposerEvents? = null
private val suggestionSearchTrigger = MutableStateFlow<Suggestion?>(null)
@@ -147,9 +152,6 @@ class MessageComposerPresenter @Inject constructor(
}
val cameraPermissionState = cameraPermissionPresenter.present()
val attachmentsState = remember {
mutableStateOf<AttachmentsState>(AttachmentsState.None)
}
val canShareLocation = remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
@@ -162,16 +164,16 @@ class MessageComposerPresenter @Inject constructor(
}
val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker { uri, mimeType ->
handlePickedMedia(attachmentsState, uri, mimeType)
handlePickedMedia(uri, mimeType)
}
val filesPicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes) { uri ->
handlePickedMedia(attachmentsState, uri)
handlePickedMedia(uri)
}
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker { uri ->
handlePickedMedia(attachmentsState, uri, MimeTypes.IMAGE_JPEG)
handlePickedMedia(uri, MimeTypes.IMAGE_JPEG)
}
val cameraVideoPicker = mediaPickerProvider.registerCameraVideoPicker { uri ->
handlePickedMedia(attachmentsState, uri, MimeTypes.VIDEO_MP4)
handlePickedMedia(uri, MimeTypes.VIDEO_MP4)
}
val isFullScreen = rememberSaveable {
mutableStateOf(false)
@@ -277,7 +279,6 @@ class MessageComposerPresenter @Inject constructor(
formattedFileSize = null
),
),
attachmentState = attachmentsState,
)
is MessageComposerEvents.SetMode -> {
localCoroutineScope.setMode(event.composerMode, markdownTextEditorState, richTextEditorState)
@@ -396,7 +397,6 @@ class MessageComposerPresenter @Inject constructor(
showTextFormatting = showTextFormatting,
canShareLocation = canShareLocation.value,
canCreatePoll = canCreatePoll.value,
attachmentsState = attachmentsState.value,
suggestions = suggestions.toPersistentList(),
resolveMentionDisplay = resolveMentionDisplay,
eventSink = { handleEvents(it) },
@@ -459,14 +459,12 @@ class MessageComposerPresenter @Inject constructor(
private fun CoroutineScope.sendAttachment(
attachment: Attachment,
attachmentState: MutableState<AttachmentsState>,
) = when (attachment) {
is Attachment.Media -> {
launch {
sendMedia(
uri = attachment.localMedia.uri,
mimeType = attachment.localMedia.info.mimeType,
attachmentState = attachmentState,
)
}
}
@@ -474,14 +472,10 @@ class MessageComposerPresenter @Inject constructor(
@UnstableApi
private fun handlePickedMedia(
attachmentsState: MutableState<AttachmentsState>,
uri: Uri?,
mimeType: String? = null,
) {
if (uri == null) {
attachmentsState.value = AttachmentsState.None
return
}
uri ?: return
val localMedia = localMediaFactory.createFromUri(
uri = uri,
mimeType = mimeType,
@@ -489,13 +483,12 @@ class MessageComposerPresenter @Inject constructor(
formattedFileSize = null
)
val mediaAttachment = Attachment.Media(localMedia)
attachmentsState.value = AttachmentsState.Previewing(persistentListOf(mediaAttachment))
navigator.onPreviewAttachment(persistentListOf(mediaAttachment))
}
private suspend fun sendMedia(
uri: Uri,
mimeType: String,
attachmentState: MutableState<AttachmentsState>,
) = runCatching {
mediaSender.sendMedia(
uri = uri,
@@ -503,12 +496,8 @@ class MessageComposerPresenter @Inject constructor(
progressCallback = null,
).getOrThrow()
}
.onSuccess {
attachmentState.value = AttachmentsState.None
}
.onFailure { cause ->
Timber.e(cause, "Failed to send attachment")
attachmentState.value = AttachmentsState.None
if (cause is CancellationException) {
throw cause
} else {

View File

@@ -7,9 +7,7 @@
package io.element.android.features.messages.impl.messagecomposer
import androidx.compose.runtime.Immutable
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.model.MessageComposerMode
import io.element.android.libraries.textcomposer.model.TextEditorState
@@ -25,14 +23,7 @@ data class MessageComposerState(
val showTextFormatting: Boolean,
val canShareLocation: Boolean,
val canCreatePoll: Boolean,
val attachmentsState: AttachmentsState,
val suggestions: ImmutableList<ResolvedSuggestion>,
val resolveMentionDisplay: (String, String) -> TextDisplay,
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,
canShareLocation: Boolean = true,
canCreatePoll: Boolean = true,
attachmentsState: AttachmentsState = AttachmentsState.None,
suggestions: ImmutableList<ResolvedSuggestion> = persistentListOf(),
eventSink: (MessageComposerEvents) -> Unit = {},
) = MessageComposerState(
@@ -42,7 +41,6 @@ fun aMessageComposerState(
showAttachmentSourcePicker = showAttachmentSourcePicker,
canShareLocation = canShareLocation,
canCreatePoll = canCreatePoll,
attachmentsState = attachmentsState,
suggestions = suggestions,
resolveMentionDisplay = { _, _ -> TextDisplay.Plain },
eventSink = eventSink,

View File

@@ -72,6 +72,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
import io.element.android.features.messages.impl.timeline.protection.mustBeProtected
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
import io.element.android.libraries.designsystem.components.EqualWidthColumn
import io.element.android.libraries.designsystem.components.avatar.Avatar
@@ -146,6 +147,13 @@ fun TimelineItemEventRow(
val coroutineScope = rememberCoroutineScope()
val interactionSource = remember { MutableInteractionSource() }
val onContentClick = if (event.mustBeProtected()) {
// In this case, let the content handle the click
{}
} else {
onEventClick
}
fun onUserDataClick() {
onUserDataClick(event.senderId)
}
@@ -178,7 +186,7 @@ fun TimelineItemEventRow(
isHighlighted = isHighlighted,
timelineRoomInfo = timelineRoomInfo,
interactionSource = interactionSource,
onContentClick = onEventClick,
onContentClick = onContentClick,
onLongClick = onLongClick,
inReplyToClick = ::inReplyToClick,
onUserDataClick = ::onUserDataClick,
@@ -212,7 +220,7 @@ fun TimelineItemEventRow(
isHighlighted = isHighlighted,
timelineRoomInfo = timelineRoomInfo,
interactionSource = interactionSource,
onContentClick = onEventClick,
onContentClick = onContentClick,
onLongClick = onLongClick,
inReplyToClick = ::inReplyToClick,
onUserDataClick = ::onUserDataClick,

View File

@@ -49,6 +49,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContentProvider
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
import io.element.android.features.messages.impl.timeline.protection.ProtectedView
import io.element.android.features.messages.impl.timeline.protection.coerceRatioWhenHidingContent
import io.element.android.libraries.designsystem.components.blurhash.blurHashBackground
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@@ -79,7 +80,7 @@ fun TimelineItemImageView(
}
TimelineItemAspectRatioBox(
modifier = containerModifier.blurHashBackground(content.blurhash, alpha = 0.9f),
aspectRatio = content.aspectRatio,
aspectRatio = coerceRatioWhenHidingContent(content.aspectRatio, hideMediaContent),
) {
ProtectedView(
hideContent = hideMediaContent,

View File

@@ -30,6 +30,7 @@ import coil.compose.AsyncImagePainter
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContentProvider
import io.element.android.features.messages.impl.timeline.protection.ProtectedView
import io.element.android.features.messages.impl.timeline.protection.coerceRatioWhenHidingContent
import io.element.android.libraries.designsystem.components.blurhash.blurHashBackground
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@@ -54,7 +55,7 @@ fun TimelineItemStickerView(
) {
TimelineItemAspectRatioBox(
modifier = Modifier.blurHashBackground(content.blurhash, alpha = 0.9f),
aspectRatio = content.aspectRatio,
aspectRatio = coerceRatioWhenHidingContent(content.aspectRatio, hideMediaContent),
minHeight = STICKER_SIZE_IN_DP,
maxHeight = STICKER_SIZE_IN_DP,
) {

View File

@@ -54,6 +54,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContentProvider
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVideoContent
import io.element.android.features.messages.impl.timeline.protection.ProtectedView
import io.element.android.features.messages.impl.timeline.protection.coerceRatioWhenHidingContent
import io.element.android.libraries.designsystem.components.blurhash.blurHashBackground
import io.element.android.libraries.designsystem.modifiers.roundedBackground
import io.element.android.libraries.designsystem.preview.ElementPreview
@@ -90,7 +91,7 @@ fun TimelineItemVideoView(
}
TimelineItemAspectRatioBox(
modifier = containerModifier.blurHashBackground(content.blurHash, alpha = 0.9f),
aspectRatio = content.aspectRatio,
aspectRatio = coerceRatioWhenHidingContent(content.aspectRatio, hideMediaContent),
contentAlignment = Alignment.Center,
) {
ProtectedView(

View File

@@ -0,0 +1,19 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.messages.impl.timeline.protection
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
class AspectRatioProvider : PreviewParameterProvider<Float?> {
override val values: Sequence<Float?> = sequenceOf(
null,
0.05f,
1f,
20f,
)
}

View File

@@ -13,7 +13,6 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
@@ -23,8 +22,10 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
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.timeline.components.event.TimelineItemAspectRatioBox
import io.element.android.libraries.designsystem.components.blurhash.blurHashBackground
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@@ -79,11 +80,12 @@ fun ProtectedView(
@PreviewsDayNight
@Composable
internal fun ProtectedViewPreview() = ElementPreview {
Box(
modifier = Modifier
.size(160.dp)
.blurHashBackground(A_BLUR_HASH)
internal fun ProtectedViewPreview(
@PreviewParameter(AspectRatioProvider::class) aspectRatio: Float?,
) = ElementPreview {
TimelineItemAspectRatioBox(
modifier = Modifier.blurHashBackground(A_BLUR_HASH, alpha = 0.9f),
aspectRatio = coerceRatioWhenHidingContent(aspectRatio, true),
) {
ProtectedView(
hideContent = true,

View File

@@ -0,0 +1,19 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.messages.impl.timeline.protection
fun coerceRatioWhenHidingContent(aspectRatio: Float?, hideContent: Boolean): Float? {
return if (hideContent) {
aspectRatio?.coerceIn(
minimumValue = 0.5f,
maximumValue = 3f
)
} else {
aspectRatio
}
}

View File

@@ -23,8 +23,6 @@ import im.vector.app.features.analytics.plan.Composer
import io.element.android.features.messages.api.MessageComposerContext
import io.element.android.features.messages.impl.voicemessages.VoiceMessageException
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.permissions.api.PermissionsEvents
import io.element.android.libraries.permissions.api.PermissionsPresenter
@@ -45,7 +43,6 @@ import javax.inject.Inject
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
@SingleIn(RoomScope::class)
class VoiceMessageComposerPresenter @Inject constructor(
private val appCoroutineScope: CoroutineScope,
private val voiceRecorder: VoiceRecorder,

View File

@@ -7,36 +7,37 @@
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.UserId
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.tests.testutils.lambda.lambdaError
import kotlinx.collections.immutable.ImmutableList
class FakeMessagesNavigator : MessagesNavigator {
var onShowEventDebugInfoClickedCount = 0
private set
var onForwardEventClickedCount = 0
private set
var onReportContentClickedCount = 0
private set
var onEditPollClickedCount = 0
private set
class FakeMessagesNavigator(
private val onShowEventDebugInfoClickLambda: (eventId: EventId?, debugInfo: TimelineItemDebugInfo) -> Unit = { _, _ -> lambdaError() },
private val onForwardEventClickLambda: (eventId: EventId) -> Unit = { _ -> lambdaError() },
private val onReportContentClickLambda: (eventId: EventId, senderId: UserId) -> Unit = { _, _ -> lambdaError() },
private val onEditPollClickLambda: (eventId: EventId) -> Unit = { _ -> lambdaError() },
private val onPreviewAttachmentLambda: (attachments: ImmutableList<Attachment>) -> Unit = { _ -> lambdaError() },
) : MessagesNavigator {
override fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
onShowEventDebugInfoClickedCount++
onShowEventDebugInfoClickLambda(eventId, debugInfo)
}
override fun onForwardEventClick(eventId: EventId) {
onForwardEventClickedCount++
onForwardEventClickLambda(eventId)
}
override fun onReportContentClick(eventId: EventId, senderId: UserId) {
onReportContentClickedCount++
onReportContentClickLambda(eventId, senderId)
}
override fun onEditPollClick(eventId: EventId) {
onEditPollClickedCount++
onEditPollClickLambda(eventId)
}
override fun onPreviewAttachment(attachments: ImmutableList<Attachment>) {
onPreviewAttachmentLambda(attachments)
}
}

View File

@@ -23,8 +23,8 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer
import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
import io.element.android.features.messages.impl.pinned.banner.aLoadedPinnedMessagesBannerState
import io.element.android.features.messages.impl.timeline.TimelineController
import io.element.android.features.messages.impl.timeline.TimelinePresenter
import io.element.android.features.messages.impl.timeline.createTimelinePresenter
import io.element.android.features.messages.impl.timeline.TimelineEvents
import io.element.android.features.messages.impl.timeline.aTimelineState
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
@@ -36,8 +36,6 @@ import io.element.android.features.messages.impl.timeline.protection.aTimelinePr
import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState
import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvider
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.features.poll.api.actions.EndPollAction
import io.element.android.features.poll.test.actions.FakeEndPollAction
import io.element.android.features.roomcall.api.aStandByCallState
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
import io.element.android.libraries.architecture.AsyncData
@@ -56,6 +54,7 @@ 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.TimelineItemDebugInfo
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
@@ -65,12 +64,14 @@ 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_THROWABLE
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.matrix.test.timeline.aTimelineItemDebugInfo
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.textcomposer.model.TextEditorState
@@ -217,7 +218,10 @@ class MessagesPresenterTest {
@Test
fun `present - handle action forward`() = runTest {
val navigator = FakeMessagesNavigator()
val onForwardEventClickLambda = lambdaRecorder<EventId, Unit> { }
val navigator = FakeMessagesNavigator(
onForwardEventClickLambda = onForwardEventClickLambda,
)
val presenter = createMessagesPresenter(navigator = navigator)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -225,7 +229,7 @@ class MessagesPresenterTest {
val initialState = awaitItem()
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Forward, aMessageEvent()))
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
assertThat(navigator.onForwardEventClickedCount).isEqualTo(1)
onForwardEventClickLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID))
}
}
@@ -452,7 +456,10 @@ class MessagesPresenterTest {
@Test
fun `present - handle action edit poll`() = runTest {
val navigator = FakeMessagesNavigator()
val onEditPollClickLambda = lambdaRecorder<EventId, Unit> { }
val navigator = FakeMessagesNavigator(
onEditPollClickLambda = onEditPollClickLambda
)
val presenter = createMessagesPresenter(navigator = navigator)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -460,22 +467,21 @@ class MessagesPresenterTest {
val initialState = awaitItem()
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent(content = aTimelineItemPollContent())))
awaitItem()
assertThat(navigator.onEditPollClickedCount).isEqualTo(1)
onEditPollClickLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID))
}
}
@Test
fun `present - handle action end poll`() = runTest {
val endPollAction = FakeEndPollAction()
val presenter = createMessagesPresenter(endPollAction = endPollAction)
val timelineEventSink = EventsRecorder<TimelineEvents>()
val presenter = createMessagesPresenter(timelineEventSink = timelineEventSink)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
endPollAction.verifyExecutionCount(0)
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.EndPoll, aMessageEvent(content = aTimelineItemPollContent())))
delay(1)
endPollAction.verifyExecutionCount(1)
timelineEventSink.assertSingle(TimelineEvents.EndPoll(AN_EVENT_ID))
cancelAndIgnoreRemainingEvents()
}
}
@@ -516,7 +522,10 @@ class MessagesPresenterTest {
@Test
fun `present - handle action report content`() = runTest {
val navigator = FakeMessagesNavigator()
val onReportContentClickLambda = lambdaRecorder { _: EventId, _: UserId -> }
val navigator = FakeMessagesNavigator(
onReportContentClickLambda = onReportContentClickLambda
)
val presenter = createMessagesPresenter(navigator = navigator)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -524,7 +533,7 @@ class MessagesPresenterTest {
val initialState = awaitItem()
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.ReportContent, aMessageEvent()))
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
assertThat(navigator.onReportContentClickedCount).isEqualTo(1)
onReportContentClickLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID), value(A_USER_ID))
}
}
@@ -542,7 +551,10 @@ class MessagesPresenterTest {
@Test
fun `present - handle action show developer info`() = runTest {
val navigator = FakeMessagesNavigator()
val onShowEventDebugInfoClickLambda = lambdaRecorder { _: EventId?, _: TimelineItemDebugInfo -> }
val navigator = FakeMessagesNavigator(
onShowEventDebugInfoClickLambda = onShowEventDebugInfoClickLambda
)
val presenter = createMessagesPresenter(navigator = navigator)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -550,7 +562,7 @@ class MessagesPresenterTest {
val initialState = awaitItem()
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.ViewSource, aMessageEvent()))
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
assertThat(navigator.onShowEventDebugInfoClickedCount).isEqualTo(1)
onShowEventDebugInfoClickLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID), value(aTimelineItemDebugInfo()))
}
}
@@ -1087,7 +1099,7 @@ class MessagesPresenterTest {
navigator: FakeMessagesNavigator = FakeMessagesNavigator(),
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
endPollAction: EndPollAction = FakeEndPollAction(),
timelineEventSink: (TimelineEvents) -> Unit = {},
permalinkParser: PermalinkParser = FakePermalinkParser(),
messageComposerPresenter: Presenter<MessageComposerState> = Presenter {
aMessageComposerState(
@@ -1097,19 +1109,12 @@ class MessagesPresenterTest {
},
actionListEventSink: (ActionListEvents) -> Unit = {},
): MessagesPresenter {
val timelinePresenterFactory = object : TimelinePresenter.Factory {
override fun create(navigator: MessagesNavigator): TimelinePresenter {
return createTimelinePresenter(
endPollAction = endPollAction,
)
}
}
val featureFlagService = FakeFeatureFlagService()
return MessagesPresenter(
room = matrixRoom,
composerPresenter = messageComposerPresenter,
voiceMessageComposerPresenter = { aVoiceMessageComposerState() },
timelinePresenterFactory = timelinePresenterFactory,
timelinePresenter = { aTimelineState(eventSink = timelineEventSink) },
timelineProtectionPresenter = { aTimelineProtectionState() },
actionListPresenterFactory = FakeActionListPresenter.Factory(actionListEventSink),
customReactionPresenter = { aCustomReactionState() },

View File

@@ -33,7 +33,6 @@ import io.element.android.features.messages.impl.actionlist.ActionListEvents
import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.actionlist.anActionListState
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.sendfailure.VerifiedUserSendFailure
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.aChangedIdentitySendFailure
import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
@@ -64,7 +63,6 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.pressBack
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import org.junit.Rule
import org.junit.Test
@@ -514,7 +512,6 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMessa
onEventClick: (event: TimelineItem.Event) -> Boolean = EnsureNeverCalledWithParamAndResult(),
onUserDataClick: (UserId) -> Unit = EnsureNeverCalledWithParam(),
onLinkClick: (String) -> Unit = EnsureNeverCalledWithParam(),
onPreviewAttachments: (ImmutableList<Attachment>) -> Unit = EnsureNeverCalledWithParam(),
onSendLocationClick: () -> Unit = EnsureNeverCalled(),
onCreatePollClick: () -> Unit = EnsureNeverCalled(),
onJoinCallClick: () -> Unit = EnsureNeverCalled(),
@@ -532,7 +529,6 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMessa
onEventContentClick = onEventClick,
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,
onPreviewAttachments = onPreviewAttachments,
onSendLocationClick = onSendLocationClick,
onCreatePollClick = onCreatePollClick,
onJoinCallClick = onJoinCallClick,

View File

@@ -521,7 +521,7 @@ class ActionListPresenterTest {
val initialState = awaitItem()
val messageEvent = aMessageEvent(
isMine = true,
isEditable = false,
isEditable = true,
content = aTimelineItemImageContent(),
)
initialState.eventSink.invoke(
@@ -567,7 +567,7 @@ class ActionListPresenterTest {
val initialState = awaitItem()
val messageEvent = aMessageEvent(
isMine = true,
isEditable = false,
isEditable = true,
content = aTimelineItemImageContent(
caption = A_CAPTION,
),

View File

@@ -18,6 +18,9 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.Composer
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.attachments.Attachment
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.messagecomposer.suggestions.SuggestionsProcessor
@@ -91,6 +94,7 @@ import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.waitForPredicate
import io.mockk.mockk
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -133,7 +137,6 @@ class MessageComposerPresenterTest {
assertThat(initialState.mode).isEqualTo(MessageComposerMode.Normal)
assertThat(initialState.showAttachmentSourcePicker).isFalse()
assertThat(initialState.canShareLocation).isTrue()
assertThat(initialState.attachmentsState).isEqualTo(AttachmentsState.None)
}
}
@@ -685,7 +688,15 @@ class MessageComposerPresenterTest {
val room = FakeMatrixRoom(
typingNoticeResult = { Result.success(Unit) }
)
val presenter = createPresenter(this, room = room)
val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList<Attachment> -> }
val navigator = FakeMessagesNavigator(
onPreviewAttachmentLambda = onPreviewAttachmentLambda
)
val presenter = createPresenter(
coroutineScope = this,
room = room,
navigator = navigator,
)
pickerProvider.givenMimeType(MimeTypes.Images)
mediaPreProcessor.givenResult(
Result.success(
@@ -709,9 +720,7 @@ class MessageComposerPresenterTest {
}.test {
val initialState = awaitFirstItem()
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery)
val previewingState = awaitItem()
assertThat(previewingState.showAttachmentSourcePicker).isFalse()
assertThat(previewingState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java)
onPreviewAttachmentLambda.assertions().isCalledOnce()
}
}
@@ -720,7 +729,15 @@ class MessageComposerPresenterTest {
val room = FakeMatrixRoom(
typingNoticeResult = { Result.success(Unit) }
)
val presenter = createPresenter(this, room = room)
val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList<Attachment> -> }
val navigator = FakeMessagesNavigator(
onPreviewAttachmentLambda = onPreviewAttachmentLambda
)
val presenter = createPresenter(
coroutineScope = this,
room = room,
navigator = navigator,
)
pickerProvider.givenMimeType(MimeTypes.Videos)
mediaPreProcessor.givenResult(
Result.success(
@@ -745,9 +762,7 @@ class MessageComposerPresenterTest {
}.test {
val initialState = awaitFirstItem()
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery)
val previewingState = awaitItem()
assertThat(previewingState.showAttachmentSourcePicker).isFalse()
assertThat(previewingState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java)
onPreviewAttachmentLambda.assertions().isCalledOnce()
}
}
@@ -772,15 +787,21 @@ class MessageComposerPresenterTest {
val room = FakeMatrixRoom(
typingNoticeResult = { Result.success(Unit) }
)
val presenter = createPresenter(this, room = room)
val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList<Attachment> -> }
val navigator = FakeMessagesNavigator(
onPreviewAttachmentLambda = onPreviewAttachmentLambda
)
val presenter = createPresenter(
coroutineScope = this,
room = room,
navigator = navigator,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles)
val sendingState = awaitItem()
assertThat(sendingState.showAttachmentSourcePicker).isFalse()
assertThat(sendingState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java)
onPreviewAttachmentLambda.assertions().isCalledOnce()
}
}
@@ -828,19 +849,22 @@ class MessageComposerPresenterTest {
typingNoticeResult = { Result.success(Unit) }
)
val permissionPresenter = FakePermissionsPresenter().apply { setPermissionGranted() }
val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList<Attachment> -> }
val navigator = FakeMessagesNavigator(
onPreviewAttachmentLambda = onPreviewAttachmentLambda
)
val presenter = createPresenter(
this,
coroutineScope = this,
room = room,
permissionPresenter = permissionPresenter,
navigator = navigator,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.PhotoFromCamera)
val finalState = awaitItem()
assertThat(finalState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java)
cancelAndIgnoreRemainingEvents()
onPreviewAttachmentLambda.assertions().isCalledOnce()
}
}
@@ -850,23 +874,23 @@ class MessageComposerPresenterTest {
typingNoticeResult = { Result.success(Unit) }
)
val permissionPresenter = FakePermissionsPresenter()
val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList<Attachment> -> }
val navigator = FakeMessagesNavigator(
onPreviewAttachmentLambda = onPreviewAttachmentLambda
)
val presenter = createPresenter(
this,
coroutineScope = this,
room = room,
permissionPresenter = permissionPresenter,
navigator = navigator,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.PhotoFromCamera)
val permissionState = awaitItem()
assertThat(permissionState.showAttachmentSourcePicker).isFalse()
assertThat(permissionState.attachmentsState).isInstanceOf(AttachmentsState.None::class.java)
permissionPresenter.setPermissionGranted()
skipItems(1)
val finalState = awaitItem()
assertThat(finalState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java)
onPreviewAttachmentLambda.assertions().isCalledOnce()
cancelAndIgnoreRemainingEvents()
}
}
@@ -877,19 +901,22 @@ class MessageComposerPresenterTest {
typingNoticeResult = { Result.success(Unit) }
)
val permissionPresenter = FakePermissionsPresenter().apply { setPermissionGranted() }
val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList<Attachment> -> }
val navigator = FakeMessagesNavigator(
onPreviewAttachmentLambda = onPreviewAttachmentLambda
)
val presenter = createPresenter(
this,
coroutineScope = this,
room = room,
permissionPresenter = permissionPresenter,
navigator = navigator,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.VideoFromCamera)
val finalState = awaitItem()
assertThat(finalState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java)
cancelAndIgnoreRemainingEvents()
onPreviewAttachmentLambda.assertions().isCalledOnce()
}
}
@@ -899,10 +926,15 @@ class MessageComposerPresenterTest {
typingNoticeResult = { Result.success(Unit) }
)
val permissionPresenter = FakePermissionsPresenter()
val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList<Attachment> -> }
val navigator = FakeMessagesNavigator(
onPreviewAttachmentLambda = onPreviewAttachmentLambda
)
val presenter = createPresenter(
this,
coroutineScope = this,
room = room,
permissionPresenter = permissionPresenter,
navigator = navigator,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -911,12 +943,9 @@ class MessageComposerPresenterTest {
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.VideoFromCamera)
val permissionState = awaitItem()
assertThat(permissionState.showAttachmentSourcePicker).isFalse()
assertThat(permissionState.attachmentsState).isInstanceOf(AttachmentsState.None::class.java)
permissionPresenter.setPermissionGranted()
skipItems(1)
val finalState = awaitItem()
assertThat(finalState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java)
cancelAndIgnoreRemainingEvents()
onPreviewAttachmentLambda.assertions().isCalledOnce()
}
}
@@ -1500,6 +1529,7 @@ class MessageComposerPresenterTest {
room: MatrixRoom = FakeMatrixRoom(
typingNoticeResult = { Result.success(Unit) }
),
navigator: MessagesNavigator = FakeMessagesNavigator(),
pickerProvider: PickerProvider = this.pickerProvider,
featureFlagService: FeatureFlagService = this.featureFlagService,
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
@@ -1514,6 +1544,7 @@ class MessageComposerPresenterTest {
isRichTextEditorEnabled: Boolean = true,
draftService: ComposerDraftService = FakeComposerDraftService(),
) = MessageComposerPresenter(
navigator = navigator,
appCoroutineScope = coroutineScope,
room = room,
mediaPickerProvider = pickerProvider,

View File

@@ -431,7 +431,10 @@ import kotlin.time.Duration.Companion.seconds
@Test
fun `present - PollEditClicked event navigates`() = runTest {
val navigator = FakeMessagesNavigator()
val onEditPollClickLambda = lambdaRecorder { _: EventId -> }
val navigator = FakeMessagesNavigator(
onEditPollClickLambda = onEditPollClickLambda
)
val presenter = createTimelinePresenter(
messagesNavigator = navigator,
)
@@ -439,7 +442,7 @@ import kotlin.time.Duration.Companion.seconds
presenter.present()
}.test {
awaitFirstItem().eventSink(TimelineEvents.EditPoll(AN_EVENT_ID))
assertThat(navigator.onEditPollClickedCount).isEqualTo(1)
onEditPollClickLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID))
}
}
@@ -657,35 +660,35 @@ import kotlin.time.Duration.Companion.seconds
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
return awaitItem()
}
}
internal fun TestScope.createTimelinePresenter(
timeline: Timeline = FakeTimeline(),
room: FakeMatrixRoom = FakeMatrixRoom(
liveTimeline = timeline,
canUserSendMessageResult = { _, _ -> Result.success(true) }
),
redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(),
endPollAction: EndPollAction = FakeEndPollAction(),
sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(),
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(),
): TimelinePresenter {
return TimelinePresenter(
timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(),
room = room,
dispatchers = testCoroutineDispatchers(),
appScope = this,
navigator = messagesNavigator,
redactedVoiceMessageManager = redactedVoiceMessageManager,
endPollAction = endPollAction,
sendPollResponseAction = sendPollResponseAction,
sessionPreferencesStore = sessionPreferencesStore,
timelineItemIndexer = timelineItemIndexer,
timelineController = TimelineController(room),
resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() },
typingNotificationPresenter = { aTypingNotificationState() },
roomCallStatePresenter = { aStandByCallState() },
)
private fun TestScope.createTimelinePresenter(
timeline: Timeline = FakeTimeline(),
room: FakeMatrixRoom = FakeMatrixRoom(
liveTimeline = timeline,
canUserSendMessageResult = { _, _ -> Result.success(true) }
),
redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(),
endPollAction: EndPollAction = FakeEndPollAction(),
sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(),
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(),
): TimelinePresenter {
return TimelinePresenter(
timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(),
room = room,
dispatchers = testCoroutineDispatchers(),
appScope = this,
navigator = messagesNavigator,
redactedVoiceMessageManager = redactedVoiceMessageManager,
endPollAction = endPollAction,
sendPollResponseAction = sendPollResponseAction,
sessionPreferencesStore = sessionPreferencesStore,
timelineItemIndexer = timelineItemIndexer,
timelineController = TimelineController(room),
resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() },
typingNotificationPresenter = { aTypingNotificationState() },
roomCallStatePresenter = { aStandByCallState() },
)
}
}

View File

@@ -154,7 +154,7 @@ test_konsist = "com.lemonappdev:konsist:0.16.1"
test_turbine = "app.cash.turbine:turbine:1.2.0"
test_truth = "com.google.truth:truth:1.4.4"
test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.18"
test_robolectric = "org.robolectric:robolectric:4.14"
test_robolectric = "org.robolectric:robolectric:4.14.1"
test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "appyx" }
test_composable_preview_scanner = "com.github.sergio-sastre.ComposablePreviewScanner:android:0.1.2"

View File

@@ -48,6 +48,9 @@ class KonsistClassNameTest {
Konsist.scopeFromProduction()
.classes()
.withAllParentsOf(PreviewParameterProvider::class)
.withoutName(
"AspectRatioProvider",
)
.also {
// Check that classes are actually found
assertThat(it.size).isGreaterThan(100)