Merge branch 'develop' into feature/fga/timeline_thread_decoration
This commit is contained in:
commit
f27f49160d
124 changed files with 566 additions and 304 deletions
|
|
@ -30,6 +30,7 @@ import androidx.compose.runtime.setValue
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.PollEnd
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
|
|
@ -65,6 +66,8 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
|||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarMessage
|
||||
import io.element.android.libraries.designsystem.utils.collectSnackbarMessageAsState
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
|
|
@ -74,6 +77,7 @@ import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
|
|||
import io.element.android.libraries.matrix.ui.room.canRedactAsState
|
||||
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -92,6 +96,8 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
private val messageSummaryFormatter: MessageSummaryFormatter,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val clipboardHelper: ClipboardHelper,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
@Assisted private val navigator: MessagesNavigator,
|
||||
) : Presenter<MessagesState> {
|
||||
|
||||
|
|
@ -140,6 +146,11 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
timelineState.eventSink(TimelineEvents.SetHighlightedEvent(composerState.mode.relatedEventId))
|
||||
}
|
||||
|
||||
var enableTextFormatting by remember { mutableStateOf(true) }
|
||||
LaunchedEffect(Unit) {
|
||||
enableTextFormatting = featureFlagService.isFeatureEnabled(FeatureFlags.RichTextEditor)
|
||||
}
|
||||
|
||||
fun handleEvents(event: MessagesEvents) {
|
||||
when (event) {
|
||||
is MessagesEvents.HandleAction -> {
|
||||
|
|
@ -175,6 +186,7 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
snackbarMessage = snackbarMessage,
|
||||
showReinvitePrompt = showReinvitePrompt,
|
||||
inviteProgress = inviteProgress.value,
|
||||
enableTextFormatting = enableTextFormatting,
|
||||
eventSink = { handleEvents(it) }
|
||||
)
|
||||
}
|
||||
|
|
@ -247,11 +259,15 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleActionEdit(targetEvent: TimelineItem.Event, composerState: MessageComposerState) {
|
||||
private suspend fun handleActionEdit(targetEvent: TimelineItem.Event, composerState: MessageComposerState) {
|
||||
val composerMode = MessageComposerMode.Edit(
|
||||
targetEvent.eventId,
|
||||
(targetEvent.content as? TimelineItemTextBasedContent)?.let {
|
||||
it.htmlBody ?: it.body
|
||||
if (featureFlagService.isFeatureEnabled(FeatureFlags.RichTextEditor)) {
|
||||
it.htmlBody ?: it.body
|
||||
} else {
|
||||
it.body
|
||||
}
|
||||
}.orEmpty(),
|
||||
targetEvent.transactionId,
|
||||
)
|
||||
|
|
@ -321,8 +337,10 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private suspend fun handleEndPollAction(event: TimelineItem.Event) {
|
||||
event.eventId?.let { room.endPoll(it, "The poll with event id: $it has ended.") }
|
||||
// TODO Polls: Send poll end analytic
|
||||
event.eventId?.let {
|
||||
room.endPoll(it, "The poll with event id: $it has ended.")
|
||||
analyticsService.capture(PollEnd())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleCopyContents(event: TimelineItem.Event) {
|
||||
|
|
|
|||
|
|
@ -45,5 +45,6 @@ data class MessagesState(
|
|||
val snackbarMessage: SnackbarMessage?,
|
||||
val inviteProgress: Async<Unit>,
|
||||
val showReinvitePrompt: Boolean,
|
||||
val enableTextFormatting: Boolean,
|
||||
val eventSink: (MessagesEvents) -> Unit
|
||||
)
|
||||
|
|
|
|||
|
|
@ -82,5 +82,6 @@ fun aMessagesState() = MessagesState(
|
|||
snackbarMessage = null,
|
||||
inviteProgress = Async.Uninitialized,
|
||||
showReinvitePrompt = false,
|
||||
enableTextFormatting = true,
|
||||
eventSink = {}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -123,7 +123,13 @@ fun MessagesView(
|
|||
fun onMessageLongClicked(event: TimelineItem.Event) {
|
||||
Timber.v("OnMessageLongClicked= ${event.id}")
|
||||
localView.hideKeyboard()
|
||||
state.actionListState.eventSink(ActionListEvents.ComputeForMessage(event, state.userHasPermissionToRedact))
|
||||
state.actionListState.eventSink(
|
||||
ActionListEvents.ComputeForMessage(
|
||||
event = event,
|
||||
canRedact = state.userHasPermissionToRedact,
|
||||
canSendMessage = state.userHasPermissionToSendMessage,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun onActionSelected(action: TimelineItemAction, event: TimelineItem.Event) {
|
||||
|
|
@ -203,8 +209,8 @@ fun MessagesView(
|
|||
CustomReactionBottomSheet(
|
||||
state = state.customReactionState,
|
||||
onEmojiSelected = { eventId, emoji ->
|
||||
state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, eventId))
|
||||
state.customReactionState.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
|
||||
state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, eventId))
|
||||
state.customReactionState.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -298,6 +304,7 @@ private fun MessagesViewContent(
|
|||
state = state.composerState,
|
||||
onSendLocationClicked = onSendLocationClicked,
|
||||
onCreatePollClicked = onCreatePollClicked,
|
||||
enableTextFormatting = state.enableTextFormatting,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(Alignment.Bottom)
|
||||
|
|
|
|||
|
|
@ -20,5 +20,9 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
|||
|
||||
sealed interface ActionListEvents {
|
||||
data object Clear : ActionListEvents
|
||||
data class ComputeForMessage(val event: TimelineItem.Event, val canRedact: Boolean) : ActionListEvents
|
||||
data class ComputeForMessage(
|
||||
val event: TimelineItem.Event,
|
||||
val canRedact: Boolean,
|
||||
val canSendMessage: Boolean,
|
||||
) : ActionListEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ class ActionListPresenter @Inject constructor(
|
|||
is ActionListEvents.ComputeForMessage -> localCoroutineScope.computeForMessage(
|
||||
timelineItem = event.event,
|
||||
userCanRedact = event.canRedact,
|
||||
userCanSendMessage = event.canSendMessage,
|
||||
target = target,
|
||||
)
|
||||
}
|
||||
|
|
@ -77,6 +78,7 @@ class ActionListPresenter @Inject constructor(
|
|||
private fun CoroutineScope.computeForMessage(
|
||||
timelineItem: TimelineItem.Event,
|
||||
userCanRedact: Boolean,
|
||||
userCanSendMessage: Boolean,
|
||||
target: MutableState<ActionListState.Target>
|
||||
) = launch {
|
||||
target.value = ActionListState.Target.Loading(timelineItem)
|
||||
|
|
@ -101,7 +103,8 @@ class ActionListPresenter @Inject constructor(
|
|||
buildList {
|
||||
val isMineOrCanRedact = timelineItem.isMine || userCanRedact
|
||||
|
||||
// TODO Poll: Reply to poll
|
||||
// TODO Poll: Reply to poll. Ensure to update `fun TimelineItemEventContent.canBeReplied()`
|
||||
// when touching this
|
||||
// if (timelineItem.isRemote) {
|
||||
// // Can only reply or forward messages already uploaded to the server
|
||||
// add(TimelineItemAction.Reply)
|
||||
|
|
@ -126,7 +129,9 @@ class ActionListPresenter @Inject constructor(
|
|||
else -> buildList<TimelineItemAction> {
|
||||
if (timelineItem.isRemote) {
|
||||
// Can only reply or forward messages already uploaded to the server
|
||||
add(TimelineItemAction.Reply)
|
||||
if (userCanSendMessage) {
|
||||
add(TimelineItemAction.Reply)
|
||||
}
|
||||
add(TimelineItemAction.Forward)
|
||||
}
|
||||
if (timelineItem.isMine && timelineItem.isTextMessage) {
|
||||
|
|
|
|||
|
|
@ -35,5 +35,5 @@ sealed class TimelineItemAction(
|
|||
data object Edit : TimelineItemAction(CommonStrings.action_edit, VectorIcons.Edit)
|
||||
data object Developer : TimelineItemAction(CommonStrings.action_view_source, VectorIcons.DeveloperMode)
|
||||
data object ReportContent : TimelineItemAction(CommonStrings.action_report_content, VectorIcons.ReportContent, destructive = true)
|
||||
data object EndPoll : TimelineItemAction(CommonStrings.action_end_poll, VectorIcons.EndPoll)
|
||||
data object EndPoll : TimelineItemAction(CommonStrings.action_end_poll, VectorIcons.PollEnd)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ internal fun AttachmentsBottomSheet(
|
|||
state: MessageComposerState,
|
||||
onSendLocationClicked: () -> Unit,
|
||||
onCreatePollClicked: () -> Unit,
|
||||
enableTextFormatting: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val localView = LocalView.current
|
||||
|
|
@ -87,6 +88,7 @@ internal fun AttachmentsBottomSheet(
|
|||
) {
|
||||
AttachmentSourcePickerMenu(
|
||||
state = state,
|
||||
enableTextFormatting = enableTextFormatting,
|
||||
onSendLocationClicked = onSendLocationClicked,
|
||||
onCreatePollClicked = onCreatePollClicked,
|
||||
)
|
||||
|
|
@ -100,6 +102,7 @@ internal fun AttachmentSourcePickerMenu(
|
|||
state: MessageComposerState,
|
||||
onSendLocationClicked: () -> Unit,
|
||||
onCreatePollClicked: () -> Unit,
|
||||
enableTextFormatting: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
|
|
@ -146,11 +149,13 @@ internal fun AttachmentSourcePickerMenu(
|
|||
text = { Text(stringResource(R.string.screen_room_attachment_source_poll)) },
|
||||
)
|
||||
}
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.ToggleTextFormatting(enabled = true)) },
|
||||
icon = { Icon(Icons.Default.FormatColorText, null) },
|
||||
text = { Text(stringResource(R.string.screen_room_attachment_text_formatting)) },
|
||||
)
|
||||
if (enableTextFormatting) {
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.ToggleTextFormatting(enabled = true)) },
|
||||
icon = { Icon(Icons.Default.FormatColorText, null) },
|
||||
text = { Text(stringResource(R.string.screen_room_attachment_text_formatting)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -163,5 +168,6 @@ internal fun AttachmentSourcePickerMenuPreview() = ElementPreview {
|
|||
),
|
||||
onSendLocationClicked = {},
|
||||
onCreatePollClicked = {},
|
||||
enableTextFormatting = true,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,14 +148,6 @@ class MessageComposerPresenter @Inject constructor(
|
|||
)
|
||||
is MessageComposerEvents.SetMode -> {
|
||||
messageComposerContext.composerMode = event.composerMode
|
||||
analyticsService.capture(
|
||||
Composer(
|
||||
inThread = messageComposerContext.composerMode.inThread,
|
||||
isEditing = messageComposerContext.composerMode.isEditing,
|
||||
isReply = messageComposerContext.composerMode.isReply,
|
||||
isLocation = false,
|
||||
)
|
||||
)
|
||||
}
|
||||
MessageComposerEvents.AddAttachment -> localCoroutineScope.launch {
|
||||
showAttachmentSourcePicker = true
|
||||
|
|
@ -238,6 +230,14 @@ class MessageComposerPresenter @Inject constructor(
|
|||
message.html,
|
||||
)
|
||||
}
|
||||
analyticsService.capture(
|
||||
Composer(
|
||||
inThread = capturedMode.inThread,
|
||||
isEditing = capturedMode.isEditing,
|
||||
isReply = capturedMode.isReply,
|
||||
messageType = Composer.MessageType.Text, // Set proper type when we'll be sending other types of messages.
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.sendAttachment(
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ fun MessageComposerView(
|
|||
state: MessageComposerState,
|
||||
onSendLocationClicked: () -> Unit,
|
||||
onCreatePollClicked: () -> Unit,
|
||||
enableTextFormatting: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun onFullscreenToggle() {
|
||||
|
|
@ -62,6 +63,7 @@ fun MessageComposerView(
|
|||
state = state,
|
||||
onSendLocationClicked = onSendLocationClicked,
|
||||
onCreatePollClicked = onCreatePollClicked,
|
||||
enableTextFormatting = enableTextFormatting,
|
||||
)
|
||||
|
||||
TextComposer(
|
||||
|
|
@ -74,6 +76,7 @@ fun MessageComposerView(
|
|||
onResetComposerMode = ::onCloseSpecialMode,
|
||||
onAddAttachment = ::onAddAttachment,
|
||||
onDismissTextFormatting = ::onDismissTextFormatting,
|
||||
enableTextFormatting = enableTextFormatting,
|
||||
onError = ::onError,
|
||||
)
|
||||
}
|
||||
|
|
@ -95,5 +98,6 @@ private fun ContentToPreview(state: MessageComposerState) {
|
|||
state = state,
|
||||
onSendLocationClicked = {},
|
||||
onCreatePollClicked = {},
|
||||
enableTextFormatting = true,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import im.vector.app.features.analytics.plan.PollVote
|
||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
|
|
@ -35,6 +36,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
|
|||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
|
||||
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
|
|
@ -51,6 +53,7 @@ class TimelinePresenter @Inject constructor(
|
|||
private val room: MatrixRoom,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val appScope: CoroutineScope,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : Presenter<TimelineState> {
|
||||
|
||||
private val timeline = room.timeline
|
||||
|
|
@ -93,7 +96,7 @@ class TimelinePresenter @Inject constructor(
|
|||
pollStartId = event.pollStartId,
|
||||
answers = listOf(event.answerId),
|
||||
)
|
||||
// TODO Polls: Send poll vote analytic
|
||||
analyticsService.capture(PollVote())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -116,7 +119,7 @@ class TimelinePresenter @Inject constructor(
|
|||
|
||||
return TimelineState(
|
||||
highlightedEventId = highlightedEventId.value,
|
||||
canReply = userHasPermissionToSendMessage,
|
||||
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
||||
paginationState = paginationState,
|
||||
timelineItems = timelineItems,
|
||||
hasNewItems = hasNewItems.value,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
data class TimelineState(
|
||||
val timelineItems: ImmutableList<TimelineItem>,
|
||||
val highlightedEventId: EventId?,
|
||||
val canReply: Boolean,
|
||||
val userHasPermissionToSendMessage: Boolean,
|
||||
val paginationState: MatrixTimeline.PaginationState,
|
||||
val hasNewItems: Boolean,
|
||||
val eventSink: (TimelineEvents) -> Unit
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ fun aTimelineState(timelineItems: ImmutableList<TimelineItem> = persistentListOf
|
|||
timelineItems = timelineItems,
|
||||
paginationState = MatrixTimeline.PaginationState(isBackPaginating = false, hasMoreToLoadBackwards = true),
|
||||
highlightedEventId = null,
|
||||
canReply = true,
|
||||
userHasPermissionToSendMessage = true,
|
||||
hasNewItems = false,
|
||||
eventSink = {},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.canBeRepliedTo
|
||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
|
||||
|
|
@ -119,7 +120,7 @@ fun TimelineView(
|
|||
TimelineItemRow(
|
||||
timelineItem = timelineItem,
|
||||
highlightedItem = state.highlightedEventId?.value,
|
||||
canReply = state.canReply,
|
||||
userHasPermissionToSendMessage = state.userHasPermissionToSendMessage,
|
||||
onClick = onMessageClicked,
|
||||
onLongClick = onMessageLongClicked,
|
||||
onUserDataClick = onUserDataClicked,
|
||||
|
|
@ -156,7 +157,7 @@ fun TimelineView(
|
|||
fun TimelineItemRow(
|
||||
timelineItem: TimelineItem,
|
||||
highlightedItem: String?,
|
||||
canReply: Boolean,
|
||||
userHasPermissionToSendMessage: Boolean,
|
||||
onUserDataClick: (UserId) -> Unit,
|
||||
onClick: (TimelineItem.Event) -> Unit,
|
||||
onLongClick: (TimelineItem.Event) -> Unit,
|
||||
|
|
@ -189,7 +190,7 @@ fun TimelineItemRow(
|
|||
TimelineItemEventRow(
|
||||
event = timelineItem,
|
||||
isHighlighted = highlightedItem == timelineItem.identifier(),
|
||||
canReply = canReply,
|
||||
canReply = userHasPermissionToSendMessage && timelineItem.content.canBeRepliedTo(),
|
||||
onClick = { onClick(timelineItem) },
|
||||
onLongClick = { onLongClick(timelineItem) },
|
||||
onUserDataClick = onUserDataClick,
|
||||
|
|
@ -228,7 +229,7 @@ fun TimelineItemRow(
|
|||
TimelineItemRow(
|
||||
timelineItem = subGroupEvent,
|
||||
highlightedItem = highlightedItem,
|
||||
canReply = false,
|
||||
userHasPermissionToSendMessage = false,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
inReplyToClick = inReplyToClick,
|
||||
|
|
|
|||
|
|
@ -18,10 +18,14 @@ package io.element.android.features.messages.impl.timeline.components.event
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.times
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.designsystem.text.toDp
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
|
@ -69,3 +73,10 @@ fun ExtraPadding.getStr(fontSize: TextUnit): String {
|
|||
// A space and some unbreakable spaces
|
||||
return " " + "\u00A0".repeat(nbOfSpaces)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExtraPadding.getDpSize(): Dp {
|
||||
if (nbChars == 0) return 0.dp
|
||||
val timestampFontSize = ElementTheme.typography.fontBodyXsRegular.fontSize // 11.sp
|
||||
return nbChars * timestampFontSize.toDp() / 3
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,9 +18,6 @@ package io.element.android.features.messages.impl.timeline.components.event
|
|||
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
|
|
@ -28,7 +25,6 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.features.messages.impl.timeline.components.html.HtmlDocument
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||
|
|
@ -51,18 +47,14 @@ fun TimelineItemTextView(
|
|||
CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.textPrimary) {
|
||||
val htmlDocument = content.htmlDocument
|
||||
if (htmlDocument != null) {
|
||||
// For now we ignore the extra padding for html content, so add some spacing
|
||||
// below the content (as previous behavior)
|
||||
Column(modifier = modifier) {
|
||||
HtmlDocument(
|
||||
document = htmlDocument,
|
||||
modifier = Modifier,
|
||||
onTextClicked = onTextClicked,
|
||||
onTextLongClicked = onTextLongClicked,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
HtmlDocument(
|
||||
document = htmlDocument,
|
||||
extraPadding = extraPadding,
|
||||
modifier = modifier,
|
||||
onTextClicked = onTextClicked,
|
||||
onTextLongClicked = onTextLongClicked,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
} else {
|
||||
Box(modifier) {
|
||||
val textWithPadding = remember(content.body) {
|
||||
|
|
|
|||
|
|
@ -25,8 +25,10 @@ import androidx.compose.foundation.layout.Box
|
|||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
|
|
@ -53,13 +55,18 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.features.messages.impl.timeline.components.event.ExtraPadding
|
||||
import io.element.android.features.messages.impl.timeline.components.event.getDpSize
|
||||
import io.element.android.features.messages.impl.timeline.components.event.noExtraPadding
|
||||
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.text.toDp
|
||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import io.element.android.libraries.theme.LinkColor
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import org.jsoup.nodes.Document
|
||||
|
|
@ -72,18 +79,28 @@ private const val CHIP_ID = "chip"
|
|||
@Composable
|
||||
fun HtmlDocument(
|
||||
document: Document,
|
||||
extraPadding: ExtraPadding,
|
||||
interactionSource: MutableInteractionSource,
|
||||
modifier: Modifier = Modifier,
|
||||
onTextClicked: () -> Unit = {},
|
||||
onTextLongClicked: () -> Unit = {},
|
||||
) {
|
||||
HtmlBody(
|
||||
body = document.body(),
|
||||
interactionSource = interactionSource,
|
||||
FlowRow(
|
||||
modifier = modifier,
|
||||
onTextClicked = onTextClicked,
|
||||
onTextLongClicked = onTextLongClicked,
|
||||
)
|
||||
) {
|
||||
HtmlBody(
|
||||
body = document.body(),
|
||||
interactionSource = interactionSource,
|
||||
onTextClicked = onTextClicked,
|
||||
onTextLongClicked = onTextLongClicked,
|
||||
)
|
||||
Spacer(
|
||||
modifier = Modifier.size(
|
||||
width = extraPadding.getDpSize(),
|
||||
height = ElementTheme.typography.fontBodyXsRegular.fontSize.toDp() * 1.25f
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -603,5 +620,9 @@ internal fun HtmlDocumentDarkPreview(@PreviewParameter(DocumentProvider::class)
|
|||
|
||||
@Composable
|
||||
private fun ContentToPreview(document: Document) {
|
||||
HtmlDocument(document, remember { MutableInteractionSource() })
|
||||
HtmlDocument(
|
||||
document = document,
|
||||
extraPadding = noExtraPadding,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,18 @@ fun TimelineItemEventContent.canBeCopied(): Boolean =
|
|||
else -> false
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the event content can be replied to.
|
||||
* Note: it should match the logic in [io.element.android.features.messages.impl.actionlist.ActionListPresenter].
|
||||
*/
|
||||
fun TimelineItemEventContent.canBeRepliedTo(): Boolean =
|
||||
when (this) {
|
||||
is TimelineItemRedactedContent,
|
||||
is TimelineItemStateContent,
|
||||
is TimelineItemPollContent -> false
|
||||
else -> true
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if user can react (i.e. send a reaction) on the event content.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import app.cash.molecule.RecompositionMode
|
|||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.PollEnd
|
||||
import io.element.android.features.messages.fixtures.aMessageEvent
|
||||
import io.element.android.features.messages.fixtures.aTimelineItemsFactory
|
||||
import io.element.android.features.messages.impl.InviteDialogAction
|
||||
|
|
@ -575,7 +576,11 @@ class MessagesPresenterTest {
|
|||
@Test
|
||||
fun `present - handle poll end`() = runTest {
|
||||
val room = FakeMatrixRoom()
|
||||
val presenter = createMessagePresenter(matrixRoom = room)
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val presenter = createMessagePresenter(
|
||||
matrixRoom = room,
|
||||
analyticsService = analyticsService,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -586,7 +591,8 @@ class MessagesPresenterTest {
|
|||
assertThat(room.endPollInvocations.size).isEqualTo(1)
|
||||
assertThat(room.endPollInvocations.first().pollStartId).isEqualTo(AN_EVENT_ID)
|
||||
assertThat(room.endPollInvocations.first().text).isEqualTo("The poll with event id: \$anEventId has ended.")
|
||||
// TODO Polls: Test poll end analytic
|
||||
assertThat(analyticsService.capturedEvents.size).isEqualTo(1)
|
||||
assertThat(analyticsService.capturedEvents.last()).isEqualTo(PollEnd())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -595,6 +601,7 @@ class MessagesPresenterTest {
|
|||
matrixRoom: MatrixRoom = FakeMatrixRoom(),
|
||||
navigator: FakeMessagesNavigator = FakeMessagesNavigator(),
|
||||
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
|
||||
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
||||
): MessagesPresenter {
|
||||
val messageComposerPresenter = MessageComposerPresenter(
|
||||
appCoroutineScope = this,
|
||||
|
|
@ -604,7 +611,7 @@ class MessagesPresenterTest {
|
|||
localMediaFactory = FakeLocalMediaFactory(mockMediaUrl),
|
||||
mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom),
|
||||
snackbarDispatcher = SnackbarDispatcher(),
|
||||
analyticsService = FakeAnalyticsService(),
|
||||
analyticsService = analyticsService,
|
||||
messageComposerContext = MessageComposerContextImpl(),
|
||||
richTextEditorStateFactory = TestRichTextEditorStateFactory(),
|
||||
|
||||
|
|
@ -613,13 +620,15 @@ class MessagesPresenterTest {
|
|||
timelineItemsFactory = aTimelineItemsFactory(),
|
||||
room = matrixRoom,
|
||||
dispatchers = coroutineDispatchers,
|
||||
appScope = this
|
||||
appScope = this,
|
||||
analyticsService = analyticsService,
|
||||
)
|
||||
val buildMeta = aBuildMeta()
|
||||
val actionListPresenter = ActionListPresenter(buildMeta = buildMeta)
|
||||
val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
|
||||
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
|
||||
val retrySendMenuPresenter = RetrySendMenuPresenter(room = matrixRoom)
|
||||
val featureFlagsService = FakeFeatureFlagService(mapOf(FeatureFlags.RichTextEditor.key to true))
|
||||
return MessagesPresenter(
|
||||
room = matrixRoom,
|
||||
composerPresenter = messageComposerPresenter,
|
||||
|
|
@ -633,6 +642,8 @@ class MessagesPresenterTest {
|
|||
messageSummaryFormatter = FakeMessageSummaryFormatter(),
|
||||
navigator = navigator,
|
||||
clipboardHelper = clipboardHelper,
|
||||
analyticsService = analyticsService,
|
||||
featureFlagService = featureFlagsService,
|
||||
dispatchers = coroutineDispatchers,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class ActionListPresenterTest {
|
|||
}.test {
|
||||
val initialState = awaitItem()
|
||||
val messageEvent = aMessageEvent(isMine = true, content = TimelineItemRedactedContent)
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
|
||||
// val loadingState = awaitItem()
|
||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||
val successState = awaitItem()
|
||||
|
|
@ -89,7 +89,7 @@ class ActionListPresenterTest {
|
|||
}.test {
|
||||
val initialState = awaitItem()
|
||||
val messageEvent = aMessageEvent(isMine = false, content = TimelineItemRedactedContent)
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
|
||||
// val loadingState = awaitItem()
|
||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||
val successState = awaitItem()
|
||||
|
|
@ -117,7 +117,7 @@ class ActionListPresenterTest {
|
|||
isMine = false,
|
||||
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
|
||||
)
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
|
||||
// val loadingState = awaitItem()
|
||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||
val successState = awaitItem()
|
||||
|
|
@ -138,6 +138,37 @@ class ActionListPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - compute for others message cannot sent message`() = runTest {
|
||||
val presenter = anActionListPresenter(isBuildDebuggable = true)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
val messageEvent = aMessageEvent(
|
||||
isMine = false,
|
||||
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
|
||||
)
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = false))
|
||||
// val loadingState = awaitItem()
|
||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
messageEvent,
|
||||
persistentListOf(
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Copy,
|
||||
TimelineItemAction.Developer,
|
||||
TimelineItemAction.ReportContent,
|
||||
)
|
||||
)
|
||||
)
|
||||
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||
assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - compute for others message and can redact`() = runTest {
|
||||
val presenter = anActionListPresenter(isBuildDebuggable = true)
|
||||
|
|
@ -149,7 +180,7 @@ class ActionListPresenterTest {
|
|||
isMine = false,
|
||||
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
|
||||
)
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, true))
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = true, canSendMessage = true))
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
|
|
@ -180,7 +211,7 @@ class ActionListPresenterTest {
|
|||
isMine = true,
|
||||
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
|
||||
)
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
|
||||
// val loadingState = awaitItem()
|
||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||
val successState = awaitItem()
|
||||
|
|
@ -213,7 +244,7 @@ class ActionListPresenterTest {
|
|||
isMine = true,
|
||||
content = aTimelineItemImageContent(),
|
||||
)
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
|
||||
// val loadingState = awaitItem()
|
||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||
val successState = awaitItem()
|
||||
|
|
@ -244,7 +275,7 @@ class ActionListPresenterTest {
|
|||
isMine = true,
|
||||
content = aTimelineItemStateEventContent(),
|
||||
)
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, false))
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, canRedact = false, canSendMessage = true))
|
||||
// val loadingState = awaitItem()
|
||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||
val successState = awaitItem()
|
||||
|
|
@ -273,7 +304,7 @@ class ActionListPresenterTest {
|
|||
isMine = true,
|
||||
content = aTimelineItemStateEventContent(),
|
||||
)
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, false))
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, canRedact = false, canSendMessage = true))
|
||||
// val loadingState = awaitItem()
|
||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||
val successState = awaitItem()
|
||||
|
|
@ -301,7 +332,7 @@ class ActionListPresenterTest {
|
|||
isMine = true,
|
||||
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
|
||||
)
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
|
||||
// val loadingState = awaitItem()
|
||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||
val successState = awaitItem()
|
||||
|
|
@ -338,10 +369,10 @@ class ActionListPresenterTest {
|
|||
content = TimelineItemRedactedContent,
|
||||
)
|
||||
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
|
||||
assertThat(awaitItem().target).isInstanceOf(ActionListState.Target.Success::class.java)
|
||||
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(redactedEvent, false))
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(redactedEvent, canRedact = false, canSendMessage = true))
|
||||
awaitItem().run {
|
||||
assertThat(target).isEqualTo(ActionListState.Target.None)
|
||||
assertThat(displayEmojiReactions).isFalse()
|
||||
|
|
@ -362,7 +393,7 @@ class ActionListPresenterTest {
|
|||
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false),
|
||||
)
|
||||
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
|
|
@ -389,7 +420,7 @@ class ActionListPresenterTest {
|
|||
isMine = true,
|
||||
content = aTimelineItemPollContent(),
|
||||
)
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
|
|
@ -415,7 +446,7 @@ class ActionListPresenterTest {
|
|||
isMine = true,
|
||||
content = aTimelineItemPollContent(isEnded = true),
|
||||
)
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import app.cash.molecule.RecompositionMode
|
|||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.PollVote
|
||||
import io.element.android.features.messages.fixtures.aTimelineItemsFactory
|
||||
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
||||
|
|
@ -37,6 +38,7 @@ import io.element.android.libraries.matrix.test.room.aMessageContent
|
|||
import io.element.android.libraries.matrix.test.room.anEventTimelineItem
|
||||
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.awaitWithLatch
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
|
|
@ -260,7 +262,11 @@ class TimelinePresenterTest {
|
|||
@Test
|
||||
fun `present - PollAnswerSelected event calls into rust room api and analytics`() = runTest {
|
||||
val room = FakeMatrixRoom()
|
||||
val presenter = createTimelinePresenter(room)
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val presenter = createTimelinePresenter(
|
||||
room = room,
|
||||
analyticsService = analyticsService,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -271,7 +277,8 @@ class TimelinePresenterTest {
|
|||
assertThat(room.sendPollResponseInvocations.size).isEqualTo(1)
|
||||
assertThat(room.sendPollResponseInvocations.first().answers).isEqualTo(listOf("anAnswerId"))
|
||||
assertThat(room.sendPollResponseInvocations.first().pollStartId).isEqualTo(AN_EVENT_ID)
|
||||
// TODO Polls: Test poll vote analytic
|
||||
assertThat(analyticsService.capturedEvents.size).isEqualTo(1)
|
||||
assertThat(analyticsService.capturedEvents.last()).isEqualTo(PollVote())
|
||||
}
|
||||
|
||||
private fun TestScope.createTimelinePresenter(
|
||||
|
|
@ -282,18 +289,21 @@ class TimelinePresenterTest {
|
|||
timelineItemsFactory = timelineItemsFactory,
|
||||
room = FakeMatrixRoom(matrixTimeline = timeline),
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
appScope = this
|
||||
appScope = this,
|
||||
analyticsService = FakeAnalyticsService(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun TestScope.createTimelinePresenter(
|
||||
room: MatrixRoom,
|
||||
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
||||
): TimelinePresenter {
|
||||
return TimelinePresenter(
|
||||
timelineItemsFactory = aTimelineItemsFactory(),
|
||||
room = room,
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
appScope = this
|
||||
appScope = this,
|
||||
analyticsService = analyticsService,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue