Media: align attachement source picker design with Figma

This commit is contained in:
ganfra 2023-05-22 21:24:43 +02:00
parent 2c3fb8a549
commit 7c7f167692
7 changed files with 48 additions and 76 deletions

View file

@ -18,7 +18,6 @@ package io.element.android.features.messages.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.actionlist.anActionListState
import io.element.android.features.messages.impl.messagecomposer.AttachmentSourcePicker
import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
import io.element.android.features.messages.impl.timeline.aTimelineItemContent import io.element.android.features.messages.impl.timeline.aTimelineItemContent
import io.element.android.features.messages.impl.timeline.aTimelineItemList import io.element.android.features.messages.impl.timeline.aTimelineItemList
@ -33,8 +32,7 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
get() = sequenceOf( get() = sequenceOf(
aMessagesState(), aMessagesState(),
aMessagesState().copy(hasNetworkConnection = false), aMessagesState().copy(hasNetworkConnection = false),
aMessagesState().copy(composerState = aMessageComposerState().copy(attachmentSourcePicker = AttachmentSourcePicker.AllMedia)), aMessagesState().copy(composerState = aMessageComposerState().copy(showAttachmentSourcePicker = true)),
aMessagesState().copy(composerState = aMessageComposerState().copy(attachmentSourcePicker = AttachmentSourcePicker.Camera)),
) )
} }

View file

@ -37,6 +37,11 @@ import androidx.compose.material.ListItem
import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.AttachFile
import androidx.compose.material.icons.filled.Collections
import androidx.compose.material.icons.filled.LocalLibrary
import androidx.compose.material.icons.filled.PhotoCamera
import androidx.compose.material.icons.filled.Videocam
import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
@ -61,7 +66,6 @@ import io.element.android.features.messages.impl.actionlist.ActionListEvents
import io.element.android.features.messages.impl.actionlist.ActionListView import io.element.android.features.messages.impl.actionlist.ActionListView
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.messagecomposer.AttachmentSourcePicker
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
import io.element.android.features.messages.impl.messagecomposer.MessageComposerView import io.element.android.features.messages.impl.messagecomposer.MessageComposerView
@ -102,7 +106,7 @@ fun MessagesView(
initialValue = ModalBottomSheetValue.Hidden, initialValue = ModalBottomSheetValue.Hidden,
) )
val composerState = state.composerState val composerState = state.composerState
val initialBottomSheetState = if (LocalInspectionMode.current && composerState.attachmentSourcePicker != null) { val initialBottomSheetState = if (LocalInspectionMode.current && composerState.showAttachmentSourcePicker != null) {
ModalBottomSheetValue.Expanded ModalBottomSheetValue.Expanded
} else { } else {
ModalBottomSheetValue.Hidden ModalBottomSheetValue.Hidden
@ -154,8 +158,8 @@ fun MessagesView(
state.eventSink(MessagesEvents.HandleAction(action, event)) state.eventSink(MessagesEvents.HandleAction(action, event))
} }
LaunchedEffect(composerState.attachmentSourcePicker) { LaunchedEffect(composerState.showAttachmentSourcePicker) {
if (composerState.attachmentSourcePicker != null) { if (composerState.showAttachmentSourcePicker) {
// We need to use this instead of `LocalFocusManager.clearFocus()` to hide the keyboard when focus is on an Android View // We need to use this instead of `LocalFocusManager.clearFocus()` to hide the keyboard when focus is on an Android View
localView.hideKeyboard() localView.hideKeyboard()
bottomSheetState.show() bottomSheetState.show()
@ -173,8 +177,7 @@ fun MessagesView(
sheetState = bottomSheetState, sheetState = bottomSheetState,
displayHandle = true, displayHandle = true,
sheetContent = { sheetContent = {
MediaPickerMenu( AttachmentSourcePickerMenu(
addAttachmentSourcePicker = composerState.attachmentSourcePicker,
eventSink = composerState.eventSink eventSink = composerState.eventSink
) )
} }
@ -305,50 +308,33 @@ fun MessagesViewTopBar(
) )
} }
@Composable
internal fun MediaPickerMenu(
addAttachmentSourcePicker: AttachmentSourcePicker?,
eventSink: (MessageComposerEvents) -> Unit,
) {
when (addAttachmentSourcePicker) {
null -> return
AttachmentSourcePicker.AllMedia -> AllMediaSourcePickerMenu(eventSink = eventSink)
AttachmentSourcePicker.Camera -> CameraSourcePickerMenu(eventSink = eventSink)
}
}
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
internal fun AllMediaSourcePickerMenu( internal fun AttachmentSourcePickerMenu(
eventSink: (MessageComposerEvents) -> Unit, eventSink: (MessageComposerEvents) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column(modifier) { Column(modifier) {
ListItem(Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery) }) { ListItem(
Text(stringResource(R.string.screen_room_attachment_source_gallery)) modifier = Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery) },
} icon = { Icon(Icons.Default.Collections, null) },
ListItem(Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles) }) { text = { Text(stringResource(R.string.screen_room_attachment_source_gallery)) },
Text(stringResource(R.string.screen_room_attachment_source_files)) )
} ListItem(
ListItem(Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.FromCamera) }) { modifier = Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles) },
Text(stringResource(R.string.screen_room_attachment_source_camera)) icon = { Icon(Icons.Default.AttachFile, null) },
} text = { Text(stringResource(R.string.screen_room_attachment_source_files)) },
} )
} ListItem(
modifier = Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.PhotoFromCamera) },
@OptIn(ExperimentalMaterialApi::class) icon = { Icon(Icons.Default.PhotoCamera, null) },
@Composable text = { Text(stringResource(R.string.screen_room_attachment_source_camera_photo)) },
internal fun CameraSourcePickerMenu( )
eventSink: (MessageComposerEvents) -> Unit, ListItem(
modifier: Modifier = Modifier, modifier = Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.VideoFromCamera) },
) { icon = { Icon(Icons.Default.Videocam, null) },
Column(modifier) { text = { Text(stringResource(R.string.screen_room_attachment_source_camera_video)) },
ListItem(Modifier.clickable { eventSink(MessageComposerEvents.PickCameraAttachmentSource.Photo) }) { )
Text(stringResource(R.string.screen_room_attachment_source_camera_photo))
}
ListItem(Modifier.clickable { eventSink(MessageComposerEvents.PickCameraAttachmentSource.Video) }) {
Text(stringResource(R.string.screen_room_attachment_source_camera_video))
}
} }
} }

