Add PickerLauncher wrapper for media/file pickers. (#361)

* Add `PickerLauncher` wrapper for media/file pickers.

* Add FileProvider path, handle Camera picker and add NoOp implementation to fix tests.

* Move media pickers to their own module.

* Add missing media pickers

* Add feature flag and some extra tests
This commit is contained in:
Jorge Martin Espinosa 2023-04-28 10:52:34 +02:00 committed by GitHub
parent 78a715ce8d
commit eeca1c9ee3
16 changed files with 483 additions and 51 deletions

View file

@ -40,6 +40,8 @@ dependencies {
implementation(projects.libraries.textcomposer)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.dateformatter.api)
implementation(projects.libraries.mediapickers)
implementation(projects.libraries.featureflag.api)
implementation(projects.features.networkmonitor.api)
implementation(libs.coil.compose)
implementation(libs.datetime)
@ -55,6 +57,7 @@ dependencies {
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.dateformatter.test)
testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.libraries.featureflag.test)
androidTestImplementation(libs.test.junitext)
ksp(libs.showkase.processor)

View file

@ -1,38 +0,0 @@
/*
* Copyright (c) 2022 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
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("io.element.android.features.messages.test", appContext.packageName)
}
}

View file

@ -24,4 +24,6 @@ sealed interface MessageComposerEvents {
object CloseSpecialMode : MessageComposerEvents
data class SetMode(val composerMode: MessageComposerMode) : MessageComposerEvents
data class UpdateText(val text: CharSequence) : MessageComposerEvents
object TakePhoto : MessageComposerEvents
}

View file

@ -21,23 +21,37 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
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.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.mediapickers.PickerProvider
import io.element.android.libraries.textcomposer.MessageComposerMode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
class MessageComposerPresenter @Inject constructor(
private val appCoroutineScope: CoroutineScope,
private val room: MatrixRoom
private val room: MatrixRoom,
private val mediaPickerProvider: PickerProvider,
private val featureFlagService: FeatureFlagService,
) : Presenter<MessageComposerState> {
@Composable
override fun present(): MessageComposerState {
val localCoroutineScope = rememberCoroutineScope()
// Example usage of custom pickers
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(onResult = { uri ->
Timber.d("Photo saved at $uri")
})
val isFullScreen = rememberSaveable {
mutableStateOf(false)
}
@ -63,9 +77,14 @@ class MessageComposerPresenter @Inject constructor(
text.value = "".toStableCharSequence()
composerMode.setToNormal()
}
is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage(event.message, composerMode, text)
is MessageComposerEvents.SetMode -> composerMode.value = event.composerMode
}
MessageComposerEvents.TakePhoto -> localCoroutineScope.launch {
if (featureFlagService.isFeatureEnabled(FeatureFlags.ShowMediaUploadingFlow)) {
cameraPhotoPicker.launch()
}
}}
}
return MessageComposerState(
@ -92,6 +111,7 @@ class MessageComposerPresenter @Inject constructor(
capturedMode.eventId,
text
)
is MessageComposerMode.Quote -> TODO()
is MessageComposerMode.Reply -> room.replyMessage(
capturedMode.eventId,

View file

@ -53,6 +53,9 @@ fun MessageComposerView(
composerMode = state.mode,
onCloseSpecialMode = ::onCloseSpecialMode,
onComposerTextChange = ::onComposerTextChange,
onAddAttachment = {
state.eventSink(MessageComposerEvents.TakePhoto)
},
composerCanSendMessage = state.isSendButtonVisible,
composerText = state.text?.charSequence?.toString(),
isInDarkMode = !ElementTheme.colors.isLight,

View file

@ -31,10 +31,12 @@ import io.element.android.features.messages.impl.actionlist.model.TimelineItemAc
import io.element.android.features.messages.impl.textcomposer.MessageComposerPresenter
import io.element.android.features.messages.impl.timeline.TimelinePresenter
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.mediapickers.PickerProvider
import io.element.android.libraries.textcomposer.MessageComposerMode
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
@ -129,7 +131,9 @@ class MessagesPresenterTest {
): MessagesPresenter {
val messageComposerPresenter = MessageComposerPresenter(
appCoroutineScope = this,
room = matrixRoom
room = matrixRoom,
mediaPickerProvider = PickerProvider(isInTest = true),
featureFlagService = FakeFeatureFlagService(),
)
val timelinePresenter = TimelinePresenter(

View file

@ -27,23 +27,37 @@ import io.element.android.features.messages.impl.textcomposer.MessageComposerEve
import io.element.android.features.messages.impl.textcomposer.MessageComposerPresenter
import io.element.android.features.messages.impl.textcomposer.MessageComposerState
import io.element.android.libraries.core.data.StableCharSequence
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.test.ANOTHER_MESSAGE
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.matrix.test.A_REPLY
import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.mediapickers.PickerProvider
import io.element.android.libraries.textcomposer.MessageComposerMode
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.Test
class MessageComposerPresenterTest {
private val pickerProvider = PickerProvider(isInTest = true)
private val featureFlagService = FakeFeatureFlagService().apply {
runBlocking {
setFeatureEnabled(FeatureFlags.ShowMediaUploadingFlow, true)
}
}
@Test
fun `present - initial state`() = runTest {
val presenter = MessageComposerPresenter(
this,
FakeMatrixRoom()
FakeMatrixRoom(),
pickerProvider,
featureFlagService,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -60,7 +74,9 @@ class MessageComposerPresenterTest {
fun `present - toggle fullscreen`() = runTest {
val presenter = MessageComposerPresenter(
this,
FakeMatrixRoom()
FakeMatrixRoom(),
pickerProvider,
featureFlagService,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -79,7 +95,9 @@ class MessageComposerPresenterTest {
fun `present - change message`() = runTest {
val presenter = MessageComposerPresenter(
this,
FakeMatrixRoom()
FakeMatrixRoom(),
pickerProvider,
featureFlagService,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -100,7 +118,9 @@ class MessageComposerPresenterTest {
fun `present - change mode to edit`() = runTest {
val presenter = MessageComposerPresenter(
this,
FakeMatrixRoom()
FakeMatrixRoom(),
pickerProvider,
featureFlagService,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -130,7 +150,9 @@ class MessageComposerPresenterTest {
fun `present - change mode to reply`() = runTest {
val presenter = MessageComposerPresenter(
this,
FakeMatrixRoom()
FakeMatrixRoom(),
pickerProvider,
featureFlagService,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -150,7 +172,9 @@ class MessageComposerPresenterTest {
fun `present - change mode to quote`() = runTest {
val presenter = MessageComposerPresenter(
this,
FakeMatrixRoom()
FakeMatrixRoom(),
pickerProvider,
featureFlagService,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -170,7 +194,9 @@ class MessageComposerPresenterTest {
fun `present - send message`() = runTest {
val presenter = MessageComposerPresenter(
this,
FakeMatrixRoom()
FakeMatrixRoom(),
pickerProvider,
featureFlagService,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -192,7 +218,9 @@ class MessageComposerPresenterTest {
val fakeMatrixRoom = FakeMatrixRoom()
val presenter = MessageComposerPresenter(
this,
fakeMatrixRoom
fakeMatrixRoom,
pickerProvider,
featureFlagService,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -223,7 +251,9 @@ class MessageComposerPresenterTest {
val fakeMatrixRoom = FakeMatrixRoom()
val presenter = MessageComposerPresenter(
this,
fakeMatrixRoom
fakeMatrixRoom,
pickerProvider,
featureFlagService,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -248,6 +278,25 @@ class MessageComposerPresenterTest {
assertThat(fakeMatrixRoom.replyMessageParameter).isEqualTo(A_REPLY)
}
}
@Test
fun `present - Take photo`() = runTest {
val fakeMatrixRoom = FakeMatrixRoom()
val presenter = MessageComposerPresenter(
this,
fakeMatrixRoom,
pickerProvider,
featureFlagService,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(MessageComposerEvents.TakePhoto)
// TODO verify some post processing of the captured image is done
}
}
}
fun anEditMode() = MessageComposerMode.Edit(AN_EVENT_ID, A_MESSAGE)