[Media upload] Add media pickers to the Room screen and the composer (#380)

* Add media pickers to the Room screen and the composer.

* Fix exclude rules for translations
This commit is contained in:
Jorge Martin Espinosa
2023-05-04 11:51:03 +02:00
committed by GitHub
parent 5c935818c6
commit 31ac97d17a
40 changed files with 438 additions and 114 deletions

1
changelog.d/360.feature Normal file
View File

@@ -0,0 +1 @@
Add media pickers to the room screen.

View File

@@ -10,9 +10,9 @@
<string name="screen_create_room_public_option_title">"Public room (anyone)"</string>
<string name="screen_create_room_room_name_label">"Room name"</string>
<string name="screen_create_room_room_name_placeholder">"e.g. Product Sprint"</string>
<string name="screen_create_room_title">"Create a room"</string>
<string name="screen_create_room_topic_label">"Topic (optional)"</string>
<string name="screen_create_room_topic_placeholder">"What is this room about?"</string>
<string name="screen_start_chat_error_starting_chat">"An error occurred when trying to start a chat"</string>
<string name="screen_start_chat_unknown_profile">"We cant validate this users Matrix ID. The invite might not be received."</string>
<string name="screen_create_room_title">"Create a room"</string>
</resources>

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Are you sure you want to sign out?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Sign out"</string>
<string name="screen_signout_confirmation_dialog_title">"Sign out"</string>
<string name="screen_signout_in_progress_dialog_content">"Signing out…"</string>
<string name="screen_signout_confirmation_dialog_submit">"Sign out"</string>
<string name="screen_signout_preference_item">"Sign out"</string>
</resources>

View File

@@ -32,6 +32,7 @@ dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
api(projects.features.messages.api)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)

View File

