Propose to edit media before sending, when coming form another application

This commit is contained in:
Benoit Marty 2020-02-13 11:55:22 +01:00
parent b7ec495d6b
commit 81de914360
20 changed files with 173 additions and 56 deletions

View file

@ -51,16 +51,21 @@ interface SendService {
/**
* Method to send a media asynchronously.
* @param attachment the media to send
* @param compressBeforeSending set to true to compress media before sending them
* @return a [Cancelable]
*/
fun sendMedia(attachment: ContentAttachmentData): Cancelable
fun sendMedia(attachment: ContentAttachmentData,
// TODO Change to a Compression Level Enum
compressBeforeSending: Boolean): Cancelable
/**
* Method to send a list of media asynchronously.
* @param attachments the list of media to send
* @return a [Cancelable]
*/
fun sendMedias(attachments: List<ContentAttachmentData>): Cancelable
fun sendMedias(attachments: List<ContentAttachmentData>,
// TODO Change to a Compression Level Enum
compressBeforeSending: Boolean): Cancelable
/**
* Send a poll to the room.

View file

@ -47,6 +47,7 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
val event: Event,
val attachment: ContentAttachmentData,
val isRoomEncrypted: Boolean,
val compressBeforeSending: Boolean,
override val lastFailureMessage: String? = null
) : SessionWorkerParams
@ -82,6 +83,8 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
)
}
// TODO Use compressBeforeSending
var uploadedThumbnailUrl: String? = null
var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null

View file

@ -111,9 +111,9 @@ internal class DefaultSendService @AssistedInject constructor(
}
}
override fun sendMedias(attachments: List<ContentAttachmentData>): Cancelable {
override fun sendMedias(attachments: List<ContentAttachmentData>, compressBeforeSending: Boolean): Cancelable {
return attachments.mapTo(CancelableBag()) {
sendMedia(it)
sendMedia(it, compressBeforeSending)
}
}
@ -201,18 +201,18 @@ internal class DefaultSendService @AssistedInject constructor(
}
}
override fun sendMedia(attachment: ContentAttachmentData): Cancelable {
override fun sendMedia(attachment: ContentAttachmentData, compressBeforeSending: Boolean): Cancelable {
// Create an event with the media file path
val event = localEchoEventFactory.createMediaEvent(roomId, attachment).also {
createLocalEcho(it)
}
return internalSendMedia(event, attachment)
return internalSendMedia(event, attachment, compressBeforeSending)
}
private fun internalSendMedia(localEcho: Event, attachment: ContentAttachmentData): Cancelable {
private fun internalSendMedia(localEcho: Event, attachment: ContentAttachmentData, compressBeforeSending: Boolean): Cancelable {
val isRoomEncrypted = cryptoService.isRoomEncrypted(roomId)
val uploadWork = createUploadMediaWork(localEcho, attachment, isRoomEncrypted, startChain = true)
val uploadWork = createUploadMediaWork(localEcho, attachment, isRoomEncrypted, compressBeforeSending, startChain = true)
val sendWork = createSendEventWork(localEcho, false)
if (isRoomEncrypted) {
@ -280,8 +280,9 @@ internal class DefaultSendService @AssistedInject constructor(
private fun createUploadMediaWork(event: Event,
attachment: ContentAttachmentData,
isRoomEncrypted: Boolean,
compressBeforeSending: Boolean,
startChain: Boolean): OneTimeWorkRequest {
val uploadMediaWorkerParams = UploadContentWorker.Params(sessionId, roomId, event, attachment, isRoomEncrypted)
val uploadMediaWorkerParams = UploadContentWorker.Params(sessionId, roomId, event, attachment, isRoomEncrypted, compressBeforeSending)
val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams)
return workManagerProvider.matrixOneTimeWorkRequestBuilder<UploadContentWorker>()

View file

@ -20,11 +20,16 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import com.kbeanie.multipicker.api.Picker.*
import com.kbeanie.multipicker.api.Picker.PICK_AUDIO
import com.kbeanie.multipicker.api.Picker.PICK_CONTACT
import com.kbeanie.multipicker.api.Picker.PICK_FILE
import com.kbeanie.multipicker.api.Picker.PICK_IMAGE_CAMERA
import com.kbeanie.multipicker.api.Picker.PICK_IMAGE_DEVICE
import com.kbeanie.multipicker.core.PickerManager
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.riotx.core.platform.Restorable
import im.vector.riotx.features.attachments.AttachmentsHelper.Callback
import timber.log.Timber
private const val CAPTURE_PATH_KEY = "CAPTURE_PATH_KEY"

View file

@ -16,7 +16,11 @@
package im.vector.riotx.features.attachments
import com.kbeanie.multipicker.api.entity.*
import com.kbeanie.multipicker.api.entity.ChosenAudio
import com.kbeanie.multipicker.api.entity.ChosenContact
import com.kbeanie.multipicker.api.entity.ChosenFile
import com.kbeanie.multipicker.api.entity.ChosenImage
import com.kbeanie.multipicker.api.entity.ChosenVideo
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import timber.log.Timber

View file

@ -21,7 +21,11 @@ import com.kbeanie.multipicker.api.callbacks.ContactPickerCallback
import com.kbeanie.multipicker.api.callbacks.FilePickerCallback
import com.kbeanie.multipicker.api.callbacks.ImagePickerCallback
import com.kbeanie.multipicker.api.callbacks.VideoPickerCallback
import com.kbeanie.multipicker.api.entity.*
import com.kbeanie.multipicker.api.entity.ChosenAudio
import com.kbeanie.multipicker.api.entity.ChosenContact
import com.kbeanie.multipicker.api.entity.ChosenFile
import com.kbeanie.multipicker.api.entity.ChosenImage
import com.kbeanie.multipicker.api.entity.ChosenVideo
/**
* This class delegates the PickerManager callbacks to an [AttachmentsHelper.Callback]

View file

@ -30,10 +30,11 @@ import im.vector.riotx.features.themes.ActivityOtherThemes
class AttachmentsPreviewActivity : VectorBaseActivity(), ToolbarConfigurable {
companion object {
const val REQUEST_CODE = 55
private const val EXTRA_FRAGMENT_ARGS = "EXTRA_FRAGMENT_ARGS"
const val RESULT_NAME = "ATTACHMENTS_PREVIEW_RESULT"
const val REQUEST_CODE = 55
private const val ATTACHMENTS_PREVIEW_RESULT = "ATTACHMENTS_PREVIEW_RESULT"
private const val KEEP_ORIGINAL_IMAGES_SIZE = "KEEP_ORIGINAL_IMAGES_SIZE"
fun newIntent(context: Context, args: AttachmentsPreviewArgs): Intent {
return Intent(context, AttachmentsPreviewActivity::class.java).apply {
@ -42,7 +43,11 @@ class AttachmentsPreviewActivity : VectorBaseActivity(), ToolbarConfigurable {
}
fun getOutput(intent: Intent): List<ContentAttachmentData> {
return intent.getParcelableArrayListExtra(RESULT_NAME)
return intent.getParcelableArrayListExtra(ATTACHMENTS_PREVIEW_RESULT)
}
fun getKeepOriginalSize(intent: Intent): Boolean {
return intent.getBooleanExtra(KEEP_ORIGINAL_IMAGES_SIZE, false)
}
}
@ -52,12 +57,20 @@ class AttachmentsPreviewActivity : VectorBaseActivity(), ToolbarConfigurable {
override fun initUiAndData() {
if (isFirstCreation()) {
val fragmentArgs: AttachmentsPreviewArgs = intent?.extras?.getParcelable(EXTRA_FRAGMENT_ARGS)
?: return
val fragmentArgs: AttachmentsPreviewArgs = intent?.extras?.getParcelable(EXTRA_FRAGMENT_ARGS) ?: return
addFragment(R.id.simpleFragmentContainer, AttachmentsPreviewFragment::class.java, fragmentArgs)
}
}
fun setResultAndFinish(data: List<ContentAttachmentData>, keepOriginalImageSize: Boolean) {
val resultIntent = Intent().apply {
putParcelableArrayListExtra(ATTACHMENTS_PREVIEW_RESULT, ArrayList(data))
putExtra(KEEP_ORIGINAL_IMAGES_SIZE, keepOriginalImageSize)
}
setResult(RESULT_OK, resultIntent)
finish()
}
override fun configure(toolbar: Toolbar) {
configureToolbar(toolbar)
}

View file

@ -54,7 +54,6 @@ import javax.inject.Inject
@Parcelize
data class AttachmentsPreviewArgs(
val roomId: String,
val attachments: List<ContentAttachmentData>
) : Parcelable
@ -133,11 +132,10 @@ class AttachmentsPreviewFragment @Inject constructor(
}
private fun setResultAndFinish() = withState(viewModel) {
val resultIntent = Intent().apply {
putParcelableArrayListExtra(AttachmentsPreviewActivity.RESULT_NAME, ArrayList(it.attachments))
}
requireActivity().setResult(RESULT_OK, resultIntent)
requireActivity().finish()
(requireActivity() as? AttachmentsPreviewActivity)?.setResultAndFinish(
it.attachments,
attachmentPreviewerSendImageOriginalSize.isChecked
)
}
private fun applyInsets() {

View file

@ -17,7 +17,6 @@
package im.vector.riotx.features.attachments.preview
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
@ -25,7 +24,6 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel
import kotlinx.coroutines.launch
class AttachmentsPreviewViewModel @AssistedInject constructor(@Assisted initialState: AttachmentsPreviewViewState)
: VectorViewModel<AttachmentsPreviewViewState, AttachmentsPreviewAction, AttachmentsPreviewViewEvents>(initialState) {

View file

@ -22,7 +22,8 @@ import im.vector.matrix.android.api.session.content.ContentAttachmentData
data class AttachmentsPreviewViewState(
val attachments: List<ContentAttachmentData>,
val currentAttachmentIndex: Int = 0
val currentAttachmentIndex: Int = 0,
val sendImagesWithOriginalSize: Boolean = false
) : MvRxState {
constructor(args: AttachmentsPreviewArgs) : this(attachments = args.attachments)

View file

@ -27,7 +27,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
data class UserIsTyping(val isTyping: Boolean) : RoomDetailAction()
data class SaveDraft(val draft: String) : RoomDetailAction()
data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : RoomDetailAction()
data class SendMedia(val attachments: List<ContentAttachmentData>) : RoomDetailAction()
data class SendMedia(val attachments: List<ContentAttachmentData>, val compressBeforeSending: Boolean) : RoomDetailAction()
data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailAction()
data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailAction()
data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailAction()

View file

@ -156,7 +156,6 @@ import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_room_detail.*
import kotlinx.android.synthetic.main.merge_composer_layout.*
import kotlinx.android.synthetic.main.merge_composer_layout.view.*
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
import org.commonmark.parser.Parser
@ -306,7 +305,10 @@ class RoomDetailFragment @Inject constructor(
is SharedData.Text -> {
roomDetailViewModel.handle(RoomDetailAction.ExitSpecialMode(composerLayout.text.toString()))
}
is SharedData.Attachments -> roomDetailViewModel.handle(RoomDetailAction.SendMedia(sharedData.attachmentData))
is SharedData.Attachments -> {
// open share edition
onContentAttachmentsReady(sharedData.attachmentData)
}
null -> Timber.v("No share data to process")
}
}
@ -506,7 +508,8 @@ class RoomDetailFragment @Inject constructor(
when (requestCode) {
AttachmentsPreviewActivity.REQUEST_CODE -> {
val sendData = AttachmentsPreviewActivity.getOutput(data)
roomDetailViewModel.handle(RoomDetailAction.SendMedia(sendData))
val keepOriginalSize = AttachmentsPreviewActivity.getKeepOriginalSize(data)
roomDetailViewModel.handle(RoomDetailAction.SendMedia(sendData, keepOriginalSize))
}
REACTION_SELECT_REQUEST_CODE -> {
val (eventId, reaction) = EmojiReactionPickerActivity.getOutput(data) ?: return
@ -1351,11 +1354,11 @@ class RoomDetailFragment @Inject constructor(
val previewable = attachments.filterPreviewables()
val nonPreviewable = attachments.filterNonPreviewables()
if (nonPreviewable.isNotEmpty()) {
// Send the non previewable event right now (?)
roomDetailViewModel.handle(RoomDetailAction.SendMedia(nonPreviewable))
// Send the non previewable attachment right now (?)
roomDetailViewModel.handle(RoomDetailAction.SendMedia(nonPreviewable, false))
}
if (previewable.isNotEmpty()) {
val intent = AttachmentsPreviewActivity.newIntent(requireContext(), AttachmentsPreviewArgs(roomDetailArgs.roomId, previewable))
val intent = AttachmentsPreviewActivity.newIntent(requireContext(), AttachmentsPreviewArgs(previewable))
startActivityForResult(intent, AttachmentsPreviewActivity.REQUEST_CODE)
}
}

View file

@ -579,10 +579,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
if (maxUploadFileSize == HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) {
// Unknown limitation
room.sendMedias(attachments)
room.sendMedias(attachments, action.compressBeforeSending)
} else {
when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) {
null -> room.sendMedias(attachments)
null -> room.sendMedias(attachments, action.compressBeforeSending)
else -> _viewEvents.post(RoomDetailViewEvents.FileTooBigError(
tooBigFile.name ?: tooBigFile.path,
tooBigFile.size,

View file

@ -22,6 +22,7 @@ import im.vector.riotx.core.platform.VectorViewModelAction
sealed class IncomingShareAction : VectorViewModelAction {
data class SelectRoom(val roomSummary: RoomSummary, val enableMultiSelect: Boolean) : IncomingShareAction()
object ShareToSelectedRooms : IncomingShareAction()
data class ShareMedia(val keepOriginalSize: Boolean) : IncomingShareAction()
data class FilterWith(val filter: String) : IncomingShareAction()
data class UpdateSharedData(val sharedData: SharedData) : IncomingShareAction()
}

View file

@ -16,6 +16,7 @@
package im.vector.riotx.features.share
import android.app.Activity
import android.content.ClipDescription
import android.content.Intent
import android.os.Bundle
@ -41,6 +42,8 @@ import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_PICK_ATTACHMENT
import im.vector.riotx.core.utils.allGranted
import im.vector.riotx.core.utils.checkPermissions
import im.vector.riotx.features.attachments.AttachmentsHelper
import im.vector.riotx.features.attachments.preview.AttachmentsPreviewActivity
import im.vector.riotx.features.attachments.preview.AttachmentsPreviewArgs
import im.vector.riotx.features.login.LoginActivity
import kotlinx.android.synthetic.main.fragment_incoming_share.*
import javax.inject.Inject
@ -99,10 +102,30 @@ class IncomingShareFragment @Inject constructor(
incomingShareViewModel.observeViewEvents {
when (it) {
is IncomingShareViewEvents.ShareToRoom -> handleShareToRoom(it)
is IncomingShareViewEvents.EditMediaBeforeSending -> handleEditMediaBeforeSending(it)
}.exhaustive
}
}
private fun handleEditMediaBeforeSending(event: IncomingShareViewEvents.EditMediaBeforeSending) {
val intent = AttachmentsPreviewActivity.newIntent(requireContext(), AttachmentsPreviewArgs(event.contentAttachmentData))
startActivityForResult(intent, AttachmentsPreviewActivity.REQUEST_CODE)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
val hasBeenHandled = attachmentsHelper.onActivityResult(requestCode, resultCode, data)
if (!hasBeenHandled && resultCode == Activity.RESULT_OK && data != null) {
when (requestCode) {
AttachmentsPreviewActivity.REQUEST_CODE -> {
val sendData = AttachmentsPreviewActivity.getOutput(data)
val keepOriginalSize = AttachmentsPreviewActivity.getKeepOriginalSize(data)
incomingShareViewModel.handle(IncomingShareAction.UpdateSharedData(SharedData.Attachments(sendData)))
incomingShareViewModel.handle(IncomingShareAction.ShareMedia(keepOriginalSize))
}
}
}
}
override fun onResume() {
super.onResume()
@ -189,7 +212,7 @@ class IncomingShareFragment @Inject constructor(
}
override fun invalidate() = withState(incomingShareViewModel) {
sendShareButton.isVisible = it.multiSelectionEnabled
sendShareButton.isVisible = it.isInMultiSelectionMode
incomingShareController.setData(it)
}

View file

@ -16,9 +16,11 @@
package im.vector.riotx.features.share
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotx.core.platform.VectorViewEvents
sealed class IncomingShareViewEvents : VectorViewEvents {
data class ShareToRoom(val roomSummary: RoomSummary, val sharedData: SharedData, val showAlert: Boolean) : IncomingShareViewEvents()
data class EditMediaBeforeSending(val contentAttachmentData: List<ContentAttachmentData>) : IncomingShareViewEvents()
}

View file

@ -24,12 +24,14 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
import im.vector.matrix.rx.rx
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.home.room.list.BreadcrumbsRoomComparator
import im.vector.riotx.features.attachments.filterNonPreviewables
import im.vector.riotx.features.attachments.filterPreviewables
import im.vector.riotx.features.home.room.list.ChronologicalRoomComparator
import java.util.concurrent.TimeUnit
@ -92,6 +94,7 @@ class IncomingShareViewModel @AssistedInject constructor(@Assisted initialState:
when (action) {
is IncomingShareAction.SelectRoom -> handleSelectRoom(action)
is IncomingShareAction.ShareToSelectedRooms -> handleShareToSelectedRooms()
is IncomingShareAction.ShareMedia -> handleShareMediaToSelectedRooms(action)
is IncomingShareAction.FilterWith -> handleFilter(action)
is IncomingShareAction.UpdateSharedData -> handleUpdateSharedData(action)
}.exhaustive
@ -108,34 +111,70 @@ class IncomingShareViewModel @AssistedInject constructor(@Assisted initialState:
private fun handleShareToSelectedRooms() = withState { state ->
val sharedData = state.sharedData ?: return@withState
if (state.selectedRoomIds.size == 1) {
// In this case the edition of the media will be handled by the RoomDetailFragment
val selectedRoomId = state.selectedRoomIds.first()
val selectedRoom = state.roomSummaries()?.find { it.roomId == selectedRoomId } ?: return@withState
_viewEvents.post(IncomingShareViewEvents.ShareToRoom(selectedRoom, sharedData, showAlert = false))
} else {
when (sharedData) {
is SharedData.Text -> {
state.selectedRoomIds.forEach { roomId ->
val room = session.getRoom(roomId)
if (sharedData is SharedData.Text) {
room?.sendTextMessage(sharedData.text)
} else if (sharedData is SharedData.Attachments) {
room?.sendMedias(sharedData.attachmentData)
}
}
is SharedData.Attachments -> {
shareAttachments(sharedData.attachmentData, state.selectedRoomIds, proposeMediaEdition = true, compressMediaBeforeSending = false)
}
}
}
}
private fun handleSelectRoom(action: IncomingShareAction.SelectRoom) = withState {
if (it.multiSelectionEnabled) {
val selectedRooms = it.selectedRoomIds
private fun shareAttachments(attachmentData: List<ContentAttachmentData>,
selectedRoomIds: Set<String>,
proposeMediaEdition: Boolean,
compressMediaBeforeSending: Boolean) {
if (!proposeMediaEdition) {
selectedRoomIds.forEach { roomId ->
val room = session.getRoom(roomId)
room?.sendMedias(attachmentData, compressMediaBeforeSending)
}
} else {
val previewable = attachmentData.filterPreviewables()
val nonPreviewable = attachmentData.filterNonPreviewables()
if (nonPreviewable.isNotEmpty()) {
// Send the non previewable attachment right now (?)
selectedRoomIds.forEach { roomId ->
val room = session.getRoom(roomId)
room?.sendMedias(nonPreviewable, compressMediaBeforeSending)
}
}
if (previewable.isNotEmpty()) {
// In case of multiple share of media, edit them first
_viewEvents.post(IncomingShareViewEvents.EditMediaBeforeSending(previewable))
}
}
}
private fun handleShareMediaToSelectedRooms(action: IncomingShareAction.ShareMedia) = withState { state ->
(state.sharedData as? SharedData.Attachments)?.let {
shareAttachments(it.attachmentData, state.selectedRoomIds, proposeMediaEdition = false, compressMediaBeforeSending = !action.keepOriginalSize)
}
}
private fun handleSelectRoom(action: IncomingShareAction.SelectRoom) = withState { state ->
if (state.isInMultiSelectionMode) {
val selectedRooms = state.selectedRoomIds
val newSelectedRooms = if (selectedRooms.contains(action.roomSummary.roomId)) {
selectedRooms.minus(action.roomSummary.roomId)
} else {
selectedRooms.plus(action.roomSummary.roomId)
}
setState { copy(multiSelectionEnabled = newSelectedRooms.isNotEmpty(), selectedRoomIds = newSelectedRooms) }
setState { copy(isInMultiSelectionMode = newSelectedRooms.isNotEmpty(), selectedRoomIds = newSelectedRooms) }
} else if (action.enableMultiSelect) {
setState { copy(multiSelectionEnabled = true, selectedRoomIds = setOf(action.roomSummary.roomId)) }
setState { copy(isInMultiSelectionMode = true, selectedRoomIds = setOf(action.roomSummary.roomId)) }
} else {
val sharedData = it.sharedData ?: return@withState
val sharedData = state.sharedData ?: return@withState
_viewEvents.post(IncomingShareViewEvents.ShareToRoom(action.roomSummary, sharedData, showAlert = true))
}
}

View file

@ -26,7 +26,7 @@ data class IncomingShareViewState(
val roomSummaries: Async<List<RoomSummary>> = Uninitialized,
val filteredRoomSummaries: Async<List<RoomSummary>> = Uninitialized,
val selectedRoomIds: Set<String> = emptySet(),
val multiSelectionEnabled: Boolean = false
val isInMultiSelectionMode: Boolean = false
) : MvRxState

View file

@ -39,11 +39,24 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="8dp"
app:layout_constraintBottom_toTopOf="@+id/attachmentPreviewerSendImageOriginalSize"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:itemCount="1"
tools:listitem="@layout/item_attachment_miniature_preview" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/attachmentPreviewerSendImageOriginalSize"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:textColor="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:itemCount="1"
tools:listitem="@layout/item_attachment_miniature_preview" />
app:layout_constraintTop_toBottomOf="@+id/attachmentPreviewerMiniatureList"
tools:text="@plurals/send_images_with_original_size" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -22,6 +22,10 @@
<!-- BEGIN Strings added by Benoit -->
<string name="message_action_item_redact">Remove…</string>
<string name="share_confirm_room">Do you want to send this attachment to %1$s?</string>
<plurals name="send_images_with_original_size">
<item quantity="one">Send image with the original size</item>
<item quantity="other">Send images with the original size</item>
</plurals>
<!-- END Strings added by Benoit -->