View file

@ -30,11 +30,8 @@ sealed interface MessageComposerEvents {
object DismissAttachmentMenu : MessageComposerEvents object DismissAttachmentMenu : MessageComposerEvents
sealed interface PickAttachmentSource : MessageComposerEvents { sealed interface PickAttachmentSource : MessageComposerEvents {
object FromGallery : PickAttachmentSource object FromGallery : PickAttachmentSource
object FromCamera : PickAttachmentSource
object FromFiles : PickAttachmentSource object FromFiles : PickAttachmentSource
} object PhotoFromCamera : PickAttachmentSource
sealed interface PickCameraAttachmentSource : MessageComposerEvents { object VideoFromCamera : PickAttachmentSource
object Photo : PickCameraAttachmentSource
object Video : PickCameraAttachmentSource
} }
} }

View file

@ -107,7 +107,7 @@ class MessageComposerPresenter @Inject constructor(
mutableStateOf(MessageComposerMode.Normal("")) mutableStateOf(MessageComposerMode.Normal(""))
} }
var attachmentSourcePicker: AttachmentSourcePicker? by remember { mutableStateOf(null) } var showAttachmentSourcePicker: Boolean by remember { mutableStateOf(false) }
LaunchedEffect(composerMode.value) { LaunchedEffect(composerMode.value) {
when (val modeValue = composerMode.value) { when (val modeValue = composerMode.value) {
@ -135,26 +135,23 @@ class MessageComposerPresenter @Inject constructor(
is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage(event.message, composerMode, text) is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage(event.message, composerMode, text)
is MessageComposerEvents.SetMode -> composerMode.value = event.composerMode is MessageComposerEvents.SetMode -> composerMode.value = event.composerMode
MessageComposerEvents.AddAttachment -> localCoroutineScope.ifMediaPickersEnabled { MessageComposerEvents.AddAttachment -> localCoroutineScope.ifMediaPickersEnabled {
attachmentSourcePicker = AttachmentSourcePicker.AllMedia showAttachmentSourcePicker = true
} }
MessageComposerEvents.DismissAttachmentMenu -> attachmentSourcePicker = null MessageComposerEvents.DismissAttachmentMenu -> showAttachmentSourcePicker = false
MessageComposerEvents.PickAttachmentSource.FromGallery -> localCoroutineScope.ifMediaPickersEnabled { MessageComposerEvents.PickAttachmentSource.FromGallery -> localCoroutineScope.ifMediaPickersEnabled {
attachmentSourcePicker = null showAttachmentSourcePicker = false
galleryMediaPicker.launch() galleryMediaPicker.launch()
} }
MessageComposerEvents.PickAttachmentSource.FromFiles -> localCoroutineScope.ifMediaPickersEnabled { MessageComposerEvents.PickAttachmentSource.FromFiles -> localCoroutineScope.ifMediaPickersEnabled {
attachmentSourcePicker = null showAttachmentSourcePicker = false
filesPicker.launch() filesPicker.launch()
} }
MessageComposerEvents.PickAttachmentSource.FromCamera -> localCoroutineScope.ifMediaPickersEnabled { MessageComposerEvents.PickAttachmentSource.PhotoFromCamera -> localCoroutineScope.ifMediaPickersEnabled {
attachmentSourcePicker = AttachmentSourcePicker.Camera showAttachmentSourcePicker = false
}
MessageComposerEvents.PickCameraAttachmentSource.Photo -> localCoroutineScope.ifMediaPickersEnabled {
attachmentSourcePicker = null
cameraPhotoPicker.launch() cameraPhotoPicker.launch()
} }
MessageComposerEvents.PickCameraAttachmentSource.Video -> localCoroutineScope.ifMediaPickersEnabled { MessageComposerEvents.PickAttachmentSource.VideoFromCamera -> localCoroutineScope.ifMediaPickersEnabled {
attachmentSourcePicker = null showAttachmentSourcePicker = false
cameraVideoPicker.launch() cameraVideoPicker.launch()
} }
} }
@ -164,7 +161,7 @@ class MessageComposerPresenter @Inject constructor(
text = text.value, text = text.value,
isFullScreen = isFullScreen.value, isFullScreen = isFullScreen.value,
mode = composerMode.value, mode = composerMode.value,
attachmentSourcePicker = attachmentSourcePicker, showAttachmentSourcePicker = showAttachmentSourcePicker,
attachmentsState = attachmentsState.value, attachmentsState = attachmentsState.value,
eventSink = ::handleEvents eventSink = ::handleEvents
) )

View file

@ -27,7 +27,7 @@ data class MessageComposerState(
val text: StableCharSequence?, val text: StableCharSequence?,
val isFullScreen: Boolean, val isFullScreen: Boolean,
val mode: MessageComposerMode, val mode: MessageComposerMode,
val attachmentSourcePicker: AttachmentSourcePicker?, val showAttachmentSourcePicker: Boolean,
val attachmentsState: AttachmentsState, val attachmentsState: AttachmentsState,
val eventSink: (MessageComposerEvents) -> Unit val eventSink: (MessageComposerEvents) -> Unit
) { ) {
@ -40,8 +40,3 @@ sealed interface AttachmentsState {
data class Previewing(val attachments: ImmutableList<Attachment>) : AttachmentsState data class Previewing(val attachments: ImmutableList<Attachment>) : AttachmentsState
data class Sending(val attachments: ImmutableList<Attachment>) : AttachmentsState data class Sending(val attachments: ImmutableList<Attachment>) : AttachmentsState
} }
sealed interface AttachmentSourcePicker {
object AllMedia : AttachmentSourcePicker
object Camera : AttachmentSourcePicker
}

View file

@ -31,7 +31,7 @@ fun aMessageComposerState() = MessageComposerState(
text = StableCharSequence(""), text = StableCharSequence(""),
isFullScreen = false, isFullScreen = false,
mode = MessageComposerMode.Normal(content = ""), mode = MessageComposerMode.Normal(content = ""),
attachmentSourcePicker = null, showAttachmentSourcePicker = false,
attachmentsState = AttachmentsState.None, attachmentsState = AttachmentsState.None,
eventSink = {} eventSink = {}
) )

View file

@ -44,7 +44,6 @@ import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediapickers.api.PickerProvider
import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediapickers.test.FakePickerProvider
import io.element.android.libraries.mediaupload.api.MediaPreProcessor import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.mediaupload.api.MediaUploadInfo import io.element.android.libraries.mediaupload.api.MediaUploadInfo
import io.element.android.libraries.mediaupload.api.ThumbnailProcessingInfo import io.element.android.libraries.mediaupload.api.ThumbnailProcessingInfo
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
@ -259,7 +258,7 @@ class MessageComposerPresenterTest {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(MessageComposerEvents.AddAttachment) initialState.eventSink(MessageComposerEvents.AddAttachment)
assertThat(awaitItem().attachmentSourcePicker).isEqualTo(AttachmentSourcePicker.AllMedia) assertThat(awaitItem().showAttachmentSourcePicker).isEqualTo(AttachmentSourcePicker.AllMedia)
} }
} }
@ -272,7 +271,7 @@ class MessageComposerPresenterTest {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromCamera) initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromCamera)
assertThat(awaitItem().attachmentSourcePicker).isEqualTo(AttachmentSourcePicker.Camera) assertThat(awaitItem().showAttachmentSourcePicker).isEqualTo(AttachmentSourcePicker.Camera)
} }
} }
@ -287,7 +286,7 @@ class MessageComposerPresenterTest {
skipItems(1) skipItems(1)
initialState.eventSink(MessageComposerEvents.DismissAttachmentMenu) initialState.eventSink(MessageComposerEvents.DismissAttachmentMenu)
assertThat(awaitItem().attachmentSourcePicker).isNull() assertThat(awaitItem().showAttachmentSourcePicker).isNull()
} }
} }