@@ -27,7 +27,6 @@ import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.messages.api.MessagesEntryPoint
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.RoomId
@ContributesNode(RoomScope::class)
class MessagesNode @AssistedInject constructor(

View File

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

View File

@@ -21,6 +21,7 @@
package io.element.android.features.messages.impl
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -35,6 +36,7 @@ import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ListItem
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
@@ -43,11 +45,14 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
@@ -57,20 +62,24 @@ import androidx.compose.ui.unit.sp
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.model.TimelineItemAction
import io.element.android.features.messages.impl.textcomposer.AttachmentSourcePicker
import io.element.android.features.messages.impl.textcomposer.MessageComposerEvents
import io.element.android.features.messages.impl.textcomposer.MessageComposerView
import io.element.android.features.messages.impl.timeline.TimelineView
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
import io.element.android.libraries.androidutils.ui.hideKeyboard
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheetLayout
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.LogCompositions
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
import kotlinx.coroutines.launch
import timber.log.Timber
@@ -86,9 +95,24 @@ fun MessagesView(
initialValue = ModalBottomSheetValue.Hidden,
)
val snackbarHostState = remember { SnackbarHostState() }
val focusManager = LocalFocusManager.current
val composerState = state.composerState
val initialBottomSheetState = if (LocalInspectionMode.current && composerState.attachmentSourcePicker != null) {
ModalBottomSheetValue.Expanded
} else {
ModalBottomSheetValue.Hidden
}
val bottomSheetState = rememberModalBottomSheetState(initialValue = initialBottomSheetState)
val coroutineScope = rememberCoroutineScope()
BackHandler(enabled = bottomSheetState.isVisible) {
coroutineScope.launch {
bottomSheetState.hide()
}
}
// This is needed because the composer is inside an AndroidView that can't be affected by the FocusManager in Compose
val localView = LocalView.current
LogCompositions(tag = "MessagesScreen", msg = "Content")
fun onMessageClicked(event: TimelineItem.Event) {
@@ -97,7 +121,7 @@ fun MessagesView(
fun onMessageLongClicked(event: TimelineItem.Event) {
Timber.v("OnMessageLongClicked= ${event.id}")
focusManager.clearFocus(force = true)
localView.hideKeyboard()
state.actionListState.eventSink(ActionListEvents.ComputeForMessage(event))
coroutineScope.launch {
itemActionsBottomSheetState.show()
@@ -108,41 +132,67 @@ fun MessagesView(
state.eventSink(MessagesEvents.HandleAction(action, event))
}
Scaffold(
modifier = modifier,
contentWindowInsets = WindowInsets.statusBars,
topBar = {
Column {
ConnectivityIndicatorView(isOnline = state.hasNetworkConnection)
MessagesViewTopBar(
roomTitle = state.roomName,
roomAvatar = state.roomAvatar,
onBackPressed = onBackPressed,
onRoomDetailsClicked = onRoomDetailsClicked,
LaunchedEffect(composerState.attachmentSourcePicker) {
if (composerState.attachmentSourcePicker != null) {
// We need to use this instead of `LocalFocusManager.clearFocus()` to hide the keyboard when focus is on an Android View
localView.hideKeyboard()
bottomSheetState.show()
} else {
bottomSheetState.hide()
}
}
// Send 'DismissAttachmentMenu' event when the bottomsheet was just hidden
LaunchedEffect(bottomSheetState.isVisible) {
if (!bottomSheetState.isVisible) {
composerState.eventSink(MessageComposerEvents.DismissAttachmentMenu)
}
}
ModalBottomSheetLayout(
sheetState = bottomSheetState,
displayHandle = true,
sheetContent = {
MediaPickerMenu(
addAttachmentSourcePicker = composerState.attachmentSourcePicker,
eventSink = composerState.eventSink
)
}
) {
Scaffold(
modifier = modifier,
contentWindowInsets = WindowInsets.statusBars,
topBar = {
Column {
ConnectivityIndicatorView(isOnline = state.hasNetworkConnection)
MessagesViewTopBar(
roomTitle = state.roomName,
roomAvatar = state.roomAvatar,
onBackPressed = onBackPressed,
onRoomDetailsClicked = onRoomDetailsClicked,
)
}
},
content = { padding ->
MessagesViewContent(
state = state,
modifier = Modifier.padding(padding),
onMessageClicked = ::onMessageClicked,
onMessageLongClicked = ::onMessageLongClicked
)
}
},
content = { padding ->
MessagesViewContent(
state = state,
modifier = Modifier.padding(padding),
onMessageClicked = ::onMessageClicked,
onMessageLongClicked = ::onMessageLongClicked
)
},
snackbarHost = {
SnackbarHost(
snackbarHostState,
modifier = Modifier.navigationBarsPadding()
)
},
)
},
snackbarHost = {
SnackbarHost(
snackbarHostState,
modifier = Modifier.navigationBarsPadding()
)
},
)
ActionListView(
state = state.actionListState,
modalBottomSheetState = itemActionsBottomSheetState,
onActionSelected = ::onActionSelected
)
ActionListView(
state = state.actionListState,
modalBottomSheetState = itemActionsBottomSheetState,
onActionSelected = ::onActionSelected
)
}
}
@Composable
@@ -216,6 +266,53 @@ 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)
@Composable
internal fun AllMediaSourcePickerMenu(
eventSink: (MessageComposerEvents) -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier) {
ListItem(Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery) }) {
Text(stringResource(R.string.screen_room_attachment_source_gallery))
}
ListItem(Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles) }) {
Text(stringResource(R.string.screen_room_attachment_source_files))
}
ListItem(Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.FromCamera) }) {
Text(stringResource(R.string.screen_room_attachment_source_camera))
}
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
internal fun CameraSourcePickerMenu(
eventSink: (MessageComposerEvents) -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier) {
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))
}
}
}
@Preview
@Composable
internal fun MessagesViewLightPreview(@PreviewParameter(MessagesStateProvider::class) state: MessagesState) =

View File

@@ -24,6 +24,15 @@ sealed interface MessageComposerEvents {
object CloseSpecialMode : MessageComposerEvents
data class SetMode(val composerMode: MessageComposerMode) : MessageComposerEvents
data class UpdateText(val text: CharSequence) : MessageComposerEvents
object TakePhoto : MessageComposerEvents
object AddAttachment : MessageComposerEvents
object DismissAttachmentMenu : MessageComposerEvents
sealed interface PickAttachmentSource : MessageComposerEvents {
object FromGallery : PickAttachmentSource
object FromCamera : PickAttachmentSource
object FromFiles : PickAttachmentSource
}
sealed interface PickCameraAttachmentSource : MessageComposerEvents {
object Photo : PickCameraAttachmentSource
object Video : PickCameraAttachmentSource
}
}

