mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-24 19:00:26 +01:00
Add initial working E2E flow for MediaSendV3.
This commit is contained in:
committed by
Greyson Parrelli
parent
17def87c17
commit
e2feaaf74c
@@ -1,77 +0,0 @@
|
||||
package org.signal.mediasend
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
|
||||
/**
|
||||
* Activity for the media sending flow.
|
||||
*/
|
||||
abstract class MediaSendActivity : FragmentActivity() {
|
||||
protected lateinit var contractArgs: MediaSendActivityContract.Args
|
||||
private set
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
contractArgs = MediaSendActivityContract.Args.fromIntent(intent)
|
||||
|
||||
setContent {
|
||||
val viewModel by viewModels<MediaSendViewModel>(factoryProducer = {
|
||||
MediaSendViewModel.Factory(
|
||||
args = contractArgs,
|
||||
isMeteredFlow = MeteredConnectivity.isMetered(applicationContext)
|
||||
)
|
||||
})
|
||||
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val backStack = rememberNavBackStack(
|
||||
if (state.isCameraFirst) MediaSendNavKey.Capture.Camera else MediaSendNavKey.Select
|
||||
)
|
||||
|
||||
SignalTheme {
|
||||
Surface {
|
||||
MediaSendNavDisplay(
|
||||
state = state,
|
||||
backStack = backStack,
|
||||
callback = viewModel,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
cameraSlot = { },
|
||||
textStoryEditorSlot = { },
|
||||
videoEditorSlot = { },
|
||||
sendSlot = { }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Creates an intent for [MediaSendActivity].
|
||||
*
|
||||
* @param context The context.
|
||||
* @param args The activity arguments.
|
||||
*/
|
||||
fun createIntent(
|
||||
context: Context,
|
||||
args: MediaSendActivityContract.Args = MediaSendActivityContract.Args()
|
||||
): Intent {
|
||||
return Intent(context, MediaSendActivity::class.java).apply {
|
||||
putExtra(MediaSendActivityContract.EXTRA_ARGS, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ import org.signal.core.models.media.Media
|
||||
* class MyMediaSendContract : MediaSendActivityContract(MyMediaSendActivity::class.java)
|
||||
* ```
|
||||
*/
|
||||
class MediaSendActivityContract : ActivityResultContract<MediaSendActivityContract.Args, MediaSendActivityContract.Result?>() {
|
||||
class MediaSendActivityContract(private val clazz: Class<out Activity>) : ActivityResultContract<MediaSendActivityContract.Args, MediaSendActivityContract.Result?>() {
|
||||
|
||||
/**
|
||||
* Creates the intent to launch the media send activity.
|
||||
@@ -38,7 +38,9 @@ class MediaSendActivityContract : ActivityResultContract<MediaSendActivityContra
|
||||
* Subclasses should override this if not using the constructor parameter.
|
||||
*/
|
||||
override fun createIntent(context: Context, input: Args): Intent {
|
||||
return MediaSendActivity.createIntent(context, input)
|
||||
return Intent(context, clazz).apply {
|
||||
putExtra(EXTRA_ARGS, input)
|
||||
}
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): Result? {
|
||||
|
||||
@@ -21,9 +21,6 @@ interface MediaSendCallback : MediaEditScreenCallback, MediaSelectScreenCallback
|
||||
/** Called when the user edits video trim data. */
|
||||
fun onVideoEdited(uri: Uri, isEdited: Boolean) {}
|
||||
|
||||
/** Called when message text changes. */
|
||||
fun onMessageChanged(text: CharSequence?) {}
|
||||
|
||||
object Empty : MediaSendCallback, MediaEditScreenCallback by MediaEditScreenCallback.Empty, MediaSelectScreenCallback by MediaSelectScreenCallback.Empty {
|
||||
override fun setFocusedMedia(media: Media) = Unit
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package org.signal.mediasend
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation3.runtime.NavBackStack
|
||||
import androidx.navigation3.runtime.NavEntry
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import androidx.navigationevent.compose.LocalNavigationEventDispatcherOwner
|
||||
import androidx.navigationevent.compose.rememberNavigationEventDispatcherOwner
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.signal.core.ui.compose.AllDevicePreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.mediasend.edit.MediaEditScreen
|
||||
import org.signal.mediasend.select.MediaSelectScreen
|
||||
|
||||
/**
|
||||
* Enforces the following flow of:
|
||||
*
|
||||
* Capture -> Edit -> Send
|
||||
* Select -> Edit -> Send
|
||||
*/
|
||||
@Composable
|
||||
fun MediaSendNavDisplay(
|
||||
stateFlow: StateFlow<MediaSendState>,
|
||||
backStack: NavBackStack<NavKey>,
|
||||
callback: MediaSendCallback,
|
||||
modifier: Modifier = Modifier,
|
||||
cameraSlot: @Composable () -> Unit = {},
|
||||
textStoryEditorSlot: @Composable () -> Unit = {},
|
||||
videoEditorSlot: @Composable () -> Unit = {},
|
||||
sendSlot: @Composable (MediaSendState) -> Unit = {}
|
||||
) {
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
modifier = modifier.fillMaxSize()
|
||||
) { key ->
|
||||
when (key) {
|
||||
is MediaSendNavKey.Capture -> NavEntry(MediaSendNavKey.Capture.Chrome) {
|
||||
MediaCaptureScreen(
|
||||
backStack = backStack,
|
||||
cameraSlot = cameraSlot,
|
||||
textStoryEditorSlot = textStoryEditorSlot
|
||||
)
|
||||
}
|
||||
|
||||
MediaSendNavKey.Select -> NavEntry(key) {
|
||||
val state by stateFlow.collectAsStateWithLifecycle()
|
||||
MediaSelectScreen(
|
||||
state = state,
|
||||
backStack = backStack,
|
||||
callback = callback
|
||||
)
|
||||
}
|
||||
|
||||
is MediaSendNavKey.Edit -> NavEntry(MediaSendNavKey.Edit) {
|
||||
val state by stateFlow.collectAsStateWithLifecycle()
|
||||
MediaEditScreen(
|
||||
state = state,
|
||||
backStack = backStack,
|
||||
videoEditorSlot = videoEditorSlot,
|
||||
callback = callback
|
||||
)
|
||||
}
|
||||
|
||||
is MediaSendNavKey.Send -> NavEntry(key) {
|
||||
val state by stateFlow.collectAsStateWithLifecycle()
|
||||
sendSlot(state)
|
||||
}
|
||||
|
||||
else -> error("Unknown key: $key")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllDevicePreviews
|
||||
@Composable
|
||||
private fun MediaSendNavDisplayPreview() {
|
||||
Previews.Preview {
|
||||
CompositionLocalProvider(LocalNavigationEventDispatcherOwner provides rememberNavigationEventDispatcherOwner(parent = null)) {
|
||||
MediaSendNavDisplay(
|
||||
stateFlow = MutableStateFlow(MediaSendState(isCameraFirst = true)),
|
||||
backStack = rememberNavBackStack(MediaSendNavKey.Edit),
|
||||
callback = MediaSendCallback.Empty,
|
||||
cameraSlot = { BoxWithText("Camera Slot") },
|
||||
textStoryEditorSlot = { BoxWithText("Text Story Editor Slot") },
|
||||
videoEditorSlot = { BoxWithText("Video Editor Slot") },
|
||||
sendSlot = { _ -> BoxWithText("Send Slot") }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxWithText(text: String, modifier: Modifier = Modifier) {
|
||||
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(text = text)
|
||||
}
|
||||
}
|
||||
@@ -1,101 +1,53 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.mediasend
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation3.runtime.NavBackStack
|
||||
import androidx.navigation3.runtime.NavEntry
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import androidx.navigationevent.NavigationEventDispatcherOwner
|
||||
import androidx.navigationevent.compose.LocalNavigationEventDispatcherOwner
|
||||
import androidx.navigationevent.compose.rememberNavigationEventDispatcherOwner
|
||||
import org.signal.core.ui.compose.AllDevicePreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.mediasend.edit.MediaEditScreen
|
||||
import org.signal.mediasend.select.MediaSelectScreen
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
|
||||
/**
|
||||
* Enforces the following flow of:
|
||||
*
|
||||
* Capture -> Edit -> Send
|
||||
* Select -> Edit -> Send
|
||||
*/
|
||||
@Composable
|
||||
fun MediaSendNavDisplay(
|
||||
state: MediaSendState,
|
||||
backStack: NavBackStack<NavKey>,
|
||||
callback: MediaSendCallback,
|
||||
fun MediaSendScreen(
|
||||
contractArgs: MediaSendActivityContract.Args,
|
||||
modifier: Modifier = Modifier,
|
||||
cameraSlot: @Composable () -> Unit = {},
|
||||
textStoryEditorSlot: @Composable () -> Unit = {},
|
||||
videoEditorSlot: @Composable () -> Unit = {},
|
||||
sendSlot: @Composable () -> Unit = {}
|
||||
sendSlot: @Composable (MediaSendState) -> Unit = {}
|
||||
) {
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
modifier = modifier.fillMaxSize()
|
||||
) { key ->
|
||||
when (key) {
|
||||
is MediaSendNavKey.Capture -> NavEntry(MediaSendNavKey.Capture.Chrome) {
|
||||
MediaCaptureScreen(
|
||||
val viewModel = viewModel<MediaSendViewModel>(factory = MediaSendViewModel.Factory(args = contractArgs))
|
||||
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val backStack = rememberNavBackStack(
|
||||
if (state.isCameraFirst) MediaSendNavKey.Capture.Camera else MediaSendNavKey.Select
|
||||
)
|
||||
|
||||
SignalTheme {
|
||||
CompositionLocalProvider(LocalNavigationEventDispatcherOwner provides LocalActivity.current as NavigationEventDispatcherOwner) {
|
||||
Surface {
|
||||
MediaSendNavDisplay(
|
||||
stateFlow = viewModel.state,
|
||||
backStack = backStack,
|
||||
callback = viewModel,
|
||||
modifier = modifier,
|
||||
cameraSlot = cameraSlot,
|
||||
textStoryEditorSlot = textStoryEditorSlot
|
||||
)
|
||||
}
|
||||
|
||||
MediaSendNavKey.Select -> NavEntry(key) {
|
||||
MediaSelectScreen(
|
||||
state = state,
|
||||
backStack = backStack,
|
||||
callback = callback
|
||||
)
|
||||
}
|
||||
|
||||
is MediaSendNavKey.Edit -> NavEntry(MediaSendNavKey.Edit) {
|
||||
MediaEditScreen(
|
||||
state = state,
|
||||
backStack = backStack,
|
||||
textStoryEditorSlot = textStoryEditorSlot,
|
||||
videoEditorSlot = videoEditorSlot,
|
||||
callback = callback
|
||||
sendSlot = sendSlot
|
||||
)
|
||||
}
|
||||
|
||||
is MediaSendNavKey.Send -> NavEntry(key) {
|
||||
sendSlot()
|
||||
}
|
||||
|
||||
else -> error("Unknown key: $key")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllDevicePreviews
|
||||
@Composable
|
||||
private fun MediaSendNavDisplayPreview() {
|
||||
Previews.Preview {
|
||||
CompositionLocalProvider(LocalNavigationEventDispatcherOwner provides rememberNavigationEventDispatcherOwner(parent = null)) {
|
||||
MediaSendNavDisplay(
|
||||
state = MediaSendState(isCameraFirst = true),
|
||||
backStack = rememberNavBackStack(MediaSendNavKey.Edit),
|
||||
callback = MediaSendCallback.Empty,
|
||||
cameraSlot = { BoxWithText("Camera Slot") },
|
||||
textStoryEditorSlot = { BoxWithText("Text Story Editor Slot") },
|
||||
videoEditorSlot = { BoxWithText("Video Editor Slot") },
|
||||
sendSlot = { BoxWithText("Send Slot") }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxWithText(text: String, modifier: Modifier = Modifier) {
|
||||
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(text = text)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,14 +48,18 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||
* [MediaSendState] is fully [Parcelable] and saved directly as a single key.
|
||||
*/
|
||||
class MediaSendViewModel(
|
||||
private val args: MediaSendActivityContract.Args,
|
||||
private val identityChangesSince: Long,
|
||||
isMeteredFlow: Flow<Boolean>,
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
private val repository: MediaSendRepository,
|
||||
private val preUploadController: PreUploadController
|
||||
private val preUploadController: PreUploadController,
|
||||
isMeteredFlow: Flow<Boolean>
|
||||
) : ViewModel(), MediaSendCallback {
|
||||
|
||||
private val args: MediaSendActivityContract.Args = savedStateHandle[KEY_ARGS]
|
||||
?: throw IllegalStateException("MediaSendViewModel requires args in SavedStateHandle. Use Factory to create.")
|
||||
|
||||
private val identityChangesSince: Long = savedStateHandle[KEY_IDENTITY_CHANGES_SINCE]
|
||||
?: throw IllegalStateException("MediaSendViewModel requires identityChangesSince in SavedStateHandle. Use Factory to create.")
|
||||
|
||||
private val defaultState = MediaSendState(
|
||||
isCameraFirst = args.isCameraFirst,
|
||||
recipientId = args.recipientId,
|
||||
@@ -138,7 +142,7 @@ class MediaSendViewModel(
|
||||
it.copy(
|
||||
mediaFolders = folders,
|
||||
selectedMediaFolder = if (it.selectedMediaFolder in folders) it.selectedMediaFolder else null,
|
||||
selectedMedia = if (it.selectedMediaFolder in folders) it.selectedMediaFolderItems else emptyList()
|
||||
selectedMediaFolderItems = if (it.selectedMediaFolder in folders) it.selectedMediaFolderItems else emptyList()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -568,8 +572,8 @@ class MediaSendViewModel(
|
||||
updateState { copy(message = text) }
|
||||
}
|
||||
|
||||
override fun onMessageChanged(text: CharSequence?) {
|
||||
setMessage(text?.toString())
|
||||
override fun onMessageChange(message: String) {
|
||||
setMessage(message)
|
||||
}
|
||||
|
||||
//endregion
|
||||
@@ -711,33 +715,43 @@ class MediaSendViewModel(
|
||||
|
||||
//endregion
|
||||
|
||||
//region Factory
|
||||
companion object {
|
||||
private const val KEY_ARGS = "media_send_vm_args"
|
||||
private const val KEY_IDENTITY_CHANGES_SINCE = "media_send_vm_identity_changes_since"
|
||||
private const val KEY_STATE = "media_send_vm_state"
|
||||
private const val KEY_EDITED_VIDEO_URIS = "media_send_vm_edited_video_uris"
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory that creates [MediaSendViewModel] from a [SavedStateHandle] and static dependencies.
|
||||
*
|
||||
* On first creation, [args] and [identityChangesSince] are written into the [SavedStateHandle].
|
||||
* On process death restoration, the [SavedStateHandle] already contains the persisted values
|
||||
* and the constructor parameters are ignored.
|
||||
*/
|
||||
class Factory(
|
||||
private val args: MediaSendActivityContract.Args,
|
||||
private val identityChangesSince: Long = System.currentTimeMillis(),
|
||||
private val isMeteredFlow: Flow<Boolean>
|
||||
private val repository: MediaSendRepository = MediaSendDependencies.mediaSendRepository,
|
||||
private val isMeteredFlow: Flow<Boolean> = MeteredConnectivity.isMetered(MediaSendDependencies.application)
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
|
||||
val savedStateHandle = extras.createSavedStateHandle()
|
||||
|
||||
if (!savedStateHandle.contains(KEY_ARGS)) {
|
||||
savedStateHandle[KEY_ARGS] = args
|
||||
}
|
||||
if (!savedStateHandle.contains(KEY_IDENTITY_CHANGES_SINCE)) {
|
||||
savedStateHandle[KEY_IDENTITY_CHANGES_SINCE] = identityChangesSince
|
||||
}
|
||||
|
||||
return MediaSendViewModel(
|
||||
args = args,
|
||||
identityChangesSince = identityChangesSince,
|
||||
isMeteredFlow = isMeteredFlow,
|
||||
savedStateHandle = savedStateHandle,
|
||||
repository = MediaSendDependencies.mediaSendRepository,
|
||||
preUploadController = PreUploadController()
|
||||
repository = repository,
|
||||
preUploadController = PreUploadController(),
|
||||
isMeteredFlow = isMeteredFlow
|
||||
) as T
|
||||
}
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
companion object {
|
||||
private const val KEY_STATE = "media_send_vm_state"
|
||||
private const val KEY_EDITED_VIDEO_URIS = "media_send_vm_edited_video_uris"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,14 @@ internal fun NavBackStack<NavKey>.goToEdit() {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun NavBackStack<NavKey>.goToSend() {
|
||||
if (contains(MediaSendNavKey.Send)) {
|
||||
popTo(MediaSendNavKey.Send)
|
||||
} else {
|
||||
add(MediaSendNavKey.Send)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun NavBackStack<NavKey>.pop() {
|
||||
if (isNotEmpty()) {
|
||||
removeAt(size - 1)
|
||||
|
||||
@@ -29,10 +29,12 @@ import org.signal.core.ui.compose.SignalIcons
|
||||
import org.signal.core.util.isNotNullOrBlank
|
||||
|
||||
@Composable
|
||||
internal fun AddAMessageRow(
|
||||
fun AddAMessageRow(
|
||||
message: String?,
|
||||
callback: AddAMessageRowCallback,
|
||||
modifier: Modifier = Modifier
|
||||
onNextClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onEmojiKeyboardClick: () -> Unit = {}
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
@@ -47,7 +49,7 @@ internal fun AddAMessageRow(
|
||||
.heightIn(min = 40.dp)
|
||||
) {
|
||||
IconButtons.IconButton(
|
||||
onClick = callback::onEmojiKeyboardClick
|
||||
onClick = onEmojiKeyboardClick
|
||||
) {
|
||||
Icon(
|
||||
painter = SignalIcons.Emoji.painter,
|
||||
@@ -74,7 +76,7 @@ internal fun AddAMessageRow(
|
||||
}
|
||||
|
||||
IconButtons.IconButton(
|
||||
onClick = callback::onNextClick,
|
||||
onClick = onNextClick,
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp)
|
||||
.background(
|
||||
@@ -99,19 +101,16 @@ private fun AddAMessageRowPreview() {
|
||||
Previews.Preview {
|
||||
AddAMessageRow(
|
||||
message = null,
|
||||
callback = AddAMessageRowCallback.Empty
|
||||
callback = AddAMessageRowCallback.Empty,
|
||||
onNextClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal interface AddAMessageRowCallback {
|
||||
interface AddAMessageRowCallback {
|
||||
fun onMessageChange(message: String)
|
||||
fun onEmojiKeyboardClick()
|
||||
fun onNextClick()
|
||||
|
||||
object Empty : AddAMessageRowCallback {
|
||||
override fun onMessageChange(message: String) = Unit
|
||||
override fun onEmojiKeyboardClick() = Unit
|
||||
override fun onNextClick() = Unit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.signal.core.util.ContentTypeUtil
|
||||
import org.signal.mediasend.EditorState
|
||||
import org.signal.mediasend.MediaSendNavKey
|
||||
import org.signal.mediasend.MediaSendState
|
||||
import org.signal.mediasend.goToSend
|
||||
|
||||
@Composable
|
||||
fun MediaEditScreen(
|
||||
@@ -111,7 +112,8 @@ fun MediaEditScreen(
|
||||
|
||||
AddAMessageRow(
|
||||
message = state.message,
|
||||
callback = AddAMessageRowCallback.Empty,
|
||||
callback = callback,
|
||||
onNextClick = { backStack.goToSend() },
|
||||
modifier = Modifier
|
||||
.widthIn(max = 624.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
@@ -145,10 +147,10 @@ private fun MediaEditScreenPreview() {
|
||||
}
|
||||
}
|
||||
|
||||
interface MediaEditScreenCallback {
|
||||
interface MediaEditScreenCallback : AddAMessageRowCallback {
|
||||
fun setFocusedMedia(media: Media)
|
||||
|
||||
object Empty : MediaEditScreenCallback {
|
||||
object Empty : MediaEditScreenCallback, AddAMessageRowCallback by AddAMessageRowCallback.Empty {
|
||||
override fun setFocusedMedia(media: Media) = Unit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
@@ -99,7 +100,9 @@ internal fun MediaSelectScreen(
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier.padding(paddingValues)
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
LazyVerticalGrid(
|
||||
columns = gridConfiguration.gridCells,
|
||||
|
||||
Reference in New Issue
Block a user