Merge branch 'develop' into renovate/io.element.android-compound-android-0.x
This commit is contained in:
commit
7d7d92aa27
34 changed files with 282 additions and 223 deletions
|
|
@ -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>)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,9 @@ 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.TimelinePresenter
|
||||||
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
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
|
@ -60,12 +62,18 @@ 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,
|
||||||
|
timelinePresenterFactory: TimelinePresenter.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),
|
||||||
|
timelinePresenter = timelinePresenterFactory.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 +122,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 +182,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 +221,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,
|
||||||
|
|
|
||||||
|
|
@ -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.pinned.banner.PinnedMessagesBannerState
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineController
|
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.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.TimelineState
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
|
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
|
||||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
|
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
|
||||||
|
|
@ -89,12 +88,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,
|
@Assisted private val timelinePresenter: Presenter<TimelineState>,
|
||||||
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>,
|
||||||
|
|
@ -111,12 +110,15 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
private val permalinkParser: PermalinkParser,
|
private val permalinkParser: PermalinkParser,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
) : Presenter<MessagesState> {
|
) : Presenter<MessagesState> {
|
||||||
private val timelinePresenter = timelinePresenterFactory.create(navigator = navigator)
|
|
||||||
private val actionListPresenter = actionListPresenterFactory.create(TimelineItemActionPostProcessor.Default)
|
private val actionListPresenter = actionListPresenterFactory.create(TimelineItemActionPostProcessor.Default)
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(navigator: MessagesNavigator): MessagesPresenter
|
fun create(
|
||||||
|
navigator: MessagesNavigator,
|
||||||
|
composerPresenter: Presenter<MessageComposerState>,
|
||||||
|
timelinePresenter: Presenter<TimelineState>,
|
||||||
|
): MessagesPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -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 = {},
|
||||||
|
|
|
||||||
|
|
@ -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.TimelineItemPollContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
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.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.canBeCopied
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.canBeForwarded
|
import io.element.android.features.messages.impl.timeline.model.event.canBeForwarded
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.canReact
|
import io.element.android.features.messages.impl.timeline.model.event.canReact
|
||||||
|
|
@ -155,18 +154,16 @@ class DefaultActionListPresenter @AssistedInject constructor(
|
||||||
add(TimelineItemAction.Forward)
|
add(TimelineItemAction.Forward)
|
||||||
}
|
}
|
||||||
if (timelineItem.isEditable) {
|
if (timelineItem.isEditable) {
|
||||||
add(TimelineItemAction.Edit)
|
if (timelineItem.content is TimelineItemEventContentWithAttachment) {
|
||||||
} else {
|
// Caption
|
||||||
// Caption
|
|
||||||
if (timelineItem.isMine &&
|
|
||||||
timelineItem.content is TimelineItemEventContentWithAttachment &&
|
|
||||||
timelineItem.content !is TimelineItemVoiceContent) {
|
|
||||||
if (timelineItem.content.caption == null) {
|
if (timelineItem.content.caption == null) {
|
||||||
add(TimelineItemAction.AddCaption)
|
add(TimelineItemAction.AddCaption)
|
||||||
} else {
|
} else {
|
||||||
add(TimelineItemAction.EditCaption)
|
add(TimelineItemAction.EditCaption)
|
||||||
add(TimelineItemAction.RemoveCaption)
|
add(TimelineItemAction.RemoveCaption)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
add(TimelineItemAction.Edit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (canRedact && timelineItem.content is TimelineItemPollContent && !timelineItem.content.isEnded) {
|
if (canRedact && timelineItem.content is TimelineItemPollContent && !timelineItem.content.isEnded) {
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ internal fun MessagesViewWithIdentityChangePreview(
|
||||||
onEventContentClick = { false },
|
onEventContentClick = { false },
|
||||||
onUserDataClick = {},
|
onUserDataClick = {},
|
||||||
onLinkClick = {},
|
onLinkClick = {},
|
||||||
onPreviewAttachments = {},
|
|
||||||
onSendLocationClick = {},
|
onSendLocationClick = {},
|
||||||
onCreatePollClick = {},
|
onCreatePollClick = {},
|
||||||
onJoinCallClick = {},
|
onJoinCallClick = {},
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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.model.event.aTimelineItemTextContent
|
||||||
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent
|
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.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.colors.AvatarColorsProvider
|
||||||
import io.element.android.libraries.designsystem.components.EqualWidthColumn
|
import io.element.android.libraries.designsystem.components.EqualWidthColumn
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
|
|
@ -146,6 +147,13 @@ fun TimelineItemEventRow(
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
||||||
|
val onContentClick = if (event.mustBeProtected()) {
|
||||||
|
// In this case, let the content handle the click
|
||||||
|
{}
|
||||||
|
} else {
|
||||||
|
onEventClick
|
||||||
|
}
|
||||||
|
|
||||||
fun onUserDataClick() {
|
fun onUserDataClick() {
|
||||||
onUserDataClick(event.senderId)
|
onUserDataClick(event.senderId)
|
||||||
}
|
}
|
||||||
|
|
@ -178,7 +186,7 @@ fun TimelineItemEventRow(
|
||||||
isHighlighted = isHighlighted,
|
isHighlighted = isHighlighted,
|
||||||
timelineRoomInfo = timelineRoomInfo,
|
timelineRoomInfo = timelineRoomInfo,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
onContentClick = onEventClick,
|
onContentClick = onContentClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
inReplyToClick = ::inReplyToClick,
|
inReplyToClick = ::inReplyToClick,
|
||||||
onUserDataClick = ::onUserDataClick,
|
onUserDataClick = ::onUserDataClick,
|
||||||
|
|
@ -212,7 +220,7 @@ fun TimelineItemEventRow(
|
||||||
isHighlighted = isHighlighted,
|
isHighlighted = isHighlighted,
|
||||||
timelineRoomInfo = timelineRoomInfo,
|
timelineRoomInfo = timelineRoomInfo,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
onContentClick = onEventClick,
|
onContentClick = onContentClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
inReplyToClick = ::inReplyToClick,
|
inReplyToClick = ::inReplyToClick,
|
||||||
onUserDataClick = ::onUserDataClick,
|
onUserDataClick = ::onUserDataClick,
|
||||||
|
|
|
||||||
|
|
@ -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.TimelineItemImageContentProvider
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
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.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.components.blurhash.blurHashBackground
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
|
@ -79,7 +80,7 @@ fun TimelineItemImageView(
|
||||||
}
|
}
|
||||||
TimelineItemAspectRatioBox(
|
TimelineItemAspectRatioBox(
|
||||||
modifier = containerModifier.blurHashBackground(content.blurhash, alpha = 0.9f),
|
modifier = containerModifier.blurHashBackground(content.blurhash, alpha = 0.9f),
|
||||||
aspectRatio = content.aspectRatio,
|
aspectRatio = coerceRatioWhenHidingContent(content.aspectRatio, hideMediaContent),
|
||||||
) {
|
) {
|
||||||
ProtectedView(
|
ProtectedView(
|
||||||
hideContent = hideMediaContent,
|
hideContent = hideMediaContent,
|
||||||
|
|
|
||||||
|
|
@ -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.TimelineItemStickerContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContentProvider
|
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.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.components.blurhash.blurHashBackground
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
|
@ -54,7 +55,7 @@ fun TimelineItemStickerView(
|
||||||
) {
|
) {
|
||||||
TimelineItemAspectRatioBox(
|
TimelineItemAspectRatioBox(
|
||||||
modifier = Modifier.blurHashBackground(content.blurhash, alpha = 0.9f),
|
modifier = Modifier.blurHashBackground(content.blurhash, alpha = 0.9f),
|
||||||
aspectRatio = content.aspectRatio,
|
aspectRatio = coerceRatioWhenHidingContent(content.aspectRatio, hideMediaContent),
|
||||||
minHeight = STICKER_SIZE_IN_DP,
|
minHeight = STICKER_SIZE_IN_DP,
|
||||||
maxHeight = STICKER_SIZE_IN_DP,
|
maxHeight = STICKER_SIZE_IN_DP,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -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.TimelineItemVideoContentProvider
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVideoContent
|
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.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.components.blurhash.blurHashBackground
|
||||||
import io.element.android.libraries.designsystem.modifiers.roundedBackground
|
import io.element.android.libraries.designsystem.modifiers.roundedBackground
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
|
@ -90,7 +91,7 @@ fun TimelineItemVideoView(
|
||||||
}
|
}
|
||||||
TimelineItemAspectRatioBox(
|
TimelineItemAspectRatioBox(
|
||||||
modifier = containerModifier.blurHashBackground(content.blurHash, alpha = 0.9f),
|
modifier = containerModifier.blurHashBackground(content.blurHash, alpha = 0.9f),
|
||||||
aspectRatio = content.aspectRatio,
|
aspectRatio = coerceRatioWhenHidingContent(content.aspectRatio, hideMediaContent),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
ProtectedView(
|
ProtectedView(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -13,7 +13,6 @@ import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.runtime.Composable
|
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.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
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.components.blurhash.blurHashBackground
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
|
@ -79,11 +80,12 @@ fun ProtectedView(
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ProtectedViewPreview() = ElementPreview {
|
internal fun ProtectedViewPreview(
|
||||||
Box(
|
@PreviewParameter(AspectRatioProvider::class) aspectRatio: Float?,
|
||||||
modifier = Modifier
|
) = ElementPreview {
|
||||||
.size(160.dp)
|
TimelineItemAspectRatioBox(
|
||||||
.blurHashBackground(A_BLUR_HASH)
|
modifier = Modifier.blurHashBackground(A_BLUR_HASH, alpha = 0.9f),
|
||||||
|
aspectRatio = coerceRatioWhenHidingContent(aspectRatio, true),
|
||||||
) {
|
) {
|
||||||
ProtectedView(
|
ProtectedView(
|
||||||
hideContent = true,
|
hideContent = true,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.api.MessageComposerContext
|
||||||
import io.element.android.features.messages.impl.voicemessages.VoiceMessageException
|
import io.element.android.features.messages.impl.voicemessages.VoiceMessageException
|
||||||
import io.element.android.libraries.architecture.Presenter
|
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.mediaupload.api.MediaSender
|
||||||
import io.element.android.libraries.permissions.api.PermissionsEvents
|
import io.element.android.libraries.permissions.api.PermissionsEvents
|
||||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||||
|
|
@ -45,7 +43,6 @@ import javax.inject.Inject
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@SingleIn(RoomScope::class)
|
|
||||||
class VoiceMessageComposerPresenter @Inject constructor(
|
class VoiceMessageComposerPresenter @Inject constructor(
|
||||||
private val appCoroutineScope: CoroutineScope,
|
private val appCoroutineScope: CoroutineScope,
|
||||||
private val voiceRecorder: VoiceRecorder,
|
private val voiceRecorder: VoiceRecorder,
|
||||||
|
|
|
||||||
|
|
@ -7,36 +7,37 @@
|
||||||
|
|
||||||
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 io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
class FakeMessagesNavigator : MessagesNavigator {
|
class FakeMessagesNavigator(
|
||||||
var onShowEventDebugInfoClickedCount = 0
|
private val onShowEventDebugInfoClickLambda: (eventId: EventId?, debugInfo: TimelineItemDebugInfo) -> Unit = { _, _ -> lambdaError() },
|
||||||
private set
|
private val onForwardEventClickLambda: (eventId: EventId) -> Unit = { _ -> lambdaError() },
|
||||||
|
private val onReportContentClickLambda: (eventId: EventId, senderId: UserId) -> Unit = { _, _ -> lambdaError() },
|
||||||
var onForwardEventClickedCount = 0
|
private val onEditPollClickLambda: (eventId: EventId) -> Unit = { _ -> lambdaError() },
|
||||||
private set
|
private val onPreviewAttachmentLambda: (attachments: ImmutableList<Attachment>) -> Unit = { _ -> lambdaError() },
|
||||||
|
) : MessagesNavigator {
|
||||||
var onReportContentClickedCount = 0
|
|
||||||
private set
|
|
||||||
|
|
||||||
var onEditPollClickedCount = 0
|
|
||||||
private set
|
|
||||||
|
|
||||||
override fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
|
override fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
|
||||||
onShowEventDebugInfoClickedCount++
|
onShowEventDebugInfoClickLambda(eventId, debugInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onForwardEventClick(eventId: EventId) {
|
override fun onForwardEventClick(eventId: EventId) {
|
||||||
onForwardEventClickedCount++
|
onForwardEventClickLambda(eventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReportContentClick(eventId: EventId, senderId: UserId) {
|
override fun onReportContentClick(eventId: EventId, senderId: UserId) {
|
||||||
onReportContentClickedCount++
|
onReportContentClickLambda(eventId, senderId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEditPollClick(eventId: EventId) {
|
override fun onEditPollClick(eventId: EventId) {
|
||||||
onEditPollClickedCount++
|
onEditPollClickLambda(eventId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPreviewAttachment(attachments: ImmutableList<Attachment>) {
|
||||||
|
onPreviewAttachmentLambda(attachments)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.messagecomposer.aMessageComposerState
|
||||||
import io.element.android.features.messages.impl.pinned.banner.aLoadedPinnedMessagesBannerState
|
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.TimelineController
|
||||||
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||||
import io.element.android.features.messages.impl.timeline.createTimelinePresenter
|
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.TimelineItemFileContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
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.impl.voicemessages.composer.aVoiceMessageComposerState
|
||||||
import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvider
|
import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvider
|
||||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
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.features.roomcall.api.aStandByCallState
|
||||||
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
|
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
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.MatrixRoomMembersState
|
||||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
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.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.EventOrTransactionId
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
|
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
|
||||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
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
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
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_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.core.aBuildMeta
|
||||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
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.FakeMatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
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.room.aRoomMember
|
||||||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
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.matrix.ui.messages.reply.InReplyToDetails
|
||||||
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
|
||||||
|
|
@ -217,7 +218,10 @@ class MessagesPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - handle action forward`() = runTest {
|
fun `present - handle action forward`() = runTest {
|
||||||
val navigator = FakeMessagesNavigator()
|
val onForwardEventClickLambda = lambdaRecorder<EventId, Unit> { }
|
||||||
|
val navigator = FakeMessagesNavigator(
|
||||||
|
onForwardEventClickLambda = onForwardEventClickLambda,
|
||||||
|
)
|
||||||
val presenter = createMessagesPresenter(navigator = navigator)
|
val presenter = createMessagesPresenter(navigator = navigator)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -225,7 +229,7 @@ class MessagesPresenterTest {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Forward, aMessageEvent()))
|
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Forward, aMessageEvent()))
|
||||||
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
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
|
@Test
|
||||||
fun `present - handle action edit poll`() = runTest {
|
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)
|
val presenter = createMessagesPresenter(navigator = navigator)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -460,22 +467,21 @@ class MessagesPresenterTest {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent(content = aTimelineItemPollContent())))
|
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent(content = aTimelineItemPollContent())))
|
||||||
awaitItem()
|
awaitItem()
|
||||||
assertThat(navigator.onEditPollClickedCount).isEqualTo(1)
|
onEditPollClickLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - handle action end poll`() = runTest {
|
fun `present - handle action end poll`() = runTest {
|
||||||
val endPollAction = FakeEndPollAction()
|
val timelineEventSink = EventsRecorder<TimelineEvents>()
|
||||||
val presenter = createMessagesPresenter(endPollAction = endPollAction)
|
val presenter = createMessagesPresenter(timelineEventSink = timelineEventSink)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
endPollAction.verifyExecutionCount(0)
|
|
||||||
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.EndPoll, aMessageEvent(content = aTimelineItemPollContent())))
|
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.EndPoll, aMessageEvent(content = aTimelineItemPollContent())))
|
||||||
delay(1)
|
delay(1)
|
||||||
endPollAction.verifyExecutionCount(1)
|
timelineEventSink.assertSingle(TimelineEvents.EndPoll(AN_EVENT_ID))
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -516,7 +522,10 @@ class MessagesPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - handle action report content`() = runTest {
|
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)
|
val presenter = createMessagesPresenter(navigator = navigator)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -524,7 +533,7 @@ class MessagesPresenterTest {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.ReportContent, aMessageEvent()))
|
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.ReportContent, aMessageEvent()))
|
||||||
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
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
|
@Test
|
||||||
fun `present - handle action show developer info`() = runTest {
|
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)
|
val presenter = createMessagesPresenter(navigator = navigator)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -550,7 +562,7 @@ class MessagesPresenterTest {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.ViewSource, aMessageEvent()))
|
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.ViewSource, aMessageEvent()))
|
||||||
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
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(),
|
navigator: FakeMessagesNavigator = FakeMessagesNavigator(),
|
||||||
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
|
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
|
||||||
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
||||||
endPollAction: EndPollAction = FakeEndPollAction(),
|
timelineEventSink: (TimelineEvents) -> Unit = {},
|
||||||
permalinkParser: PermalinkParser = FakePermalinkParser(),
|
permalinkParser: PermalinkParser = FakePermalinkParser(),
|
||||||
messageComposerPresenter: Presenter<MessageComposerState> = Presenter {
|
messageComposerPresenter: Presenter<MessageComposerState> = Presenter {
|
||||||
aMessageComposerState(
|
aMessageComposerState(
|
||||||
|
|
@ -1097,19 +1109,12 @@ class MessagesPresenterTest {
|
||||||
},
|
},
|
||||||
actionListEventSink: (ActionListEvents) -> Unit = {},
|
actionListEventSink: (ActionListEvents) -> Unit = {},
|
||||||
): MessagesPresenter {
|
): MessagesPresenter {
|
||||||
val timelinePresenterFactory = object : TimelinePresenter.Factory {
|
|
||||||
override fun create(navigator: MessagesNavigator): TimelinePresenter {
|
|
||||||
return createTimelinePresenter(
|
|
||||||
endPollAction = endPollAction,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val featureFlagService = FakeFeatureFlagService()
|
val featureFlagService = FakeFeatureFlagService()
|
||||||
return MessagesPresenter(
|
return MessagesPresenter(
|
||||||
room = matrixRoom,
|
room = matrixRoom,
|
||||||
composerPresenter = messageComposerPresenter,
|
composerPresenter = messageComposerPresenter,
|
||||||
voiceMessageComposerPresenter = { aVoiceMessageComposerState() },
|
voiceMessageComposerPresenter = { aVoiceMessageComposerState() },
|
||||||
timelinePresenterFactory = timelinePresenterFactory,
|
timelinePresenter = { aTimelineState(eventSink = timelineEventSink) },
|
||||||
timelineProtectionPresenter = { aTimelineProtectionState() },
|
timelineProtectionPresenter = { aTimelineProtectionState() },
|
||||||
actionListPresenterFactory = FakeActionListPresenter.Factory(actionListEventSink),
|
actionListPresenterFactory = FakeActionListPresenter.Factory(actionListEventSink),
|
||||||
customReactionPresenter = { aCustomReactionState() },
|
customReactionPresenter = { aCustomReactionState() },
|
||||||
|
|
|
||||||
|
|
@ -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.ActionListState
|
||||||
import io.element.android.features.messages.impl.actionlist.anActionListState
|
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.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.VerifiedUserSendFailure
|
||||||
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.aChangedIdentitySendFailure
|
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.aChangedIdentitySendFailure
|
||||||
import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
|
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.ensureCalledOnce
|
||||||
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||||
import io.element.android.tests.testutils.pressBack
|
import io.element.android.tests.testutils.pressBack
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -514,7 +512,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 +529,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,
|
||||||
|
|
|
||||||
|
|
@ -521,7 +521,7 @@ class ActionListPresenterTest {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
val messageEvent = aMessageEvent(
|
val messageEvent = aMessageEvent(
|
||||||
isMine = true,
|
isMine = true,
|
||||||
isEditable = false,
|
isEditable = true,
|
||||||
content = aTimelineItemImageContent(),
|
content = aTimelineItemImageContent(),
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(
|
initialState.eventSink.invoke(
|
||||||
|
|
@ -567,7 +567,7 @@ class ActionListPresenterTest {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
val messageEvent = aMessageEvent(
|
val messageEvent = aMessageEvent(
|
||||||
isMine = true,
|
isMine = true,
|
||||||
isEditable = false,
|
isEditable = true,
|
||||||
content = aTimelineItemImageContent(
|
content = aTimelineItemImageContent(
|
||||||
caption = A_CAPTION,
|
caption = A_CAPTION,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,9 @@ 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.attachments.Attachment
|
||||||
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
|
||||||
|
|
@ -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.test
|
||||||
import io.element.android.tests.testutils.waitForPredicate
|
import io.element.android.tests.testutils.waitForPredicate
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
|
@ -133,7 +137,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 +688,15 @@ class MessageComposerPresenterTest {
|
||||||
val room = FakeMatrixRoom(
|
val room = FakeMatrixRoom(
|
||||||
typingNoticeResult = { Result.success(Unit) }
|
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)
|
pickerProvider.givenMimeType(MimeTypes.Images)
|
||||||
mediaPreProcessor.givenResult(
|
mediaPreProcessor.givenResult(
|
||||||
Result.success(
|
Result.success(
|
||||||
|
|
@ -709,9 +720,7 @@ class MessageComposerPresenterTest {
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery)
|
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery)
|
||||||
val previewingState = awaitItem()
|
onPreviewAttachmentLambda.assertions().isCalledOnce()
|
||||||
assertThat(previewingState.showAttachmentSourcePicker).isFalse()
|
|
||||||
assertThat(previewingState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -720,7 +729,15 @@ class MessageComposerPresenterTest {
|
||||||
val room = FakeMatrixRoom(
|
val room = FakeMatrixRoom(
|
||||||
typingNoticeResult = { Result.success(Unit) }
|
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)
|
pickerProvider.givenMimeType(MimeTypes.Videos)
|
||||||
mediaPreProcessor.givenResult(
|
mediaPreProcessor.givenResult(
|
||||||
Result.success(
|
Result.success(
|
||||||
|
|
@ -745,9 +762,7 @@ class MessageComposerPresenterTest {
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery)
|
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery)
|
||||||
val previewingState = awaitItem()
|
onPreviewAttachmentLambda.assertions().isCalledOnce()
|
||||||
assertThat(previewingState.showAttachmentSourcePicker).isFalse()
|
|
||||||
assertThat(previewingState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -772,15 +787,21 @@ class MessageComposerPresenterTest {
|
||||||
val room = FakeMatrixRoom(
|
val room = FakeMatrixRoom(
|
||||||
typingNoticeResult = { Result.success(Unit) }
|
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) {
|
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()
|
onPreviewAttachmentLambda.assertions().isCalledOnce()
|
||||||
assertThat(sendingState.showAttachmentSourcePicker).isFalse()
|
|
||||||
assertThat(sendingState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -828,19 +849,22 @@ class MessageComposerPresenterTest {
|
||||||
typingNoticeResult = { Result.success(Unit) }
|
typingNoticeResult = { Result.success(Unit) }
|
||||||
)
|
)
|
||||||
val permissionPresenter = FakePermissionsPresenter().apply { setPermissionGranted() }
|
val permissionPresenter = FakePermissionsPresenter().apply { setPermissionGranted() }
|
||||||
|
val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList<Attachment> -> }
|
||||||
|
val navigator = FakeMessagesNavigator(
|
||||||
|
onPreviewAttachmentLambda = onPreviewAttachmentLambda
|
||||||
|
)
|
||||||
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()
|
onPreviewAttachmentLambda.assertions().isCalledOnce()
|
||||||
assertThat(finalState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java)
|
|
||||||
cancelAndIgnoreRemainingEvents()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -850,23 +874,23 @@ class MessageComposerPresenterTest {
|
||||||
typingNoticeResult = { Result.success(Unit) }
|
typingNoticeResult = { Result.success(Unit) }
|
||||||
)
|
)
|
||||||
val permissionPresenter = FakePermissionsPresenter()
|
val permissionPresenter = FakePermissionsPresenter()
|
||||||
|
val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList<Attachment> -> }
|
||||||
|
val navigator = FakeMessagesNavigator(
|
||||||
|
onPreviewAttachmentLambda = onPreviewAttachmentLambda
|
||||||
|
)
|
||||||
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)
|
onPreviewAttachmentLambda.assertions().isCalledOnce()
|
||||||
val finalState = awaitItem()
|
|
||||||
assertThat(finalState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java)
|
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -877,19 +901,22 @@ class MessageComposerPresenterTest {
|
||||||
typingNoticeResult = { Result.success(Unit) }
|
typingNoticeResult = { Result.success(Unit) }
|
||||||
)
|
)
|
||||||
val permissionPresenter = FakePermissionsPresenter().apply { setPermissionGranted() }
|
val permissionPresenter = FakePermissionsPresenter().apply { setPermissionGranted() }
|
||||||
|
val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList<Attachment> -> }
|
||||||
|
val navigator = FakeMessagesNavigator(
|
||||||
|
onPreviewAttachmentLambda = onPreviewAttachmentLambda
|
||||||
|
)
|
||||||
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()
|
onPreviewAttachmentLambda.assertions().isCalledOnce()
|
||||||
assertThat(finalState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java)
|
|
||||||
cancelAndIgnoreRemainingEvents()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -899,10 +926,15 @@ class MessageComposerPresenterTest {
|
||||||
typingNoticeResult = { Result.success(Unit) }
|
typingNoticeResult = { Result.success(Unit) }
|
||||||
)
|
)
|
||||||
val permissionPresenter = FakePermissionsPresenter()
|
val permissionPresenter = FakePermissionsPresenter()
|
||||||
|
val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList<Attachment> -> }
|
||||||
|
val navigator = FakeMessagesNavigator(
|
||||||
|
onPreviewAttachmentLambda = onPreviewAttachmentLambda
|
||||||
|
)
|
||||||
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 +943,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()
|
onPreviewAttachmentLambda.assertions().isCalledOnce()
|
||||||
assertThat(finalState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java)
|
|
||||||
cancelAndIgnoreRemainingEvents()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1500,6 +1529,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 +1544,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,
|
||||||
|
|
|
||||||
|
|
@ -431,7 +431,10 @@ import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - PollEditClicked event navigates`() = runTest {
|
fun `present - PollEditClicked event navigates`() = runTest {
|
||||||
val navigator = FakeMessagesNavigator()
|
val onEditPollClickLambda = lambdaRecorder { _: EventId -> }
|
||||||
|
val navigator = FakeMessagesNavigator(
|
||||||
|
onEditPollClickLambda = onEditPollClickLambda
|
||||||
|
)
|
||||||
val presenter = createTimelinePresenter(
|
val presenter = createTimelinePresenter(
|
||||||
messagesNavigator = navigator,
|
messagesNavigator = navigator,
|
||||||
)
|
)
|
||||||
|
|
@ -439,7 +442,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
awaitFirstItem().eventSink(TimelineEvents.EditPoll(AN_EVENT_ID))
|
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 {
|
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
|
||||||
return awaitItem()
|
return awaitItem()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
internal fun TestScope.createTimelinePresenter(
|
private fun TestScope.createTimelinePresenter(
|
||||||
timeline: Timeline = FakeTimeline(),
|
timeline: Timeline = FakeTimeline(),
|
||||||
room: FakeMatrixRoom = FakeMatrixRoom(
|
room: FakeMatrixRoom = FakeMatrixRoom(
|
||||||
liveTimeline = timeline,
|
liveTimeline = timeline,
|
||||||
canUserSendMessageResult = { _, _ -> Result.success(true) }
|
canUserSendMessageResult = { _, _ -> Result.success(true) }
|
||||||
),
|
),
|
||||||
redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
|
redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
|
||||||
messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(),
|
messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(),
|
||||||
endPollAction: EndPollAction = FakeEndPollAction(),
|
endPollAction: EndPollAction = FakeEndPollAction(),
|
||||||
sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(),
|
sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(),
|
||||||
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
|
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||||
timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(),
|
timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(),
|
||||||
): TimelinePresenter {
|
): TimelinePresenter {
|
||||||
return TimelinePresenter(
|
return TimelinePresenter(
|
||||||
timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(),
|
timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(),
|
||||||
room = room,
|
room = room,
|
||||||
dispatchers = testCoroutineDispatchers(),
|
dispatchers = testCoroutineDispatchers(),
|
||||||
appScope = this,
|
appScope = this,
|
||||||
navigator = messagesNavigator,
|
navigator = messagesNavigator,
|
||||||
redactedVoiceMessageManager = redactedVoiceMessageManager,
|
redactedVoiceMessageManager = redactedVoiceMessageManager,
|
||||||
endPollAction = endPollAction,
|
endPollAction = endPollAction,
|
||||||
sendPollResponseAction = sendPollResponseAction,
|
sendPollResponseAction = sendPollResponseAction,
|
||||||
sessionPreferencesStore = sessionPreferencesStore,
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
timelineItemIndexer = timelineItemIndexer,
|
timelineItemIndexer = timelineItemIndexer,
|
||||||
timelineController = TimelineController(room),
|
timelineController = TimelineController(room),
|
||||||
resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() },
|
resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() },
|
||||||
typingNotificationPresenter = { aTypingNotificationState() },
|
typingNotificationPresenter = { aTypingNotificationState() },
|
||||||
roomCallStatePresenter = { aStandByCallState() },
|
roomCallStatePresenter = { aStandByCallState() },
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@ test_konsist = "com.lemonappdev:konsist:0.16.1"
|
||||||
test_turbine = "app.cash.turbine:turbine:1.2.0"
|
test_turbine = "app.cash.turbine:turbine:1.2.0"
|
||||||
test_truth = "com.google.truth:truth:1.4.4"
|
test_truth = "com.google.truth:truth:1.4.4"
|
||||||
test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.18"
|
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_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "appyx" }
|
||||||
test_composable_preview_scanner = "com.github.sergio-sastre.ComposablePreviewScanner:android:0.1.2"
|
test_composable_preview_scanner = "com.github.sergio-sastre.ComposablePreviewScanner:android:0.1.2"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,9 @@ class KonsistClassNameTest {
|
||||||
Konsist.scopeFromProduction()
|
Konsist.scopeFromProduction()
|
||||||
.classes()
|
.classes()
|
||||||
.withAllParentsOf(PreviewParameterProvider::class)
|
.withAllParentsOf(PreviewParameterProvider::class)
|
||||||
|
.withoutName(
|
||||||
|
"AspectRatioProvider",
|
||||||
|
)
|
||||||
.also {
|
.also {
|
||||||
// Check that classes are actually found
|
// Check that classes are actually found
|
||||||
assertThat(it.size).isGreaterThan(100)
|
assertThat(it.size).isGreaterThan(100)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:db067468af10a72fbaa437a00937afddba100f5345317b9c23b03c8920d5cffa
|
oid sha256:f40c56aa3b794ce62db9688bf481561d5229a459921e54f55885a6db8f74fe21
|
||||||
size 33401
|
size 64604
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:78ea6cbfcf12e405eca8b953b3d847e73f80121ad47beb6346563a2e9b5d567b
|
||||||
|
size 56342
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:07ab4fb940d9cae7dcd82d1d6e7235091154414bbb1a6f3a7f557775f8730497
|
||||||
|
size 79880
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a4bafcbc5bc5a62e686adc7194f72a7394b462ad4d99afb0a35380c2f8e1fbeb
|
||||||
|
size 37409
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c0fc9f20bae5c54d807f5917844669bb8909514483d2ad29b348ef18f8f986a6
|
oid sha256:2e3dfc75223f91ecd21c59980a393244be7dfb93a0ebee0885d75fdf5d33d53a
|
||||||
size 33356
|
size 64347
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:047da66e7f7e78478b1d8442028224073e3d31493b5facf926472fc526532be1
|
||||||
|
size 56473
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:0d16153ecf852fc504cabc60722eccbe484c992c1bced50f067fb58c46dfd4f7
|
||||||
|
size 79088
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:2aa2e5da25b8a83db35d9b065f0b89cc745cfbda3f6a81d31e445fa31bf4431a
|
||||||
|
size 37381
|
||||||
Loading…
Add table
Add a link
Reference in a new issue