View File

@@ -19,13 +19,17 @@ package io.element.android.features.messages.impl.textcomposer
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
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.di.RoomScope
import io.element.android.libraries.di.SingleIn
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
@@ -36,6 +40,7 @@ import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@SingleIn(RoomScope::class)
class MessageComposerPresenter @Inject constructor(
private val appCoroutineScope: CoroutineScope,
private val room: MatrixRoom,
@@ -47,11 +52,22 @@ class MessageComposerPresenter @Inject constructor(
override fun present(): MessageComposerState {
val localCoroutineScope = rememberCoroutineScope()
// Example usage of custom pickers
val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker(onResult = { uri ->
Timber.d("Media picked from $uri")
})
val filesPicker = mediaPickerProvider.registerFilePicker(onResult = { uri ->
Timber.d("File picked from $uri")
})
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(onResult = { uri ->
Timber.d("Photo saved at $uri")
})
val cameraVideoPicker = mediaPickerProvider.registerCameraVideoPicker(onResult = { uri ->
Timber.d("Video saved at $uri")
})
val isFullScreen = rememberSaveable {
mutableStateOf(false)
}
@@ -62,6 +78,8 @@ class MessageComposerPresenter @Inject constructor(
mutableStateOf(MessageComposerMode.Normal(""))
}
var attachmentSourcePicker: AttachmentSourcePicker? by remember { mutableStateOf(null) }
LaunchedEffect(composerMode.value) {
when (val modeValue = composerMode.value) {
is MessageComposerMode.Edit -> text.value = modeValue.defaultContent.toStableCharSequence()
@@ -80,21 +98,47 @@ class MessageComposerPresenter @Inject constructor(
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()
}
}}
MessageComposerEvents.AddAttachment -> localCoroutineScope.ifMediaPickersEnabled {
attachmentSourcePicker = AttachmentSourcePicker.AllMedia
}
MessageComposerEvents.DismissAttachmentMenu -> attachmentSourcePicker = null
MessageComposerEvents.PickAttachmentSource.FromGallery -> localCoroutineScope.ifMediaPickersEnabled {
attachmentSourcePicker = null
galleryMediaPicker.launch()
}
MessageComposerEvents.PickAttachmentSource.FromFiles -> localCoroutineScope.ifMediaPickersEnabled {
attachmentSourcePicker = null
filesPicker.launch()
}
MessageComposerEvents.PickAttachmentSource.FromCamera -> localCoroutineScope.ifMediaPickersEnabled {
attachmentSourcePicker = AttachmentSourcePicker.Camera
}
MessageComposerEvents.PickCameraAttachmentSource.Photo -> localCoroutineScope.ifMediaPickersEnabled {
attachmentSourcePicker = null
cameraPhotoPicker.launch()
}
MessageComposerEvents.PickCameraAttachmentSource.Video -> localCoroutineScope.ifMediaPickersEnabled {
attachmentSourcePicker = null
cameraVideoPicker.launch()
}
}
}
return MessageComposerState(
text = text.value,
isFullScreen = isFullScreen.value,
mode = composerMode.value,
attachmentSourcePicker = attachmentSourcePicker,
eventSink = ::handleEvents
)
}
private fun CoroutineScope.ifMediaPickersEnabled(action: suspend () -> Unit) = launch {
if (featureFlagService.isFeatureEnabled(FeatureFlags.ShowMediaUploadingFlow)) {
action()
}
}
private fun MutableState<MessageComposerMode>.setToNormal() {
value = MessageComposerMode.Normal("")
}

View File

@@ -25,7 +25,13 @@ data class MessageComposerState(
val text: StableCharSequence?,
val isFullScreen: Boolean,
val mode: MessageComposerMode,
val attachmentSourcePicker: AttachmentSourcePicker?,
val eventSink: (MessageComposerEvents) -> Unit
) {
val isSendButtonVisible: Boolean = text?.charSequence.isNullOrEmpty().not()
}
sealed interface AttachmentSourcePicker {
object AllMedia : AttachmentSourcePicker
object Camera : AttachmentSourcePicker
}

View File

