[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:
committed by
GitHub
parent
5c935818c6
commit
31ac97d17a
1
changelog.d/360.feature
Normal file
1
changelog.d/360.feature
Normal file
@@ -0,0 +1 @@
|
||||
Add media pickers to the room screen.
|
||||
@@ -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 can’t validate this user’s Matrix ID. The invite might not be received."</string>
|
||||
<string name="screen_create_room_title">"Create a room"</string>
|
||||
</resources>
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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) =
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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("")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -31,5 +31,6 @@ fun aMessageComposerState() = MessageComposerState(
|
||||
text = StableCharSequence(""),
|
||||
isFullScreen = false,
|
||||
mode = MessageComposerMode.Normal(content = ""),
|
||||
attachmentSourcePicker = null,
|
||||
eventSink = {}
|
||||
)
|
||||
|
||||
@@ -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(),
|
||||
|
||||
9
features/messages/impl/src/main/res/values/localazy.xml
Normal file
9
features/messages/impl/src/main/res/values/localazy.xml
Normal 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 & Video Library"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Failed processing media to upload, please try again."</string>
|
||||
</resources>
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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_.*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user