Browse Source

Merge pull request #2087 from element-hq/feature/bma/enablechatBackup

Enable Chat backup, Mentions and Read Receipt in release.
pull/2097/head
Benoit Marty 9 months ago committed by GitHub
parent
commit
deff2d8fc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      changelog.d/2087.misc
  2. 2
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt
  3. 50
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt
  4. 105
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt
  5. 34
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt
  6. 6
      libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt
  7. 6
      libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt

1
changelog.d/2087.misc

@ -0,0 +1 @@
Enable Chat backup, Mentions and Read Receipt in release.

2
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt

@ -186,7 +186,7 @@ class TimelinePresenter @AssistedInject constructor(
showReadReceipts = readReceiptsEnabled, showReadReceipts = readReceiptsEnabled,
newEventState = newItemState.value, newEventState = newItemState.value,
sessionState = sessionState, sessionState = sessionState,
eventSink = ::handleEvents eventSink = { handleEvents(it) }
) )
} }

50
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt

@ -19,6 +19,7 @@ package io.element.android.features.messages.impl
import android.net.Uri import android.net.Uri
import app.cash.molecule.RecompositionMode import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.ActionListPresenter
@ -171,8 +172,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID)) initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID))
assertThat(room.myReactions.count()).isEqualTo(1) assertThat(room.myReactions.count()).isEqualTo(1)
@ -188,6 +188,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Forward, aMessageEvent())) initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Forward, aMessageEvent()))
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
@ -203,8 +204,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Copy, event)) initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Copy, event))
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
assertThat(clipboardHelper.clipboardContents).isEqualTo((event.content as TimelineItemTextContent).body) assertThat(clipboardHelper.clipboardContents).isEqualTo((event.content as TimelineItemTextContent).body)
@ -217,8 +217,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent())) initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent()))
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java)
@ -232,6 +231,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(3)
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent(eventId = null))) initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent(eventId = null)))
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
@ -246,8 +246,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
val mediaMessage = aMessageEvent( val mediaMessage = aMessageEvent(
content = TimelineItemImageContent( content = TimelineItemImageContent(
body = "image.jpg", body = "image.jpg",
@ -277,8 +276,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
val mediaMessage = aMessageEvent( val mediaMessage = aMessageEvent(
content = TimelineItemVideoContent( content = TimelineItemVideoContent(
body = "video.mp4", body = "video.mp4",
@ -309,8 +307,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
val mediaMessage = aMessageEvent( val mediaMessage = aMessageEvent(
content = TimelineItemFileContent( content = TimelineItemFileContent(
body = "file.pdf", body = "file.pdf",
@ -336,8 +333,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent())) initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent()))
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Edit::class.java) assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Edit::class.java)
@ -352,8 +348,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent(content = aTimelineItemPollContent()))) initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent(content = aTimelineItemPollContent())))
assertThat(navigator.onEditPollClickedCount).isEqualTo(1) assertThat(navigator.onEditPollClickedCount).isEqualTo(1)
} }
@ -383,6 +378,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.ReportContent, aMessageEvent())) initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.ReportContent, aMessageEvent()))
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
@ -396,10 +392,10 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.Dismiss) initialState.eventSink.invoke(MessagesEvents.Dismiss)
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
} }
} }
@ -410,6 +406,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.ViewSource, aMessageEvent())) initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.ViewSource, aMessageEvent()))
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
@ -450,8 +447,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
assertThat(initialState.showReinvitePrompt).isFalse() assertThat(initialState.showReinvitePrompt).isFalse()
initialState.composerState.richTextEditorState.requestFocus() initialState.composerState.richTextEditorState.requestFocus()
val focusedState = awaitItem() val focusedState = awaitItem()
@ -466,8 +462,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
assertThat(initialState.showReinvitePrompt).isFalse() assertThat(initialState.showReinvitePrompt).isFalse()
initialState.composerState.richTextEditorState.requestFocus() initialState.composerState.richTextEditorState.requestFocus()
val focusedState = awaitItem() val focusedState = awaitItem()
@ -585,8 +580,8 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val state = awaitFirstItem()
assertThat(awaitItem().userHasPermissionToSendMessage).isTrue() assertThat(state.userHasPermissionToSendMessage).isTrue()
} }
} }
@ -625,8 +620,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
val poll = aMessageEvent( val poll = aMessageEvent(
content = aTimelineItemPollContent() content = aTimelineItemPollContent()
) )
@ -641,6 +635,12 @@ class MessagesPresenterTest {
} }
} }
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
// Skip 2 item if Mentions feature is enabled, else 1
skipItems(if (FeatureFlags.Mentions.defaultValue) 2 else 1)
return awaitItem()
}
private fun TestScope.createMessagesPresenter( private fun TestScope.createMessagesPresenter(
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(), coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
matrixRoom: MatrixRoom = FakeMatrixRoom().apply { matrixRoom: MatrixRoom = FakeMatrixRoom().apply {

105
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt

@ -112,8 +112,7 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
assertThat(initialState.isFullScreen).isFalse() assertThat(initialState.isFullScreen).isFalse()
assertThat(initialState.richTextEditorState.messageHtml).isEqualTo("") assertThat(initialState.richTextEditorState.messageHtml).isEqualTo("")
assertThat(initialState.mode).isEqualTo(MessageComposerMode.Normal) assertThat(initialState.mode).isEqualTo(MessageComposerMode.Normal)
@ -129,8 +128,7 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.eventSink.invoke(MessageComposerEvents.ToggleFullScreenState) initialState.eventSink.invoke(MessageComposerEvents.ToggleFullScreenState)
val fullscreenState = awaitItem() val fullscreenState = awaitItem()
assertThat(fullscreenState.isFullScreen).isTrue() assertThat(fullscreenState.isFullScreen).isTrue()
@ -146,8 +144,7 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.richTextEditorState.setHtml(A_MESSAGE) initialState.richTextEditorState.setHtml(A_MESSAGE)
assertThat(initialState.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE) assertThat(initialState.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE)
initialState.richTextEditorState.setHtml("") initialState.richTextEditorState.setHtml("")
@ -162,8 +159,7 @@ class MessageComposerPresenterTest {
val state = presenter.present() val state = presenter.present()
remember(state, state.richTextEditorState.messageHtml) { state } remember(state, state.richTextEditorState.messageHtml) { state }
}.test { }.test {
skipItems(1) var state = awaitFirstItem()
var state = awaitItem()
val mode = anEditMode() val mode = anEditMode()
state.eventSink.invoke(MessageComposerEvents.SetMode(mode)) state.eventSink.invoke(MessageComposerEvents.SetMode(mode))
state = awaitItem() state = awaitItem()
@ -183,8 +179,7 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) var state = awaitFirstItem()
var state = awaitItem()
val mode = aReplyMode() val mode = aReplyMode()
state.eventSink.invoke(MessageComposerEvents.SetMode(mode)) state.eventSink.invoke(MessageComposerEvents.SetMode(mode))
state = awaitItem() state = awaitItem()
@ -200,8 +195,7 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) var state = awaitFirstItem()
var state = awaitItem()
val mode = aReplyMode() val mode = aReplyMode()
state.eventSink.invoke(MessageComposerEvents.SetMode(mode)) state.eventSink.invoke(MessageComposerEvents.SetMode(mode))
state = awaitItem() state = awaitItem()
@ -220,8 +214,7 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) var state = awaitFirstItem()
var state = awaitItem()
val mode = aQuoteMode() val mode = aQuoteMode()
state.eventSink.invoke(MessageComposerEvents.SetMode(mode)) state.eventSink.invoke(MessageComposerEvents.SetMode(mode))
state = awaitItem() state = awaitItem()
@ -238,8 +231,7 @@ class MessageComposerPresenterTest {
val state = presenter.present() val state = presenter.present()
remember(state, state.richTextEditorState.messageHtml) { state } remember(state, state.richTextEditorState.messageHtml) { state }
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.richTextEditorState.setHtml(A_MESSAGE) initialState.richTextEditorState.setHtml(A_MESSAGE)
val withMessageState = awaitItem() val withMessageState = awaitItem()
assertThat(withMessageState.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE) assertThat(withMessageState.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE)
@ -269,8 +261,7 @@ class MessageComposerPresenterTest {
val state = presenter.present() val state = presenter.present()
remember(state, state.richTextEditorState.messageHtml) { state } remember(state, state.richTextEditorState.messageHtml) { state }
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
assertThat(initialState.richTextEditorState.messageHtml).isEqualTo("") assertThat(initialState.richTextEditorState.messageHtml).isEqualTo("")
val mode = anEditMode() val mode = anEditMode()
initialState.eventSink.invoke(MessageComposerEvents.SetMode(mode)) initialState.eventSink.invoke(MessageComposerEvents.SetMode(mode))
@ -308,8 +299,7 @@ class MessageComposerPresenterTest {
val state = presenter.present() val state = presenter.present()
remember(state, state.richTextEditorState.messageHtml) { state } remember(state, state.richTextEditorState.messageHtml) { state }
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
assertThat(initialState.richTextEditorState.messageHtml).isEqualTo("") assertThat(initialState.richTextEditorState.messageHtml).isEqualTo("")
val mode = anEditMode(eventId = null, transactionId = A_TRANSACTION_ID) val mode = anEditMode(eventId = null, transactionId = A_TRANSACTION_ID)
initialState.eventSink.invoke(MessageComposerEvents.SetMode(mode)) initialState.eventSink.invoke(MessageComposerEvents.SetMode(mode))
@ -346,8 +336,7 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
assertThat(initialState.richTextEditorState.messageHtml).isEqualTo("") assertThat(initialState.richTextEditorState.messageHtml).isEqualTo("")
val mode = aReplyMode() val mode = aReplyMode()
initialState.eventSink.invoke(MessageComposerEvents.SetMode(mode)) initialState.eventSink.invoke(MessageComposerEvents.SetMode(mode))
@ -377,8 +366,7 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
assertThat(initialState.showAttachmentSourcePicker).isFalse() assertThat(initialState.showAttachmentSourcePicker).isFalse()
initialState.eventSink(MessageComposerEvents.AddAttachment) initialState.eventSink(MessageComposerEvents.AddAttachment)
assertThat(awaitItem().showAttachmentSourcePicker).isTrue() assertThat(awaitItem().showAttachmentSourcePicker).isTrue()
@ -391,8 +379,7 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.eventSink(MessageComposerEvents.AddAttachment) initialState.eventSink(MessageComposerEvents.AddAttachment)
skipItems(1) skipItems(1)
@ -426,8 +413,7 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery) initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery)
val previewingState = awaitItem() val previewingState = awaitItem()
assertThat(previewingState.showAttachmentSourcePicker).isFalse() assertThat(previewingState.showAttachmentSourcePicker).isFalse()
@ -461,8 +447,7 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery) initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery)
val previewingState = awaitItem() val previewingState = awaitItem()
assertThat(previewingState.showAttachmentSourcePicker).isFalse() assertThat(previewingState.showAttachmentSourcePicker).isFalse()
@ -480,8 +465,7 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery) initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery)
// No crashes here, otherwise it fails // No crashes here, otherwise it fails
} }
@ -501,8 +485,7 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles) initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles)
val sendingState = awaitItem() val sendingState = awaitItem()
assertThat(sendingState.showAttachmentSourcePicker).isFalse() assertThat(sendingState.showAttachmentSourcePicker).isFalse()
@ -523,8 +506,7 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.eventSink(MessageComposerEvents.AddAttachment) initialState.eventSink(MessageComposerEvents.AddAttachment)
val attachmentOpenState = awaitItem() val attachmentOpenState = awaitItem()
assertThat(attachmentOpenState.showAttachmentSourcePicker).isTrue() assertThat(attachmentOpenState.showAttachmentSourcePicker).isTrue()
@ -541,8 +523,7 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.eventSink(MessageComposerEvents.AddAttachment) initialState.eventSink(MessageComposerEvents.AddAttachment)
val attachmentOpenState = awaitItem() val attachmentOpenState = awaitItem()
assertThat(attachmentOpenState.showAttachmentSourcePicker).isTrue() assertThat(attachmentOpenState.showAttachmentSourcePicker).isTrue()
@ -564,8 +545,7 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.PhotoFromCamera) initialState.eventSink(MessageComposerEvents.PickAttachmentSource.PhotoFromCamera)
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java) assertThat(finalState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java)
@ -585,8 +565,7 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.PhotoFromCamera) initialState.eventSink(MessageComposerEvents.PickAttachmentSource.PhotoFromCamera)
val permissionState = awaitItem() val permissionState = awaitItem()
assertThat(permissionState.showAttachmentSourcePicker).isFalse() assertThat(permissionState.showAttachmentSourcePicker).isFalse()
@ -611,8 +590,7 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.VideoFromCamera) initialState.eventSink(MessageComposerEvents.PickAttachmentSource.VideoFromCamera)
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java) assertThat(finalState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java)
@ -632,8 +610,7 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.VideoFromCamera) initialState.eventSink(MessageComposerEvents.PickAttachmentSource.VideoFromCamera)
val permissionState = awaitItem() val permissionState = awaitItem()
assertThat(permissionState.showAttachmentSourcePicker).isFalse() assertThat(permissionState.showAttachmentSourcePicker).isFalse()
@ -655,8 +632,7 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles) initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles)
val sendingState = awaitItem() val sendingState = awaitItem()
assertThat(sendingState.attachmentsState).isInstanceOf(AttachmentsState.Sending::class.java) assertThat(sendingState.attachmentsState).isInstanceOf(AttachmentsState.Sending::class.java)
@ -675,8 +651,7 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles) initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles)
val sendingState = awaitItem() val sendingState = awaitItem()
assertThat(sendingState.showAttachmentSourcePicker).isFalse() assertThat(sendingState.showAttachmentSourcePicker).isFalse()
@ -693,8 +668,7 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.eventSink(MessageComposerEvents.Error(testException)) initialState.eventSink(MessageComposerEvents.Error(testException))
assertThat(analyticsService.trackedErrors).containsExactly(testException) assertThat(analyticsService.trackedErrors).containsExactly(testException)
} }
@ -706,8 +680,7 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
assertThat(initialState.showTextFormatting).isFalse() assertThat(initialState.showTextFormatting).isFalse()
initialState.eventSink(MessageComposerEvents.AddAttachment) initialState.eventSink(MessageComposerEvents.AddAttachment)
val composerOptions = awaitItem() val composerOptions = awaitItem()
@ -733,9 +706,11 @@ class MessageComposerPresenterTest {
isDirect = false, isDirect = false,
isOneToOne = false, isOneToOne = false,
).apply { ).apply {
givenRoomMembersState(MatrixRoomMembersState.Ready( givenRoomMembersState(
MatrixRoomMembersState.Ready(
persistentListOf(currentUser, invitedUser, bob, david), persistentListOf(currentUser, invitedUser, bob, david),
)) )
)
givenCanTriggerRoomNotification(Result.success(true)) givenCanTriggerRoomNotification(Result.success(true))
} }
val flagsService = FakeFeatureFlagService( val flagsService = FakeFeatureFlagService(
@ -797,9 +772,11 @@ class MessageComposerPresenterTest {
isDirect = true, isDirect = true,
isOneToOne = true, isOneToOne = true,
).apply { ).apply {
givenRoomMembersState(MatrixRoomMembersState.Ready( givenRoomMembersState(
MatrixRoomMembersState.Ready(
persistentListOf(currentUser, invitedUser, bob, david), persistentListOf(currentUser, invitedUser, bob, david),
)) )
)
givenCanTriggerRoomNotification(Result.success(true)) givenCanTriggerRoomNotification(Result.success(true))
} }
val flagsService = FakeFeatureFlagService( val flagsService = FakeFeatureFlagService(
@ -828,8 +805,7 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.richTextEditorState.setHtml("Hey @bo") initialState.richTextEditorState.setHtml("Hey @bo")
initialState.eventSink(MessageComposerEvents.InsertMention(MentionSuggestion.Member(aRoomMember(userId = A_USER_ID_2)))) initialState.eventSink(MessageComposerEvents.InsertMention(MentionSuggestion.Member(aRoomMember(userId = A_USER_ID_2))))
@ -846,8 +822,7 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
// Check intentional mentions on message sent // Check intentional mentions on message sent
val mentionUser1 = listOf(A_USER_ID.value) val mentionUser1 = listOf(A_USER_ID.value)
@ -929,6 +904,12 @@ class MessageComposerPresenterTest {
currentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)), currentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)),
permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionPresenter), permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionPresenter),
) )
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
// Skip 2 item if Mentions feature is enabled, else 1
skipItems(if (FeatureFlags.Mentions.defaultValue) 2 else 1)
return awaitItem()
}
} }
fun anEditMode( fun anEditMode(

34
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt

@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline
import app.cash.molecule.RecompositionMode import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.FakeMessagesNavigator import io.element.android.features.messages.impl.FakeMessagesNavigator
@ -33,6 +34,7 @@ import io.element.android.features.poll.api.actions.EndPollAction
import io.element.android.features.poll.api.actions.SendPollResponseAction import io.element.android.features.poll.api.actions.SendPollResponseAction
import io.element.android.features.poll.test.actions.FakeEndPollAction import io.element.android.features.poll.test.actions.FakeEndPollAction
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
@ -73,7 +75,7 @@ class TimelinePresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitFirstItem()
assertThat(initialState.timelineItems).isEmpty() assertThat(initialState.timelineItems).isEmpty()
val loadedNoTimelineState = awaitItem() val loadedNoTimelineState = awaitItem()
assertThat(loadedNoTimelineState.timelineItems).isEmpty() assertThat(loadedNoTimelineState.timelineItems).isEmpty()
@ -87,7 +89,7 @@ class TimelinePresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitFirstItem()
assertThat(initialState.paginationState.hasMoreToLoadBackwards).isTrue() assertThat(initialState.paginationState.hasMoreToLoadBackwards).isTrue()
assertThat(initialState.paginationState.isBackPaginating).isFalse() assertThat(initialState.paginationState.isBackPaginating).isFalse()
initialState.eventSink.invoke(TimelineEvents.LoadMore) initialState.eventSink.invoke(TimelineEvents.LoadMore)
@ -106,7 +108,7 @@ class TimelinePresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitFirstItem()
skipItems(1) skipItems(1)
assertThat(initialState.highlightedEventId).isNull() assertThat(initialState.highlightedEventId).isNull()
initialState.eventSink.invoke(TimelineEvents.SetHighlightedEvent(AN_EVENT_ID)) initialState.eventSink.invoke(TimelineEvents.SetHighlightedEvent(AN_EVENT_ID))
@ -130,7 +132,7 @@ class TimelinePresenterTest {
presenter.present() presenter.present()
}.test { }.test {
assertThat(timeline.sendReadReceiptCount).isEqualTo(0) assertThat(timeline.sendReadReceiptCount).isEqualTo(0)
val initialState = awaitItem() val initialState = awaitFirstItem()
// Wait for timeline items to be populated // Wait for timeline items to be populated
skipItems(1) skipItems(1)
awaitWithLatch { latch -> awaitWithLatch { latch ->
@ -154,7 +156,7 @@ class TimelinePresenterTest {
presenter.present() presenter.present()
}.test { }.test {
assertThat(timeline.sendReadReceiptCount).isEqualTo(0) assertThat(timeline.sendReadReceiptCount).isEqualTo(0)
val initialState = awaitItem() val initialState = awaitFirstItem()
// Wait for timeline items to be populated // Wait for timeline items to be populated
skipItems(1) skipItems(1)
awaitWithLatch { latch -> awaitWithLatch { latch ->
@ -178,7 +180,7 @@ class TimelinePresenterTest {
presenter.present() presenter.present()
}.test { }.test {
assertThat(timeline.sendReadReceiptCount).isEqualTo(0) assertThat(timeline.sendReadReceiptCount).isEqualTo(0)
val initialState = awaitItem() val initialState = awaitFirstItem()
// Wait for timeline items to be populated // Wait for timeline items to be populated
skipItems(1) skipItems(1)
awaitWithLatch { latch -> awaitWithLatch { latch ->
@ -197,7 +199,7 @@ class TimelinePresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitFirstItem()
assertThat(initialState.newEventState).isEqualTo(NewEventState.None) assertThat(initialState.newEventState).isEqualTo(NewEventState.None)
assertThat(initialState.timelineItems.size).isEqualTo(0) assertThat(initialState.timelineItems.size).isEqualTo(0)
timeline.updateTimelineItems { timeline.updateTimelineItems {
@ -245,7 +247,7 @@ class TimelinePresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitFirstItem()
assertThat(initialState.newEventState).isEqualTo(NewEventState.None) assertThat(initialState.newEventState).isEqualTo(NewEventState.None)
assertThat(initialState.timelineItems.size).isEqualTo(0) assertThat(initialState.timelineItems.size).isEqualTo(0)
val now = Date().time val now = Date().time
@ -302,7 +304,7 @@ class TimelinePresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitFirstItem()
initialState.eventSink.invoke(TimelineEvents.PollAnswerSelected(AN_EVENT_ID, "anAnswerId")) initialState.eventSink.invoke(TimelineEvents.PollAnswerSelected(AN_EVENT_ID, "anAnswerId"))
} }
delay(1) delay(1)
@ -318,7 +320,7 @@ class TimelinePresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitFirstItem()
initialState.eventSink.invoke(TimelineEvents.PollEndClicked(AN_EVENT_ID)) initialState.eventSink.invoke(TimelineEvents.PollEndClicked(AN_EVENT_ID))
} }
delay(1) delay(1)
@ -334,7 +336,7 @@ class TimelinePresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
awaitItem().eventSink(TimelineEvents.PollEditClicked(AN_EVENT_ID)) awaitFirstItem().eventSink(TimelineEvents.PollEditClicked(AN_EVENT_ID))
assertThat(navigator.onEditPollClickedCount).isEqualTo(1) assertThat(navigator.onEditPollClickedCount).isEqualTo(1)
} }
} }
@ -351,13 +353,21 @@ class TimelinePresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) // skip initial state
assertThat(redactedVoiceMessageManager.invocations.size).isEqualTo(0) assertThat(redactedVoiceMessageManager.invocations.size).isEqualTo(0)
awaitFirstItem()
awaitItem().let { awaitItem().let {
assertThat(it.timelineItems).isNotEmpty() assertThat(it.timelineItems).isNotEmpty()
}
assertThat(redactedVoiceMessageManager.invocations.size).isEqualTo(1) assertThat(redactedVoiceMessageManager.invocations.size).isEqualTo(1)
} }
} }
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
// Skip 1 item if Mentions feature is enabled
if (FeatureFlags.Mentions.defaultValue) {
skipItems(1)
}
return awaitItem()
} }
private fun TestScope.createTimelinePresenter( private fun TestScope.createTimelinePresenter(

6
libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt

@ -65,21 +65,21 @@ enum class FeatureFlags(
key = "feature.mentions", key = "feature.mentions",
title = "Mentions", title = "Mentions",
description = "Type `@` to get mention suggestions and insert them", description = "Type `@` to get mention suggestions and insert them",
defaultValue = false, defaultValue = true,
isFinished = false, isFinished = false,
), ),
SecureStorage( SecureStorage(
key = "feature.securestorage", key = "feature.securestorage",
title = "Chat backup", title = "Chat backup",
description = "Allow access to backup and restore chat history settings", description = "Allow access to backup and restore chat history settings",
defaultValue = false, defaultValue = true,
isFinished = false, isFinished = false,
), ),
ReadReceipts( ReadReceipts(
key = "feature.readreceipts", key = "feature.readreceipts",
title = "Show read receipts", title = "Show read receipts",
description = null, description = null,
defaultValue = false, defaultValue = true,
isFinished = false, isFinished = false,
), ),
} }

6
libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt

@ -39,9 +39,9 @@ class StaticFeatureFlagProvider @Inject constructor() :
FeatureFlags.NotificationSettings -> true FeatureFlags.NotificationSettings -> true
FeatureFlags.VoiceMessages -> true FeatureFlags.VoiceMessages -> true
FeatureFlags.PinUnlock -> true FeatureFlags.PinUnlock -> true
FeatureFlags.Mentions -> false FeatureFlags.Mentions -> true
FeatureFlags.SecureStorage -> false FeatureFlags.SecureStorage -> true
FeatureFlags.ReadReceipts -> false FeatureFlags.ReadReceipts -> true
} }
} else { } else {
false false

Loading…
Cancel
Save