@@ -31,5 +31,6 @@ fun aMessageComposerState() = MessageComposerState(
text = StableCharSequence(""),
isFullScreen = false,
mode = MessageComposerMode.Normal(content = ""),
attachmentSourcePicker = null,
eventSink = {}
)

View File

@@ -54,7 +54,7 @@ fun MessageComposerView(
onCloseSpecialMode = ::onCloseSpecialMode,
onComposerTextChange = ::onComposerTextChange,
onAddAttachment = {
state.eventSink(MessageComposerEvents.TakePhoto)
state.eventSink(MessageComposerEvents.AddAttachment)
},
composerCanSendMessage = state.isSendButtonVisible,
composerText = state.text?.charSequence?.toString(),

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_room_attachment_source_camera">"Camera"</string>
<string name="screen_room_attachment_source_camera_photo">"Take photo"</string>
<string name="screen_room_attachment_source_camera_video">"Record a video"</string>
<string name="screen_room_attachment_source_files">"Attachment"</string>
<string name="screen_room_attachment_source_gallery">"Photo &amp; Video Library"</string>
<string name="screen_room_error_failed_processing_media">"Failed processing media to upload, please try again."</string>
</resources>

View File

@@ -22,7 +22,9 @@ import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test
import com.google.common.truth.Truth
import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.textcomposer.AttachmentSourcePicker
import io.element.android.features.messages.impl.textcomposer.MessageComposerEvents
import io.element.android.features.messages.impl.textcomposer.MessageComposerPresenter
import io.element.android.features.messages.impl.textcomposer.MessageComposerState
@@ -279,6 +281,103 @@ class MessageComposerPresenterTest {
}
}
@Test
fun `present - Open attachments menu`() = runTest {
val fakeMatrixRoom = FakeMatrixRoom()
val presenter = MessageComposerPresenter(
this,
fakeMatrixRoom,
pickerProvider,
featureFlagService,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(MessageComposerEvents.AddAttachment)
assertThat(awaitItem().attachmentSourcePicker).isEqualTo(AttachmentSourcePicker.AllMedia)
}
}
@Test
fun `present - Open camera attachments menu`() = runTest {
val fakeMatrixRoom = FakeMatrixRoom()
val presenter = MessageComposerPresenter(
this,
fakeMatrixRoom,
pickerProvider,
featureFlagService,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromCamera)
assertThat(awaitItem().attachmentSourcePicker).isEqualTo(AttachmentSourcePicker.Camera)
}
}
@Test
fun `present - Dismiss attachments menu`() = runTest {
val fakeMatrixRoom = FakeMatrixRoom()
val presenter = MessageComposerPresenter(
this,
fakeMatrixRoom,
pickerProvider,
featureFlagService,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(MessageComposerEvents.AddAttachment)
skipItems(1)
initialState.eventSink(MessageComposerEvents.DismissAttachmentMenu)
assertThat(awaitItem().attachmentSourcePicker).isNull()
}
}
@Test
fun `present - Pick media from gallery`() = runTest {
val fakeMatrixRoom = FakeMatrixRoom()
val presenter = MessageComposerPresenter(
this,
fakeMatrixRoom,
pickerProvider,
featureFlagService,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery)
// TODO verify some post processing of the selected media is done
}
}
@Test
fun `present - Pick file from storage`() = runTest {
val fakeMatrixRoom = FakeMatrixRoom()
val presenter = MessageComposerPresenter(
this,
fakeMatrixRoom,
pickerProvider,
featureFlagService,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles)
// TODO verify some post processing of the selected media is done
}
}
@Test
fun `present - Take photo`() = runTest {
val fakeMatrixRoom = FakeMatrixRoom()
@@ -292,11 +391,30 @@ class MessageComposerPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(MessageComposerEvents.TakePhoto)
initialState.eventSink(MessageComposerEvents.PickCameraAttachmentSource.Photo)
// TODO verify some post processing of the captured image is done
}
}
@Test
fun `present - Record video`() = runTest {
val fakeMatrixRoom = FakeMatrixRoom()
val presenter = MessageComposerPresenter(
this,
fakeMatrixRoom,
pickerProvider,
featureFlagService,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(MessageComposerEvents.PickCameraAttachmentSource.Video)
// TODO verify some post processing of the captured video is done
}
}
}
fun anEditMode() = MessageComposerMode.Edit(AN_EVENT_ID, A_MESSAGE)

