Merge branch 'develop' into feature/fga/better_timeline_scroll

This commit is contained in:
ganfra 2023-07-17 23:35:41 +02:00
commit 8f01e8133f
156 changed files with 3806 additions and 539 deletions

View file

@ -25,7 +25,6 @@ import io.element.android.features.messages.impl.timeline.components.customreact
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.core.data.StableCharSequence
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomId
@ -48,7 +47,7 @@ fun aMessagesState() = MessagesState(
roomAvatar = AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom),
userHasPermissionToSendMessage = true,
composerState = aMessageComposerState().copy(
text = StableCharSequence("Hello"),
text = "Hello",
isFullScreen = false,
mode = MessageComposerMode.Normal("Hello"),
),

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.messagecomposer
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.messages.api.MessageComposerContext
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.textcomposer.MessageComposerMode
import javax.inject.Inject
@SingleIn(RoomScope::class)
@ContributesBinding(RoomScope::class)
class MessageComposerContextImpl @Inject constructor() : MessageComposerContext {
override var composerMode: MessageComposerMode by mutableStateOf(MessageComposerMode.Normal(""))
internal set
}

View file

@ -26,7 +26,7 @@ sealed interface MessageComposerEvents {
data class SendMessage(val message: String) : MessageComposerEvents
object CloseSpecialMode : MessageComposerEvents
data class SetMode(val composerMode: MessageComposerMode) : MessageComposerEvents
data class UpdateText(val text: CharSequence) : MessageComposerEvents
data class UpdateText(val text: String) : MessageComposerEvents
object AddAttachment : MessageComposerEvents
object DismissAttachmentMenu : MessageComposerEvents
sealed interface PickAttachmentSource : MessageComposerEvents {

View file

@ -34,8 +34,6 @@ 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.media.local.LocalMediaFactory
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.data.StableCharSequence
import io.element.android.libraries.core.data.toStableCharSequence
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.SnackbarMessage
import io.element.android.libraries.di.RoomScope
@ -64,6 +62,7 @@ class MessageComposerPresenter @Inject constructor(
private val mediaSender: MediaSender,
private val snackbarDispatcher: SnackbarDispatcher,
private val analyticsService: AnalyticsService,
private val messageComposerContext: MessageComposerContextImpl,
) : Presenter<MessageComposerState> {
@SuppressLint("UnsafeOptInUsageError")
@ -93,18 +92,15 @@ class MessageComposerPresenter @Inject constructor(
val hasFocus = remember {
mutableStateOf(false)
}
val text: MutableState<StableCharSequence> = remember {
mutableStateOf(StableCharSequence(""))
}
val composerMode: MutableState<MessageComposerMode> = rememberSaveable {
mutableStateOf(MessageComposerMode.Normal(""))
val text: MutableState<String> = rememberSaveable {
mutableStateOf("")
}
var showAttachmentSourcePicker: Boolean by remember { mutableStateOf(false) }
LaunchedEffect(composerMode.value) {
when (val modeValue = composerMode.value) {
is MessageComposerMode.Edit -> text.value = modeValue.defaultContent.toStableCharSequence()
LaunchedEffect(messageComposerContext.composerMode) {
when (val modeValue = messageComposerContext.composerMode) {
is MessageComposerMode.Edit -> text.value = modeValue.defaultContent
else -> Unit
}
}
@ -122,20 +118,24 @@ class MessageComposerPresenter @Inject constructor(
is MessageComposerEvents.FocusChanged -> hasFocus.value = event.hasFocus
is MessageComposerEvents.UpdateText -> text.value = event.text.toStableCharSequence()
is MessageComposerEvents.UpdateText -> text.value = event.text
MessageComposerEvents.CloseSpecialMode -> {
text.value = "".toStableCharSequence()
composerMode.setToNormal()
text.value = ""
messageComposerContext.composerMode = MessageComposerMode.Normal("")
}
is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage(event.message, composerMode, text)
is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage(
text = event.message,
updateComposerMode = { messageComposerContext.composerMode = it },
textState = text
)
is MessageComposerEvents.SetMode -> {
composerMode.value = event.composerMode
messageComposerContext.composerMode = event.composerMode
analyticsService.capture(
Composer(
inThread = false,
isEditing = composerMode.value is MessageComposerMode.Edit,
isReply = composerMode.value is MessageComposerMode.Reply,
inThread = messageComposerContext.composerMode.inThread,
isEditing = messageComposerContext.composerMode.isEditing,
isReply = messageComposerContext.composerMode.isReply,
isLocation = false,
)
)
@ -171,7 +171,7 @@ class MessageComposerPresenter @Inject constructor(
text = text.value,
isFullScreen = isFullScreen.value,
hasFocus = hasFocus.value,
mode = composerMode.value,
mode = messageComposerContext.composerMode,
showAttachmentSourcePicker = showAttachmentSourcePicker,
attachmentsState = attachmentsState.value,
eventSink = ::handleEvents
@ -184,31 +184,30 @@ class MessageComposerPresenter @Inject constructor(
}
}
private fun MutableState<MessageComposerMode>.setToNormal() {
value = MessageComposerMode.Normal("")
}
private fun CoroutineScope.sendMessage(text: String, composerMode: MutableState<MessageComposerMode>, textState: MutableState<StableCharSequence>) =
launch {
val capturedMode = composerMode.value
// Reset composer right away
textState.value = "".toStableCharSequence()
composerMode.setToNormal()
when (capturedMode) {
is MessageComposerMode.Normal -> room.sendMessage(text)
is MessageComposerMode.Edit -> {
val eventId = capturedMode.eventId
val transactionId = capturedMode.transactionId
room.editMessage(eventId, transactionId, text)
}
is MessageComposerMode.Quote -> TODO()
is MessageComposerMode.Reply -> room.replyMessage(
capturedMode.eventId,
text
)
private fun CoroutineScope.sendMessage(
text: String,
updateComposerMode: (newComposerMode: MessageComposerMode) -> Unit,
textState: MutableState<String>
) = launch {
val capturedMode = messageComposerContext.composerMode
// Reset composer right away
textState.value = ""
updateComposerMode(MessageComposerMode.Normal(""))
when (capturedMode) {
is MessageComposerMode.Normal -> room.sendMessage(text)
is MessageComposerMode.Edit -> {
val eventId = capturedMode.eventId
val transactionId = capturedMode.transactionId
room.editMessage(eventId, transactionId, text)
}
is MessageComposerMode.Quote -> TODO()
is MessageComposerMode.Reply -> room.replyMessage(
capturedMode.eventId,
text
)
}
}
private fun CoroutineScope.sendAttachment(
attachment: Attachment,

View file

@ -18,13 +18,12 @@ package io.element.android.features.messages.impl.messagecomposer
import androidx.compose.runtime.Immutable
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.libraries.core.data.StableCharSequence
import io.element.android.libraries.textcomposer.MessageComposerMode
import kotlinx.collections.immutable.ImmutableList
@Immutable
data class MessageComposerState(
val text: StableCharSequence?,
val text: String?,
val isFullScreen: Boolean,
val hasFocus: Boolean,
val mode: MessageComposerMode,
@ -32,7 +31,7 @@ data class MessageComposerState(
val attachmentsState: AttachmentsState,
val eventSink: (MessageComposerEvents) -> Unit
) {
val isSendButtonVisible: Boolean = text?.charSequence.isNullOrEmpty().not()
val isSendButtonVisible: Boolean = text.isNullOrEmpty().not()
}
@Immutable

View file

@ -17,7 +17,6 @@
package io.element.android.features.messages.impl.messagecomposer
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.core.data.StableCharSequence
import io.element.android.libraries.textcomposer.MessageComposerMode
open class MessageComposerStateProvider : PreviewParameterProvider<MessageComposerState> {
@ -28,7 +27,7 @@ open class MessageComposerStateProvider : PreviewParameterProvider<MessageCompos
}
fun aMessageComposerState() = MessageComposerState(
text = StableCharSequence(""),
text = "",
isFullScreen = false,
hasFocus = false,
mode = MessageComposerMode.Normal(content = ""),

View file

@ -47,7 +47,7 @@ fun MessageComposerView(
state.eventSink(MessageComposerEvents.CloseSpecialMode)
}
fun onComposerTextChange(text: CharSequence) {
fun onComposerTextChange(text: String) {
state.eventSink(MessageComposerEvents.UpdateText(text))
}
@ -69,7 +69,7 @@ fun MessageComposerView(
onAddAttachment = ::onAddAttachment,
onFocusChanged = ::onFocusChanged,
composerCanSendMessage = state.isSendButtonVisible,
composerText = state.text?.charSequence?.toString(),
composerText = state.text,
modifier = modifier
)
}

View file

@ -27,6 +27,7 @@ import io.element.android.features.messages.impl.timeline.model.virtual.aTimelin
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
@ -103,7 +104,7 @@ fun aTimelineItemDaySeparator(): TimelineItem.Virtual {
internal fun aTimelineItemEvent(
eventId: EventId = EventId("\$" + Random.nextInt().toString()),
transactionId: String? = null,
transactionId: TransactionId? = null,
isMine: Boolean = false,
content: TimelineItemEventContent = aTimelineItemTextContent(),
groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,

View file

@ -18,10 +18,11 @@ package io.element.android.features.messages.impl.timeline.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineEncryptedHistoryBannerView
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemDaySeparatorView
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineLoadingMoreIndicator
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemEncryptedHistoryBannerVirtualModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel
@Composable
@ -32,5 +33,7 @@ fun TimelineItemVirtualRow(
when (virtual.model) {
is TimelineItemDaySeparatorModel -> TimelineItemDaySeparatorView(virtual.model, modifier)
TimelineItemReadMarkerModel -> return
is TimelineItemEncryptedHistoryBannerVirtualModel -> TimelineEncryptedHistoryBannerView(modifier)
}
}

View file

@ -0,0 +1,69 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.timeline.components.virtual
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.features.messages.impl.R
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.theme.ElementTheme
@Composable
fun TimelineEncryptedHistoryBannerView(modifier: Modifier = Modifier) {
Row(
modifier = modifier
.padding(start = 16.dp, end = 16.dp, top = 24.dp, bottom = 32.dp)
.clip(MaterialTheme.shapes.small)
.border(1.dp, ElementTheme.colors.borderInfoSubtle, MaterialTheme.shapes.small)
.background(ElementTheme.colors.bgInfoSubtle)
.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Icon(
imageVector = Icons.Default.Info,
contentDescription = "Info",
tint = ElementTheme.colors.iconInfoPrimary
)
Text(
text = stringResource(R.string.screen_room_encrypted_history_banner),
style = ElementTheme.typography.fontBodyMdMedium,
color = ElementTheme.colors.textInfoPrimary
)
}
}
@DayNightPreviews
@Composable
internal fun TimelineEncryptedHistoryBannerViewPreview() {
ElementTheme {
TimelineEncryptedHistoryBannerView()
}
}

View file

@ -45,7 +45,6 @@ class TimelineItemsFactory @Inject constructor(
private val virtualItemFactory: TimelineItemVirtualFactory,
private val timelineItemGrouper: TimelineItemGrouper,
) {
private val timelineItems = MutableStateFlow(persistentListOf<TimelineItem>())
private val timelineItemsCache = arrayListOf<TimelineItem?>()

View file

@ -17,6 +17,7 @@
package io.element.android.features.messages.impl.timeline.factories.virtual
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemEncryptedHistoryBannerVirtualModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
@ -30,8 +31,13 @@ class TimelineItemVirtualFactory @Inject constructor(
fun create(
virtualTimelineItem: MatrixTimelineItem.Virtual,
): TimelineItem.Virtual {
val id = if (virtualTimelineItem.virtual is VirtualTimelineItem.EncryptedHistoryBanner) {
"encrypted_history_banner"
} else {
virtualTimelineItem.uniqueId.toString()
}
return TimelineItem.Virtual(
id = virtualTimelineItem.uniqueId.toString(),
id = id,
model = virtualTimelineItem.computeModel()
)
}
@ -40,6 +46,7 @@ class TimelineItemVirtualFactory @Inject constructor(
return when (val inner = virtual) {
is VirtualTimelineItem.DayDivider -> daySeparatorFactory.create(inner)
is VirtualTimelineItem.ReadMarker -> TimelineItemReadMarkerModel
is VirtualTimelineItem.EncryptedHistoryBanner -> TimelineItemEncryptedHistoryBannerVirtualModel
}
}
}

View file

@ -22,6 +22,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
@ -54,7 +55,7 @@ sealed interface TimelineItem {
data class Event(
val id: String,
val eventId: EventId? = null,
val transactionId: String? = null,
val transactionId: TransactionId? = null,
val senderId: UserId,
val senderDisplayName: String?,
val senderAvatar: AvatarData,

View file

@ -0,0 +1,21 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.timeline.model.virtual
object TimelineItemEncryptedHistoryBannerVirtualModel : TimelineItemVirtualModel {
override val type: String = "TimelineItemEncryptedHistoryBannerVirtualModel"
}

View file

@ -4,10 +4,27 @@
<item quantity="one">"%1$d changement dans la conversation"</item>
<item quantity="other">"%1$d changements dans la conversation"</item>
</plurals>
<plurals name="screen_room_timeline_more_reactions">
<item quantity="one"></item>
<item quantity="other">"%1$d de plus"</item>
</plurals>
<string name="screen_room_attachment_source_camera">"Appareil photo"</string>
<string name="screen_room_attachment_source_camera_photo">"Prendre une photo"</string>
<string name="screen_room_attachment_source_camera_video">"Enregistrer une vidéo"</string>
<string name="screen_room_attachment_source_files">"Pièce-jointe"</string>
<string name="screen_room_attachment_source_gallery">"Gallerie photo et vidéo"</string>
<string name="screen_room_encrypted_history_banner">"Lhistorique des messages nest pas disponible actuellement dans ce salon"</string>
<string name="screen_room_error_failed_retrieving_user_details">"Impossible de récupérer les détails de lutilisateur"</string>
<string name="screen_room_invite_again_alert_message">"Souhaitez-vous les inviter à revenir ?"</string>
<string name="screen_room_invite_again_alert_title">"Vous êtes seul dans ce chat"</string>
<string name="screen_room_message_copied">"Message copié"</string>
<string name="screen_room_no_permission_to_post">"Vous navez pas le droit de poster dans ce salon"</string>
<string name="screen_room_reactions_show_less">"Afficher moins"</string>
<string name="screen_room_reactions_show_more">"Afficher plus"</string>
<string name="screen_room_retry_send_menu_send_again_action">"Renvoyer"</string>
<string name="screen_room_retry_send_menu_title">"Votre message n\'a pas pu être envoyé"</string>
<string name="screen_room_timeline_add_reaction">"Ajouter un emoji"</string>
<string name="screen_room_timeline_less_reactions">"Montrer moins"</string>
<string name="screen_room_error_failed_processing_media">"Échec du traitement du média avant son envoi, veuillez réessayer."</string>
<string name="screen_room_retry_send_menu_remove_action">"Supprimer"</string>
</resources>

View file

@ -5,6 +5,11 @@
<item quantity="few">"%1$d zmeny miestnosti"</item>
<item quantity="other">"%1$d zmien miestnosti"</item>
</plurals>
<plurals name="screen_room_timeline_more_reactions">
<item quantity="one"></item>
<item quantity="few">"%1$d ďalšie"</item>
<item quantity="other">"%1$d ďalších"</item>
</plurals>
<string name="screen_room_attachment_source_camera">"Kamera"</string>
<string name="screen_room_attachment_source_camera_photo">"Odfotiť"</string>
<string name="screen_room_attachment_source_camera_video">"Nahrať video"</string>
@ -21,6 +26,8 @@
<string name="screen_room_reactions_show_more">"Zobraziť viac"</string>
<string name="screen_room_retry_send_menu_send_again_action">"Odoslať znova"</string>
<string name="screen_room_retry_send_menu_title">"Vašu správu sa nepodarilo odoslať"</string>
<string name="screen_room_timeline_add_reaction">"Pridať emoji"</string>
<string name="screen_room_timeline_less_reactions">"Zobraziť menej"</string>
<string name="screen_room_error_failed_processing_media">"Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova."</string>
<string name="screen_room_retry_send_menu_remove_action">"Odstrániť"</string>
</resources>

View file

@ -19,6 +19,17 @@
<string name="screen_room_invite_again_alert_title">"You are alone in this chat"</string>
<string name="screen_room_message_copied">"Message copied"</string>
<string name="screen_room_no_permission_to_post">"You do not have permission to post to this room"</string>
<string name="screen_room_notification_settings_allow_custom">"Allow custom setting"</string>
<string name="screen_room_notification_settings_allow_custom_footnote">"Turning this on will override your default setting"</string>
<string name="screen_room_notification_settings_custom_settings_title">"Notify me in this chat for"</string>
<string name="screen_room_notification_settings_default_setting_footnote">"You can change it in your %1$s."</string>
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"global settings"</string>
<string name="screen_room_notification_settings_default_setting_title">"Default setting"</string>
<string name="screen_room_notification_settings_error_loading_settings">"An error occurred while loading notification settings."</string>
<string name="screen_room_notification_settings_error_restoring_default">"Failed restoring the default mode, please try again."</string>
<string name="screen_room_notification_settings_error_setting_mode">"Failed setting the mode, please try again."</string>
<string name="screen_room_notification_settings_mode_all_messages">"All messages"</string>
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Mentions and Keywords only"</string>
<string name="screen_room_reactions_show_less">"Show less"</string>
<string name="screen_room_reactions_show_more">"Show more"</string>
<string name="screen_room_retry_send_menu_send_again_action">"Send again"</string>

View file

@ -31,6 +31,7 @@ import io.element.android.features.messages.impl.MessagesPresenter
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.messagecomposer.MessageComposerContextImpl
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.TimelinePresenter
@ -568,6 +569,7 @@ class MessagesPresenterTest {
mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom),
snackbarDispatcher = SnackbarDispatcher(),
analyticsService = FakeAnalyticsService(),
messageComposerContext = MessageComposerContextImpl(),
)
val timelinePresenter = TimelinePresenter(
timelineItemsFactory = aTimelineItemsFactory(),

View file

@ -26,17 +26,18 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.analytics.test.FakeAnalyticsService
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
import io.element.android.features.messages.impl.messagecomposer.MessageComposerContextImpl
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.messagecomposer.MessageComposerState
import io.element.android.features.messages.media.FakeLocalMediaFactory
import io.element.android.libraries.core.data.StableCharSequence
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.room.MatrixRoom
@ -82,7 +83,7 @@ class MessageComposerPresenterTest {
}.test {
val initialState = awaitItem()
assertThat(initialState.isFullScreen).isFalse()
assertThat(initialState.text).isEqualTo(StableCharSequence(""))
assertThat(initialState.text).isEqualTo("")
assertThat(initialState.mode).isEqualTo(MessageComposerMode.Normal(""))
assertThat(initialState.showAttachmentSourcePicker).isFalse()
assertThat(initialState.attachmentsState).isEqualTo(AttachmentsState.None)
@ -115,11 +116,11 @@ class MessageComposerPresenterTest {
val initialState = awaitItem()
initialState.eventSink.invoke(MessageComposerEvents.UpdateText(A_MESSAGE))
val withMessageState = awaitItem()
assertThat(withMessageState.text).isEqualTo(StableCharSequence(A_MESSAGE))
assertThat(withMessageState.text).isEqualTo(A_MESSAGE)
assertThat(withMessageState.isSendButtonVisible).isTrue()
withMessageState.eventSink.invoke(MessageComposerEvents.UpdateText(""))
val withEmptyMessageState = awaitItem()
assertThat(withEmptyMessageState.text).isEqualTo(StableCharSequence(""))
assertThat(withEmptyMessageState.text).isEqualTo("")
assertThat(withEmptyMessageState.isSendButtonVisible).isFalse()
}
}
@ -136,7 +137,7 @@ class MessageComposerPresenterTest {
state = awaitItem()
assertThat(state.mode).isEqualTo(mode)
state = awaitItem()
assertThat(state.text).isEqualTo(StableCharSequence(A_MESSAGE))
assertThat(state.text).isEqualTo(A_MESSAGE)
assertThat(state.isSendButtonVisible).isTrue()
backToNormalMode(state, skipCount = 1)
}
@ -153,7 +154,7 @@ class MessageComposerPresenterTest {
state.eventSink.invoke(MessageComposerEvents.SetMode(mode))
state = awaitItem()
assertThat(state.mode).isEqualTo(mode)
assertThat(state.text).isEqualTo(StableCharSequence(""))
assertThat(state.text).isEqualTo("")
assertThat(state.isSendButtonVisible).isFalse()
backToNormalMode(state)
}
@ -170,7 +171,7 @@ class MessageComposerPresenterTest {
state.eventSink.invoke(MessageComposerEvents.SetMode(mode))
state = awaitItem()
assertThat(state.mode).isEqualTo(mode)
assertThat(state.text).isEqualTo(StableCharSequence(""))
assertThat(state.text).isEqualTo("")
assertThat(state.isSendButtonVisible).isFalse()
backToNormalMode(state)
}
@ -185,11 +186,11 @@ class MessageComposerPresenterTest {
val initialState = awaitItem()
initialState.eventSink.invoke(MessageComposerEvents.UpdateText(A_MESSAGE))
val withMessageState = awaitItem()
assertThat(withMessageState.text).isEqualTo(StableCharSequence(A_MESSAGE))
assertThat(withMessageState.text).isEqualTo(A_MESSAGE)
assertThat(withMessageState.isSendButtonVisible).isTrue()
withMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(A_MESSAGE))
val messageSentState = awaitItem()
assertThat(messageSentState.text).isEqualTo(StableCharSequence(""))
assertThat(messageSentState.text).isEqualTo("")
assertThat(messageSentState.isSendButtonVisible).isFalse()
}
}
@ -205,21 +206,21 @@ class MessageComposerPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.text).isEqualTo(StableCharSequence(""))
assertThat(initialState.text).isEqualTo("")
val mode = anEditMode()
initialState.eventSink.invoke(MessageComposerEvents.SetMode(mode))
skipItems(1)
val withMessageState = awaitItem()
assertThat(withMessageState.mode).isEqualTo(mode)
assertThat(withMessageState.text).isEqualTo(StableCharSequence(A_MESSAGE))
assertThat(withMessageState.text).isEqualTo(A_MESSAGE)
assertThat(withMessageState.isSendButtonVisible).isTrue()
withMessageState.eventSink.invoke(MessageComposerEvents.UpdateText(ANOTHER_MESSAGE))
val withEditedMessageState = awaitItem()
assertThat(withEditedMessageState.text).isEqualTo(StableCharSequence(ANOTHER_MESSAGE))
assertThat(withEditedMessageState.text).isEqualTo(ANOTHER_MESSAGE)
withEditedMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(ANOTHER_MESSAGE))
skipItems(1)
val messageSentState = awaitItem()
assertThat(messageSentState.text).isEqualTo(StableCharSequence(""))
assertThat(messageSentState.text).isEqualTo("")
assertThat(messageSentState.isSendButtonVisible).isFalse()
assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE)
}
@ -236,21 +237,21 @@ class MessageComposerPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.text).isEqualTo(StableCharSequence(""))
assertThat(initialState.text).isEqualTo("")
val mode = anEditMode(eventId = null, transactionId = A_TRANSACTION_ID)
initialState.eventSink.invoke(MessageComposerEvents.SetMode(mode))
skipItems(1)
val withMessageState = awaitItem()
assertThat(withMessageState.mode).isEqualTo(mode)
assertThat(withMessageState.text).isEqualTo(StableCharSequence(A_MESSAGE))
assertThat(withMessageState.text).isEqualTo(A_MESSAGE)
assertThat(withMessageState.isSendButtonVisible).isTrue()
withMessageState.eventSink.invoke(MessageComposerEvents.UpdateText(ANOTHER_MESSAGE))
val withEditedMessageState = awaitItem()
assertThat(withEditedMessageState.text).isEqualTo(StableCharSequence(ANOTHER_MESSAGE))
assertThat(withEditedMessageState.text).isEqualTo(ANOTHER_MESSAGE)
withEditedMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(ANOTHER_MESSAGE))
skipItems(1)
val messageSentState = awaitItem()
assertThat(messageSentState.text).isEqualTo(StableCharSequence(""))
assertThat(messageSentState.text).isEqualTo("")
assertThat(messageSentState.isSendButtonVisible).isFalse()
assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE)
}
@ -267,21 +268,21 @@ class MessageComposerPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.text).isEqualTo(StableCharSequence(""))
assertThat(initialState.text).isEqualTo("")
val mode = aReplyMode()
initialState.eventSink.invoke(MessageComposerEvents.SetMode(mode))
val state = awaitItem()
assertThat(state.mode).isEqualTo(mode)
assertThat(state.text).isEqualTo(StableCharSequence(""))
assertThat(state.text).isEqualTo("")
assertThat(state.isSendButtonVisible).isFalse()
initialState.eventSink.invoke(MessageComposerEvents.UpdateText(A_REPLY))
val withMessageState = awaitItem()
assertThat(withMessageState.text).isEqualTo(StableCharSequence(A_REPLY))
assertThat(withMessageState.text).isEqualTo(A_REPLY)
assertThat(withMessageState.isSendButtonVisible).isTrue()
withMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(A_REPLY))
skipItems(1)
val messageSentState = awaitItem()
assertThat(messageSentState.text).isEqualTo(StableCharSequence(""))
assertThat(messageSentState.text).isEqualTo("")
assertThat(messageSentState.isSendButtonVisible).isFalse()
assertThat(fakeMatrixRoom.replyMessageParameter).isEqualTo(A_REPLY)
}
@ -484,7 +485,7 @@ class MessageComposerPresenterTest {
skipItems(skipCount)
val normalState = awaitItem()
assertThat(normalState.mode).isEqualTo(MessageComposerMode.Normal(""))
assertThat(normalState.text).isEqualTo(StableCharSequence(""))
assertThat(normalState.text).isEqualTo("")
assertThat(normalState.isSendButtonVisible).isFalse()
}
@ -503,14 +504,15 @@ class MessageComposerPresenterTest {
localMediaFactory,
MediaSender(mediaPreProcessor, room),
snackbarDispatcher,
FakeAnalyticsService()
FakeAnalyticsService(),
MessageComposerContextImpl(),
)
}
fun anEditMode(
eventId: EventId? = AN_EVENT_ID,
message: String = A_MESSAGE,
transactionId: String? = null,
transactionId: TransactionId? = null,
) = MessageComposerMode.Edit(eventId, message, transactionId)
fun aReplyMode() = MessageComposerMode.Reply(A_USER_NAME, null, AN_EVENT_ID, A_MESSAGE)
fun aQuoteMode() = MessageComposerMode.Quote(AN_EVENT_ID, A_MESSAGE)