Benoit Marty
4 months ago
committed by
Benoit Marty
21 changed files with 771 additions and 6 deletions
@ -0,0 +1,29 @@ |
|||||||
|
/* |
||||||
|
* 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. |
||||||
|
*/ |
||||||
|
|
||||||
|
plugins { |
||||||
|
id("io.element.android-library") |
||||||
|
id("kotlin-parcelize") |
||||||
|
} |
||||||
|
|
||||||
|
android { |
||||||
|
namespace = "io.element.android.features.share.api" |
||||||
|
} |
||||||
|
|
||||||
|
dependencies { |
||||||
|
implementation(projects.libraries.architecture) |
||||||
|
implementation(projects.libraries.matrix.api) |
||||||
|
} |
@ -0,0 +1,41 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2024 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.share.api |
||||||
|
|
||||||
|
import android.content.Intent |
||||||
|
import com.bumble.appyx.core.modality.BuildContext |
||||||
|
import com.bumble.appyx.core.node.Node |
||||||
|
import com.bumble.appyx.core.plugin.Plugin |
||||||
|
import io.element.android.libraries.architecture.FeatureEntryPoint |
||||||
|
import io.element.android.libraries.architecture.NodeInputs |
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId |
||||||
|
|
||||||
|
interface ShareEntryPoint : FeatureEntryPoint { |
||||||
|
data class Params(val intent: Intent) : NodeInputs |
||||||
|
|
||||||
|
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder |
||||||
|
|
||||||
|
interface Callback : Plugin { |
||||||
|
fun onDone(roomIds: List<RoomId>) |
||||||
|
} |
||||||
|
|
||||||
|
interface NodeBuilder { |
||||||
|
fun params(params: Params): NodeBuilder |
||||||
|
fun callback(callback: Callback): NodeBuilder |
||||||
|
fun build(): Node |
||||||
|
} |
||||||
|
} |
@ -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. |
||||||
|
*/ |
||||||
|
|
||||||
|
plugins { |
||||||
|
id("io.element.android-compose-library") |
||||||
|
alias(libs.plugins.ksp) |
||||||
|
alias(libs.plugins.anvil) |
||||||
|
id("kotlin-parcelize") |
||||||
|
} |
||||||
|
|
||||||
|
android { |
||||||
|
namespace = "io.element.android.features.share.impl" |
||||||
|
|
||||||
|
testOptions { |
||||||
|
unitTests { |
||||||
|
isIncludeAndroidResources = true |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
anvil { |
||||||
|
generateDaggerFactories.set(true) |
||||||
|
} |
||||||
|
|
||||||
|
dependencies { |
||||||
|
anvil(projects.anvilcodegen) |
||||||
|
implementation(projects.anvilannotations) |
||||||
|
|
||||||
|
implementation(projects.appconfig) |
||||||
|
implementation(projects.libraries.androidutils) |
||||||
|
implementation(projects.libraries.core) |
||||||
|
implementation(projects.libraries.androidutils) |
||||||
|
implementation(projects.libraries.architecture) |
||||||
|
implementation(projects.libraries.matrix.api) |
||||||
|
implementation(projects.libraries.matrixui) |
||||||
|
implementation(projects.libraries.designsystem) |
||||||
|
implementation(projects.libraries.mediaupload.api) |
||||||
|
implementation(projects.libraries.roomselect.api) |
||||||
|
implementation(projects.libraries.uiStrings) |
||||||
|
implementation(projects.libraries.testtags) |
||||||
|
api(libs.statemachine) |
||||||
|
api(projects.features.share.api) |
||||||
|
|
||||||
|
testImplementation(libs.test.junit) |
||||||
|
testImplementation(libs.coroutines.test) |
||||||
|
testImplementation(libs.molecule.runtime) |
||||||
|
testImplementation(libs.test.truth) |
||||||
|
testImplementation(libs.test.turbine) |
||||||
|
testImplementation(libs.test.robolectric) |
||||||
|
testImplementation(libs.androidx.compose.ui.test.junit) |
||||||
|
testImplementation(projects.libraries.matrix.test) |
||||||
|
testImplementation(projects.tests.testutils) |
||||||
|
testReleaseImplementation(libs.androidx.compose.ui.test.manifest) |
||||||
|
|
||||||
|
ksp(libs.showkase.processor) |
||||||
|
} |
@ -0,0 +1,49 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2024 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.share.impl |
||||||
|
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext |
||||||
|
import com.bumble.appyx.core.node.Node |
||||||
|
import com.bumble.appyx.core.plugin.Plugin |
||||||
|
import com.squareup.anvil.annotations.ContributesBinding |
||||||
|
import io.element.android.features.share.api.ShareEntryPoint |
||||||
|
import io.element.android.libraries.architecture.createNode |
||||||
|
import io.element.android.libraries.di.SessionScope |
||||||
|
import javax.inject.Inject |
||||||
|
|
||||||
|
@ContributesBinding(SessionScope::class) |
||||||
|
class DefaultShareEntryPoint @Inject constructor() : ShareEntryPoint { |
||||||
|
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): ShareEntryPoint.NodeBuilder { |
||||||
|
val plugins = ArrayList<Plugin>() |
||||||
|
|
||||||
|
return object : ShareEntryPoint.NodeBuilder { |
||||||
|
override fun params(params: ShareEntryPoint.Params): ShareEntryPoint.NodeBuilder { |
||||||
|
plugins += ShareNode.Inputs(intent = params.intent) |
||||||
|
return this |
||||||
|
} |
||||||
|
|
||||||
|
override fun callback(callback: ShareEntryPoint.Callback): ShareEntryPoint.NodeBuilder { |
||||||
|
plugins += callback |
||||||
|
return this |
||||||
|
} |
||||||
|
|
||||||
|
override fun build(): Node { |
||||||
|
return parentNode.createNode<ShareNode>(buildContext, plugins) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2024 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.share.impl |
||||||
|
|
||||||
|
sealed interface ShareEvents { |
||||||
|
data object ClearError : ShareEvents |
||||||
|
} |
@ -0,0 +1,126 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2024 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.share.impl |
||||||
|
|
||||||
|
import android.content.ComponentName |
||||||
|
import android.content.Context |
||||||
|
import android.content.Intent |
||||||
|
import android.content.pm.PackageManager |
||||||
|
import android.content.pm.ResolveInfo |
||||||
|
import android.net.Uri |
||||||
|
import com.squareup.anvil.annotations.ContributesBinding |
||||||
|
import io.element.android.libraries.androidutils.compat.getParcelableArrayListExtraCompat |
||||||
|
import io.element.android.libraries.androidutils.compat.getParcelableExtraCompat |
||||||
|
import io.element.android.libraries.androidutils.compat.queryIntentActivitiesCompat |
||||||
|
import io.element.android.libraries.core.mimetype.MimeTypes |
||||||
|
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAny |
||||||
|
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeApplication |
||||||
|
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAudio |
||||||
|
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeFile |
||||||
|
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage |
||||||
|
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeText |
||||||
|
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo |
||||||
|
import io.element.android.libraries.di.AppScope |
||||||
|
import io.element.android.libraries.di.ApplicationContext |
||||||
|
import javax.inject.Inject |
||||||
|
|
||||||
|
interface ShareIntentHandler { |
||||||
|
suspend fun handleIncomingShareIntent( |
||||||
|
intent: Intent, |
||||||
|
onFile: suspend (List<DefaultShareIntentHandler.FileToShare>) -> Boolean, |
||||||
|
onPlainText: suspend (String) -> Boolean, |
||||||
|
): Boolean |
||||||
|
} |
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class) |
||||||
|
class DefaultShareIntentHandler @Inject constructor( |
||||||
|
@ApplicationContext private val context: Context, |
||||||
|
) : ShareIntentHandler { |
||||||
|
data class FileToShare( |
||||||
|
val uri: Uri, |
||||||
|
val mimeType: String, |
||||||
|
) |
||||||
|
|
||||||
|
/** |
||||||
|
* This methods aims to handle incoming share intents. |
||||||
|
* |
||||||
|
* @return true if it can handle the intent data, false otherwise |
||||||
|
*/ |
||||||
|
override suspend fun handleIncomingShareIntent( |
||||||
|
intent: Intent, |
||||||
|
onFile: suspend (List<FileToShare>) -> Boolean, |
||||||
|
onPlainText: suspend (String) -> Boolean, |
||||||
|
): Boolean { |
||||||
|
val type = intent.resolveType(context) ?: return false |
||||||
|
return when { |
||||||
|
type == MimeTypes.PlainText -> handlePlainText(intent, onPlainText) |
||||||
|
type.isMimeTypeImage() || |
||||||
|
type.isMimeTypeVideo() || |
||||||
|
type.isMimeTypeAudio() || |
||||||
|
type.isMimeTypeApplication() || |
||||||
|
type.isMimeTypeFile() || |
||||||
|
type.isMimeTypeText() || |
||||||
|
type.isMimeTypeAny() -> onFile(getIncomingFiles(intent, type)) |
||||||
|
else -> false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private suspend fun handlePlainText(intent: Intent, onPlainText: suspend (String) -> Boolean): Boolean { |
||||||
|
val content = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)?.toString() |
||||||
|
return if (content?.isNotEmpty() == true) { |
||||||
|
onPlainText(content) |
||||||
|
} else { |
||||||
|
false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Use this function to retrieve files which are shared from another application or internally |
||||||
|
* by using android.intent.action.SEND or android.intent.action.SEND_MULTIPLE actions. |
||||||
|
*/ |
||||||
|
private fun getIncomingFiles(data: Intent, type: String): List<FileToShare> { |
||||||
|
val uriList = mutableListOf<Uri>() |
||||||
|
if (data.action == Intent.ACTION_SEND) { |
||||||
|
data.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM)?.let { uriList.add(it) } |
||||||
|
} else if (data.action == Intent.ACTION_SEND_MULTIPLE) { |
||||||
|
val extraUriList: List<Uri>? = data.getParcelableArrayListExtraCompat(Intent.EXTRA_STREAM) |
||||||
|
extraUriList?.let { uriList.addAll(it) } |
||||||
|
} |
||||||
|
val resInfoList: List<ResolveInfo> = context.packageManager.queryIntentActivitiesCompat(data, PackageManager.MATCH_DEFAULT_ONLY) |
||||||
|
uriList.forEach { |
||||||
|
for (resolveInfo in resInfoList) { |
||||||
|
val packageName: String = resolveInfo.activityInfo.packageName |
||||||
|
// Replace implicit intent by an explicit to fix crash on some devices like Xiaomi. |
||||||
|
// see https://juejin.cn/post/7031736325422186510 |
||||||
|
try { |
||||||
|
context.grantUriPermission(packageName, it, Intent.FLAG_GRANT_READ_URI_PERMISSION) |
||||||
|
} catch (e: Exception) { |
||||||
|
continue |
||||||
|
} |
||||||
|
data.action = null |
||||||
|
data.component = ComponentName(packageName, resolveInfo.activityInfo.name) |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
return uriList.map { uri -> |
||||||
|
FileToShare( |
||||||
|
uri = uri, |
||||||
|
mimeType = type |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,101 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 20244 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.share.impl |
||||||
|
|
||||||
|
import android.content.Intent |
||||||
|
import android.os.Parcelable |
||||||
|
import androidx.compose.foundation.layout.Box |
||||||
|
import androidx.compose.runtime.Composable |
||||||
|
import androidx.compose.ui.Modifier |
||||||
|
import com.bumble.appyx.core.composable.Children |
||||||
|
import com.bumble.appyx.core.modality.BuildContext |
||||||
|
import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel |
||||||
|
import com.bumble.appyx.core.node.Node |
||||||
|
import com.bumble.appyx.core.node.ParentNode |
||||||
|
import com.bumble.appyx.core.plugin.Plugin |
||||||
|
import dagger.assisted.Assisted |
||||||
|
import dagger.assisted.AssistedInject |
||||||
|
import io.element.android.anvilannotations.ContributesNode |
||||||
|
import io.element.android.features.share.api.ShareEntryPoint |
||||||
|
import io.element.android.libraries.architecture.NodeInputs |
||||||
|
import io.element.android.libraries.architecture.inputs |
||||||
|
import io.element.android.libraries.di.SessionScope |
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId |
||||||
|
import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint |
||||||
|
import io.element.android.libraries.roomselect.api.RoomSelectMode |
||||||
|
import kotlinx.parcelize.Parcelize |
||||||
|
|
||||||
|
@ContributesNode(SessionScope::class) |
||||||
|
class ShareNode @AssistedInject constructor( |
||||||
|
@Assisted buildContext: BuildContext, |
||||||
|
@Assisted plugins: List<Plugin>, |
||||||
|
presenterFactory: SharePresenter.Factory, |
||||||
|
private val roomSelectEntryPoint: RoomSelectEntryPoint, |
||||||
|
) : ParentNode<ShareNode.NavTarget>( |
||||||
|
navModel = PermanentNavModel( |
||||||
|
navTargets = setOf(NavTarget), |
||||||
|
savedStateMap = buildContext.savedStateMap, |
||||||
|
), |
||||||
|
buildContext = buildContext, |
||||||
|
plugins = plugins, |
||||||
|
) { |
||||||
|
@Parcelize |
||||||
|
object NavTarget : Parcelable |
||||||
|
|
||||||
|
data class Inputs(val intent: Intent) : NodeInputs |
||||||
|
|
||||||
|
private val inputs = inputs<Inputs>() |
||||||
|
private val presenter = presenterFactory.create(inputs.intent) |
||||||
|
private val callbacks = plugins.filterIsInstance<ShareEntryPoint.Callback>() |
||||||
|
|
||||||
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { |
||||||
|
val callback = object : RoomSelectEntryPoint.Callback { |
||||||
|
override fun onRoomSelected(roomIds: List<RoomId>) { |
||||||
|
presenter.onRoomSelected(roomIds) |
||||||
|
} |
||||||
|
|
||||||
|
override fun onCancel() { |
||||||
|
navigateUp() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return roomSelectEntryPoint.nodeBuilder(this, buildContext) |
||||||
|
.callback(callback) |
||||||
|
.params(RoomSelectEntryPoint.Params(mode = RoomSelectMode.Share)) |
||||||
|
.build() |
||||||
|
} |
||||||
|
|
||||||
|
@Composable |
||||||
|
override fun View(modifier: Modifier) { |
||||||
|
Box(modifier = modifier) { |
||||||
|
// Will render to room select screen |
||||||
|
Children( |
||||||
|
navModel = navModel, |
||||||
|
) |
||||||
|
|
||||||
|
val state = presenter.present() |
||||||
|
ShareView( |
||||||
|
state = state, |
||||||
|
onShareSuccess = ::onShareSuccess, |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun onShareSuccess(roomIds: List<RoomId>) { |
||||||
|
callbacks.forEach { it.onDone(roomIds) } |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,112 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2024 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.share.impl |
||||||
|
|
||||||
|
import android.content.Intent |
||||||
|
import androidx.compose.runtime.Composable |
||||||
|
import androidx.compose.runtime.MutableState |
||||||
|
import androidx.compose.runtime.mutableStateOf |
||||||
|
import dagger.assisted.Assisted |
||||||
|
import dagger.assisted.AssistedFactory |
||||||
|
import dagger.assisted.AssistedInject |
||||||
|
import io.element.android.libraries.architecture.AsyncAction |
||||||
|
import io.element.android.libraries.architecture.Presenter |
||||||
|
import io.element.android.libraries.architecture.runCatchingUpdatingState |
||||||
|
import io.element.android.libraries.core.bool.orFalse |
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient |
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId |
||||||
|
import io.element.android.libraries.mediaupload.api.MediaPreProcessor |
||||||
|
import io.element.android.libraries.mediaupload.api.MediaSender |
||||||
|
import kotlinx.coroutines.CoroutineScope |
||||||
|
import kotlinx.coroutines.launch |
||||||
|
|
||||||
|
class SharePresenter @AssistedInject constructor( |
||||||
|
@Assisted private val intent: Intent, |
||||||
|
private val appCoroutineScope: CoroutineScope, |
||||||
|
private val shareIntentHandler: ShareIntentHandler, |
||||||
|
private val matrixClient: MatrixClient, |
||||||
|
private val mediaPreProcessor: MediaPreProcessor, |
||||||
|
) : Presenter<ShareState> { |
||||||
|
@AssistedFactory |
||||||
|
interface Factory { |
||||||
|
fun create(intent: Intent): SharePresenter |
||||||
|
} |
||||||
|
|
||||||
|
private val shareActionState: MutableState<AsyncAction<List<RoomId>>> = mutableStateOf(AsyncAction.Uninitialized) |
||||||
|
|
||||||
|
fun onRoomSelected(roomIds: List<RoomId>) { |
||||||
|
appCoroutineScope.share(intent, roomIds, shareActionState) |
||||||
|
} |
||||||
|
|
||||||
|
@Composable |
||||||
|
override fun present(): ShareState { |
||||||
|
fun handleEvents(event: ShareEvents) { |
||||||
|
when (event) { |
||||||
|
ShareEvents.ClearError -> shareActionState.value = AsyncAction.Uninitialized |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return ShareState( |
||||||
|
shareAction = shareActionState.value, |
||||||
|
eventSink = { handleEvents(it) } |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
private fun CoroutineScope.share( |
||||||
|
intent: Intent, |
||||||
|
roomIds: List<RoomId>, |
||||||
|
shareActionState: MutableState<AsyncAction<List<RoomId>>>, |
||||||
|
) = launch { |
||||||
|
suspend { |
||||||
|
val result = shareIntentHandler.handleIncomingShareIntent( |
||||||
|
intent, |
||||||
|
onFile = { filesToShare -> |
||||||
|
roomIds |
||||||
|
.map { roomId -> |
||||||
|
val room = matrixClient.getRoom(roomId) ?: return@map false |
||||||
|
val mediaSender = MediaSender(preProcessor = mediaPreProcessor, room = room) |
||||||
|
filesToShare |
||||||
|
.map { fileToShare -> |
||||||
|
mediaSender.sendMedia( |
||||||
|
uri = fileToShare.uri, |
||||||
|
mimeType = fileToShare.mimeType, |
||||||
|
compressIfPossible = true, |
||||||
|
).isSuccess |
||||||
|
} |
||||||
|
.all { it } |
||||||
|
} |
||||||
|
.all { it } |
||||||
|
}, |
||||||
|
onPlainText = { text -> |
||||||
|
roomIds |
||||||
|
.map { roomId -> |
||||||
|
matrixClient.getRoom(roomId)?.sendMessage( |
||||||
|
body = text, |
||||||
|
htmlBody = null, |
||||||
|
mentions = emptyList(), |
||||||
|
)?.isSuccess.orFalse() |
||||||
|
} |
||||||
|
.all { it } |
||||||
|
} |
||||||
|
) |
||||||
|
if (!result) { |
||||||
|
throw Exception("Failed to handle incoming share intent") |
||||||
|
} |
||||||
|
roomIds |
||||||
|
}.runCatchingUpdatingState(shareActionState) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,25 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2024 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.share.impl |
||||||
|
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction |
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId |
||||||
|
|
||||||
|
data class ShareState( |
||||||
|
val shareAction: AsyncAction<List<RoomId>>, |
||||||
|
val eventSink: (ShareEvents) -> Unit |
||||||
|
) |
@ -0,0 +1,47 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2024 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.share.impl |
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider |
||||||
|
import io.element.android.libraries.architecture.AsyncAction |
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId |
||||||
|
|
||||||
|
open class ShareStateProvider : PreviewParameterProvider<ShareState> { |
||||||
|
override val values: Sequence<ShareState> |
||||||
|
get() = sequenceOf( |
||||||
|
aShareState(), |
||||||
|
aShareState( |
||||||
|
shareAction = AsyncAction.Loading, |
||||||
|
), |
||||||
|
aShareState( |
||||||
|
shareAction = AsyncAction.Success( |
||||||
|
listOf(RoomId("!room2:domain")), |
||||||
|
) |
||||||
|
), |
||||||
|
aShareState( |
||||||
|
shareAction = AsyncAction.Failure(Throwable("error")), |
||||||
|
), |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
fun aShareState( |
||||||
|
shareAction: AsyncAction<List<RoomId>> = AsyncAction.Uninitialized, |
||||||
|
eventSink: (ShareEvents) -> Unit = {} |
||||||
|
) = ShareState( |
||||||
|
shareAction = shareAction, |
||||||
|
eventSink = eventSink |
||||||
|
) |
@ -0,0 +1,49 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2024 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.share.impl |
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable |
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter |
||||||
|
import io.element.android.libraries.designsystem.components.async.AsyncActionView |
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview |
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight |
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId |
||||||
|
|
||||||
|
@Composable |
||||||
|
fun ShareView( |
||||||
|
state: ShareState, |
||||||
|
onShareSuccess: (List<RoomId>) -> Unit, |
||||||
|
) { |
||||||
|
AsyncActionView( |
||||||
|
async = state.shareAction, |
||||||
|
onSuccess = { |
||||||
|
onShareSuccess(it) |
||||||
|
}, |
||||||
|
onErrorDismiss = { |
||||||
|
state.eventSink(ShareEvents.ClearError) |
||||||
|
}, |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
@PreviewsDayNight |
||||||
|
@Composable |
||||||
|
internal fun ShareViewPreview(@PreviewParameter(ShareStateProvider::class) state: ShareState) = ElementPreview { |
||||||
|
ShareView( |
||||||
|
state = state, |
||||||
|
onShareSuccess = {} |
||||||
|
) |
||||||
|
} |
Loading…
Reference in new issue