View File

@@ -6,7 +6,6 @@
</plurals>
<string name="screen_room_details_encryption_enabled_subtitle">"Messages are secured with locks. Only you and the recipients have the unique keys to unlock them."</string>
<string name="screen_room_details_encryption_enabled_title">"Message encryption enabled"</string>
<string name="screen_room_details_invite_people_title">"Invite people"</string>
<string name="screen_room_details_share_room_title">"Share room"</string>
<string name="screen_dm_details_block_alert_action">"Block"</string>
<string name="screen_dm_details_block_alert_description">"Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime."</string>
@@ -14,6 +13,7 @@
<string name="screen_dm_details_unblock_alert_action">"Unblock"</string>
<string name="screen_dm_details_unblock_alert_description">"On unblocking the user, you will be able to see all messages by them again."</string>
<string name="screen_dm_details_unblock_user">"Unblock user"</string>
<string name="screen_room_details_invite_people_title">"Invite people"</string>
<string name="screen_room_details_leave_room_title">"Leave room"</string>
<string name="screen_room_details_people_title">"People"</string>
<string name="screen_room_details_security_title">"Security"</string>

View File

@@ -19,9 +19,17 @@
package io.element.android.libraries.designsystem.theme.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetDefaults
import androidx.compose.material.ModalBottomSheetState
@@ -30,12 +38,14 @@ import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.modifiers.applyIf
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.PreviewGroup
@@ -46,15 +56,36 @@ fun ModalBottomSheetLayout(
sheetContent: @Composable ColumnScope.() -> Unit,
modifier: Modifier = Modifier,
sheetState: ModalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden),
sheetShape: Shape = MaterialTheme.shapes.large,
sheetShape: Shape = MaterialTheme.shapes.large.copy(bottomStart = CornerSize(0.dp), bottomEnd = CornerSize(0.dp)),
sheetElevation: Dp = ModalBottomSheetDefaults.Elevation,
sheetBackgroundColor: Color = MaterialTheme.colorScheme.surface,
sheetContentColor: Color = contentColorFor(sheetBackgroundColor),
scrimColor: Color = ModalBottomSheetDefaults.scrimColor,
displayHandle: Boolean = false,
useSystemPadding: Boolean = true,
content: @Composable () -> Unit = {}
) {
androidx.compose.material.ModalBottomSheetLayout(
sheetContent = sheetContent,
sheetContent = {
Column(
Modifier.fillMaxWidth()
.applyIf(useSystemPadding, ifTrue = {
navigationBarsPadding()
})
) {
if (displayHandle) {
Spacer(modifier = Modifier.height(16.dp))
Box(
modifier = Modifier
.background(MaterialTheme.colorScheme.onSurfaceVariant, RoundedCornerShape(2.dp))
.size(width = 32.dp, height = 4.dp)
.align(Alignment.CenterHorizontally),
)
Spacer(modifier = Modifier.height(24.dp))
}
sheetContent()
}
},
modifier = modifier,
sheetState = sheetState,
sheetShape = sheetShape,
@@ -79,10 +110,13 @@ internal fun ModalBottomSheetLayoutDarkPreview() =
@Composable
private fun ContentToPreview() {
ModalBottomSheetLayout(
modifier = Modifier.height(100.dp),
modifier = Modifier.height(140.dp),
displayHandle = true,
sheetState = ModalBottomSheetState(ModalBottomSheetValue.Expanded),
sheetContent = {
Text(text = "Sheet Content", modifier = Modifier.padding(16.dp).background(color = Color.Green))
Text(text = "Sheet Content", modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 20.dp)
.background(color = Color.Green))
}
) {
Text(text = "Content", modifier = Modifier.background(color = Color.Red))

View File

@@ -25,6 +25,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@@ -55,8 +57,9 @@ fun TextComposer(
if (LocalInspectionMode.current) {
FakeComposer(modifier)
} else {
val focusRequester = FocusRequester()
AndroidView(
modifier = modifier,
modifier = modifier.focusRequester(focusRequester),
factory = { context ->
RichTextComposerLayout(context).apply {
// Sets up listeners for View -> Compose communication

View File

@@ -95,14 +95,10 @@
<string name="room_timeline_read_marker_title">"Neu"</string>
<string name="screen_analytics_settings_share_data">"Teile Analyse-Daten"</string>
<string name="screen_media_picker_error_failed_selection">"Medienauswahl fehlgeschlagen, bitte versuche es erneut."</string>
<string name="screen_room_member_details_block_alert_action">"Blockieren"</string>
<string name="screen_room_member_details_block_user">"Nutzer blockieren"</string>
<string name="screen_room_member_details_unblock_alert_action">"Blockierung aufheben"</string>
<string name="screen_room_member_details_unblock_user">"Nutzer entblockieren"</string>
<string name="settings_rageshake_detection_threshold">"Erkennungsschwelle"</string>
<string name="settings_version_number">"Version: %1$s (%2$s)"</string>
<string name="test_language_identifier">"de"</string>
<string name="dialog_title_error">"Fehler"</string>
<string name="dialog_title_success">"Erfolg"</string>
<string name="screen_report_content_block_user">"Nutzer blockieren"</string>
</resources>
</resources>

View File

@@ -128,12 +128,6 @@
<string name="room_timeline_beginning_of_room_no_name">"Este es el principio de esta conversación."</string>
<string name="room_timeline_read_marker_title">"Nuevos"</string>
<string name="screen_report_content_block_user_hint">"Marque si quieres ocultar todos los mensajes actuales y futuros de este usuario"</string>
<string name="screen_room_member_details_block_alert_action">"Bloquear"</string>
<string name="screen_room_member_details_block_alert_description">"Los usuarios bloqueados no podrán enviarte mensajes y se ocultarán todos sus mensajes. Puede revertir esta acción en cualquier momento."</string>
<string name="screen_room_member_details_block_user">"Bloquear usuario"</string>
<string name="screen_room_member_details_unblock_alert_action">"Desbloquear"</string>
<string name="screen_room_member_details_unblock_alert_description">"Al desbloquear al usuario, podrás volver a ver todos sus mensajes."</string>
<string name="screen_room_member_details_unblock_user">"Desbloquear usuario"</string>
<string name="settings_rageshake">"Agitar con fuerza"</string>
<string name="settings_rageshake_detection_threshold">"Umbral de detección"</string>
<string name="settings_title_general">"General"</string>
@@ -142,4 +136,4 @@
<string name="dialog_title_error">"Error"</string>
<string name="dialog_title_success">"Terminado"</string>
<string name="screen_report_content_block_user">"Bloquear usuario"</string>
</resources>
</resources>

View File

@@ -128,12 +128,6 @@
<string name="room_timeline_beginning_of_room_no_name">"Questo è l\'inizio della conversazione."</string>
<string name="room_timeline_read_marker_title">"Nuovo"</string>
<string name="screen_report_content_block_user_hint">"Seleziona se vuoi nascondere tutti i messaggi attuali e futuri di questo utente"</string>
<string name="screen_room_member_details_block_alert_action">"Blocca"</string>
<string name="screen_room_member_details_block_alert_description">"Gli utenti bloccati non saranno in grado di inviarti nuovi messaggi e tutti quelli già esistenti saranno nascosti. Potrai annullare questa azione in qualsiasi momento."</string>
<string name="screen_room_member_details_block_user">"Blocca utente"</string>
<string name="screen_room_member_details_unblock_alert_action">"Sblocca"</string>
<string name="screen_room_member_details_unblock_alert_description">"Dopo aver sbloccato l\'utente, potrai vedere nuovamente tutti i suoi messaggi."</string>
<string name="screen_room_member_details_unblock_user">"Sblocca utente"</string>
<string name="settings_rageshake">"Rageshake"</string>
<string name="settings_rageshake_detection_threshold">"Soglia di rilevamento"</string>
<string name="settings_title_general">"Generali"</string>
@@ -142,4 +136,4 @@
<string name="dialog_title_error">"Errore"</string>
<string name="dialog_title_success">"Operazione riuscita"</string>
<string name="screen_report_content_block_user">"Blocca utente"</string>
</resources>
</resources>

View File

@@ -143,12 +143,6 @@
<string name="screen_analytics_prompt_third_party_sharing"><b>"Nu"</b>" împărtășim informații cu terți"</string>
<string name="screen_analytics_prompt_title">"Ajutați la îmbunătățirea %1$s"</string>
<string name="screen_report_content_block_user_hint">"Confirmați că doriți să ascundeți toate mesajele curente și viitoare de la acest utilizator"</string>
<string name="screen_room_member_details_block_alert_action">"Blocați"</string>
<string name="screen_room_member_details_block_alert_description">"Utilizatorii blocați nu vă vor putea trimite mesaje și toate mesajele lor vor fi ascunse. Puteți anula această acțiune oricând."</string>
<string name="screen_room_member_details_block_user">"Blocați utilizatorul"</string>
<string name="screen_room_member_details_unblock_alert_action">"Deblocați"</string>
<string name="screen_room_member_details_unblock_alert_description">"La deblocarea utilizatorului, veți putea vedea din nou toate mesajele de la acesta."</string>
<string name="screen_room_member_details_unblock_user">"Deblocați utilizatorul"</string>
<string name="settings_rageshake">"Rageshake"</string>
<string name="settings_rageshake_detection_threshold">"Prag de detecție"</string>
<string name="settings_title_general">"General"</string>
@@ -160,4 +154,4 @@
<string name="screen_analytics_settings_read_terms">"Puteți citi toate condițiile noastre %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"aici"</string>
<string name="screen_report_content_block_user">"Blocați utilizatorul"</string>
</resources>
</resources>

View File

@@ -147,12 +147,6 @@
<string name="screen_media_upload_preview_error_failed_processing">"Failed processing media to upload, please try again."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Failed uploading media, please try again."</string>
<string name="screen_report_content_block_user_hint">"Check if you want to hide all current and future messages from this user"</string>
<string name="screen_room_member_details_block_alert_action">"Block"</string>
<string name="screen_room_member_details_block_alert_description">"Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime."</string>
<string name="screen_room_member_details_block_user">"Block user"</string>
<string name="screen_room_member_details_unblock_alert_action">"Unblock"</string>
<string name="screen_room_member_details_unblock_alert_description">"On unblocking the user, you will be able to see all messages by them again."</string>
<string name="screen_room_member_details_unblock_user">"Unblock user"</string>
<string name="settings_rageshake">"Rageshake"</string>
<string name="settings_rageshake_detection_threshold">"Detection threshold"</string>
<string name="settings_title_general">"General"</string>

View File

@@ -85,6 +85,18 @@
"screen_room_member_list_.*",
"screen_dm_details_.*"
]
},
{
"name": ":features:messages:impl",
"includeRegex": [
"screen_room_.*",
"screen_dm_details_.*"
],
"excludeRegex": [
"screen_room_details_.*",
"screen_room_member.*",
"screen_dm_.*"
]
}
]
}

View File

@@ -36,10 +36,13 @@ allActions = []
# Iterating on the config
for entry in config["modules"]:
# Create action for the default language
excludeRegex = regexToAlwaysExclude
if "excludeRegex" in entry:
excludeRegex += entry["excludeRegex"]
action = baseAction | {
"output": convertModuleToPath(entry["name"]) + "/src/main/res/values/localazy.xml",
"includeKeys": list(map(lambda i: "REGEX:" + i, entry["includeRegex"])),
"excludeKeys": list(map(lambda i: "REGEX:" + i, regexToAlwaysExclude)),
"excludeKeys": list(map(lambda i: "REGEX:" + i, excludeRegex)),
"conditions": [
"equals: ${languageCode}, en"
]
@@ -51,7 +54,7 @@ for entry in config["modules"]:
actionTranslation = baseAction | {
"output": convertModuleToPath(entry["name"]) + "/src/main/res/values-${langAndroidResNoScript}/translations.xml",
"includeKeys": list(map(lambda i: "REGEX:" + i, entry["includeRegex"])),
"excludeKeys": list(map(lambda i: "REGEX:" + i, regexToAlwaysExclude)),
"excludeKeys": list(map(lambda i: "REGEX:" + i, excludeRegex)),
"conditions": [
"!equals: ${languageCode}, en"
]