Start post submission activity

This commit is contained in:
Marie 2022-10-30 16:24:46 +01:00 committed by Matthieu
parent 2d712ed395
commit 851d95bf0f
10 changed files with 812 additions and 155 deletions

View File

@ -8,6 +8,7 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: 'jacoco' apply plugin: 'jacoco'
apply plugin: "kotlin-parcelize"
// Force latest version of Jacoco, initially done to resolve https://github.com/jacoco/jacoco/issues/1155 // Force latest version of Jacoco, initially done to resolve https://github.com/jacoco/jacoco/issues/1155

View File

@ -72,6 +72,9 @@
<data android:mimeType="video/*" /> <data android:mimeType="video/*" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".postCreation.PostSubmissionActivity">
</activity>
<activity <activity
android:name=".profile.FollowsActivity" android:name=".profile.FollowsActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"

View File

@ -19,6 +19,7 @@ import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.content.ContextCompat
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doAfterTextChanged
@ -32,6 +33,7 @@ import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityPostCreationBinding import org.pixeldroid.app.databinding.ActivityPostCreationBinding
import org.pixeldroid.app.postCreation.camera.CameraActivity import org.pixeldroid.app.postCreation.camera.CameraActivity
import org.pixeldroid.app.postCreation.carousel.CarouselItem import org.pixeldroid.app.postCreation.carousel.CarouselItem
import org.pixeldroid.app.searchDiscover.TrendingActivity
import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
@ -112,37 +114,14 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
binding.uploadProgressBar.visibility = binding.uploadProgressBar.visibility =
if (uiState.uploadProgressBarVisible) VISIBLE else INVISIBLE if (uiState.uploadProgressBarVisible) VISIBLE else INVISIBLE
binding.uploadProgressBar.progress = uiState.uploadProgress binding.uploadProgressBar.progress = uiState.uploadProgress
binding.uploadCompletedTextview.visibility =
if (uiState.uploadCompletedTextviewVisible) VISIBLE else INVISIBLE
binding.removePhotoButton.isEnabled = uiState.removePhotoButtonEnabled binding.removePhotoButton.isEnabled = uiState.removePhotoButtonEnabled
binding.editPhotoButton.isEnabled = uiState.editPhotoButtonEnabled binding.editPhotoButton.isEnabled = uiState.editPhotoButtonEnabled
binding.uploadError.visibility =
if (uiState.uploadErrorVisible) VISIBLE else INVISIBLE
binding.uploadErrorTextExplanation.visibility =
if (uiState.uploadErrorExplanationVisible) VISIBLE else INVISIBLE
binding.toolbarPostCreation.visibility = binding.toolbarPostCreation.visibility =
if (uiState.isCarousel) VISIBLE else INVISIBLE if (uiState.isCarousel) VISIBLE else INVISIBLE
binding.carousel.layoutCarousel = uiState.isCarousel binding.carousel.layoutCarousel = uiState.isCarousel
binding.uploadErrorTextExplanation.text = uiState.uploadErrorExplanationText
} }
} }
} }
binding.newPostDescriptionInputField.doAfterTextChanged {
model.newPostDescriptionChanged(binding.newPostDescriptionInputField.text)
}
val existingDescription: String? = intent.getStringExtra(PICTURE_DESCRIPTION)
binding.newPostDescriptionInputField.setText(
// Set description from redraft if any, otherwise from the template
existingDescription ?: model.uiState.value.newPostDescriptionText
)
binding.postTextInputLayout.counterMaxLength = instance.maxStatusChars
binding.carousel.apply { binding.carousel.apply {
layoutCarouselCallback = { model.becameCarousel(it)} layoutCarouselCallback = { model.becameCarousel(it)}
@ -156,13 +135,9 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
} }
// get the description and send the post // get the description and send the post
binding.postCreationSendButton.setOnClickListener { binding.postCreationSendButton.setOnClickListener {
if (validatePost() && model.isNotEmpty()) model.upload() if (validatePost() && model.isNotEmpty()) {
} model.upload(it.context, binding.root.context)
}
// Button to retry image upload when it fails
binding.retryUploadButton.setOnClickListener {
model.resetUploadStatus()
model.upload()
} }
binding.editPhotoButton.setOnClickListener { binding.editPhotoButton.setOnClickListener {
@ -181,7 +156,6 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
} }
} }
binding.removePhotoButton.setOnClickListener { binding.removePhotoButton.setOnClickListener {
binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition -> binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
model.removeAt(currentPosition) model.removeAt(currentPosition)
@ -300,14 +274,6 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
private fun validatePost(): Boolean { private fun validatePost(): Boolean {
binding.postTextInputLayout.run {
val content = editText?.length() ?: 0
if (content > counterMaxLength) {
// error, too many characters
error = resources.getQuantityString(R.plurals.description_max_characters, counterMaxLength, counterMaxLength)
return false
}
}
if(model.getPhotoData().value?.all { it.videoEncodeProgress == null } == false){ if(model.getPhotoData().value?.all { it.videoEncodeProgress == null } == false){
AlertDialog.Builder(this).apply { AlertDialog.Builder(this).apply {
setMessage(R.string.still_encoding) setMessage(R.string.still_encoding)
@ -321,10 +287,10 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
private fun enableButton(enable: Boolean = true){ private fun enableButton(enable: Boolean = true){
binding.postCreationSendButton.isEnabled = enable binding.postCreationSendButton.isEnabled = enable
if(enable){ if(enable){
binding.postingProgressBar.visibility = GONE binding.submittingProgressBar.visibility = GONE
binding.postCreationSendButton.visibility = VISIBLE binding.postCreationSendButton.visibility = VISIBLE
} else { } else {
binding.postingProgressBar.visibility = VISIBLE binding.submittingProgressBar.visibility = VISIBLE
binding.postCreationSendButton.visibility = GONE binding.postCreationSendButton.visibility = GONE
} }

View File

@ -2,12 +2,15 @@ package org.pixeldroid.app.postCreation
import android.app.Application import android.app.Application
import android.content.ClipData import android.content.ClipData
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Parcelable
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.text.Editable import android.text.Editable
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
@ -22,6 +25,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import okhttp3.MultipartBody import okhttp3.MultipartBody
import org.pixeldroid.app.MainActivity import org.pixeldroid.app.MainActivity
import org.pixeldroid.app.R import org.pixeldroid.app.R
@ -37,6 +41,7 @@ import retrofit2.HttpException
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
import java.io.Serializable
import java.net.URI import java.net.URI
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.ceil import kotlin.math.ceil
@ -61,8 +66,9 @@ data class PostCreationActivityUiState(
val uploadErrorVisible: Boolean = false, val uploadErrorVisible: Boolean = false,
val uploadErrorExplanationText: String = "", val uploadErrorExplanationText: String = "",
val uploadErrorExplanationVisible: Boolean = false, val uploadErrorExplanationVisible: Boolean = false,
) )
@Parcelize
data class PhotoData( data class PhotoData(
var imageUri: Uri, var imageUri: Uri,
var size: Long, var size: Long,
@ -74,12 +80,12 @@ data class PhotoData(
var videoEncodeStabilizationFirstPass: Boolean? = null, var videoEncodeStabilizationFirstPass: Boolean? = null,
var videoEncodeComplete: Boolean = false, var videoEncodeComplete: Boolean = false,
var videoEncodeError: Boolean = false, var videoEncodeError: Boolean = false,
) ) : Parcelable
class PostCreationViewModel(application: Application, clipdata: ClipData? = null, val instance: InstanceDatabaseEntity? = null) : AndroidViewModel(application) { class PostCreationViewModel(application: Application, clipdata: ClipData? = null, val instance: InstanceDatabaseEntity? = null) : AndroidViewModel(application) {
private val photoData: MutableLiveData<MutableList<PhotoData>> by lazy { private val photoData: MutableLiveData<MutableList<PhotoData>> by lazy {
MutableLiveData<MutableList<PhotoData>>().also { MutableLiveData<MutableList<PhotoData>>().also {
it.value = clipdata?.let { it1 -> addPossibleImages(it1, mutableListOf()) } it.value = clipdata?.let { it1 -> addPossibleImages(it1, mutableListOf()) }
} }
} }
@ -238,7 +244,7 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
* [PhotoData.uploadId] (for the list of ids of the uploads). * [PhotoData.uploadId] (for the list of ids of the uploads).
*/ */
@OptIn(ExperimentalUnsignedTypes::class) @OptIn(ExperimentalUnsignedTypes::class)
fun upload() { fun upload(context: Context, bindingContext: Context) {
_uiState.update { currentUiState -> _uiState.update { currentUiState ->
currentUiState.copy( currentUiState.copy(
postCreationSendButtonEnabled = false, postCreationSendButtonEnabled = false,
@ -251,6 +257,10 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
) )
} }
val intent = Intent(context, PostSubmissionActivity::class.java)
intent.putExtra(PostSubmissionActivity.PHOTO_DATA, getPhotoData().value?.let { ArrayList(it) })
ContextCompat.startActivity(bindingContext, intent, null)
for (data: PhotoData in getPhotoData().value ?: emptyList()) { for (data: PhotoData in getPhotoData().value ?: emptyList()) {
val extension = data.imageUri.fileExtension(getApplication<PixelDroidApplication>().contentResolver) val extension = data.imageUri.fileExtension(getApplication<PixelDroidApplication>().contentResolver)
@ -409,7 +419,7 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
} }
fun modifyAt(position: Int, data: Intent): Unit? { fun modifyAt(position: Int, data: Intent): Unit? {
val result: PhotoData = photoData.value?.getOrNull(position)?.run { val result: PhotoData = photoData.value?.getOrNull(position)?.run {
if (video) { if (video) {
val modified: Boolean = data.getBooleanExtra(VideoEditActivity.MODIFIED, false) val modified: Boolean = data.getBooleanExtra(VideoEditActivity.MODIFIED, false)
if(modified){ if(modified){
@ -446,10 +456,6 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
return Unit return Unit
} }
fun newPostDescriptionChanged(text: Editable?) {
_uiState.update { it.copy(newPostDescriptionText = text.toString()) }
}
/** /**
* @param originalUri the Uri of the file you sent to be edited * @param originalUri the Uri of the file you sent to be edited
* @param progress percentage of (this pass of) encoding that is done * @param progress percentage of (this pass of) encoding that is done
@ -520,11 +526,10 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
) )
} }
} }
} }
class PostCreationViewModelFactory(val application: Application, val clipdata: ClipData, val instance: InstanceDatabaseEntity) : ViewModelProvider.Factory { class PostCreationViewModelFactory(val application: Application, val clipdata: ClipData, val instance: InstanceDatabaseEntity) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T { override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.getConstructor(Application::class.java, ClipData::class.java, InstanceDatabaseEntity::class.java).newInstance(application, clipdata, instance) return modelClass.getConstructor(Application::class.java, ClipData::class.java, InstanceDatabaseEntity::class.java).newInstance(application, clipdata, instance)
} }
} }

View File

@ -0,0 +1,194 @@
package org.pixeldroid.app.postCreation
import android.app.Activity
import android.app.AlertDialog
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Intent
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.*
import android.provider.MediaStore
import android.util.Log
import android.view.View
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.view.View.GONE
import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityPostCreationBinding
import org.pixeldroid.app.databinding.ActivityPostSubmissionBinding
import org.pixeldroid.app.postCreation.camera.CameraActivity
import org.pixeldroid.app.postCreation.carousel.CarouselItem
import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import org.pixeldroid.app.utils.fileExtension
import org.pixeldroid.app.utils.getMimeType
import org.pixeldroid.app.utils.setSquareImageFromURL
import java.io.File
import java.io.OutputStream
import java.text.SimpleDateFormat
import java.util.*
import kotlin.collections.ArrayList
class PostSubmissionActivity : BaseThemedWithoutBarActivity() {
companion object {
internal const val PICTURE_DESCRIPTION = "picture_description"
internal const val TEMP_FILES = "temp_files"
internal const val POST_REDRAFT = "post_redraft"
internal const val PHOTO_DATA = "photo_data"
}
private var user: UserDatabaseEntity? = null
private lateinit var instance: InstanceDatabaseEntity
private lateinit var binding: ActivityPostSubmissionBinding
private lateinit var model: PostSubmissionViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityPostSubmissionBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
user = db.userDao().getActiveUser()
instance = user?.run {
db.instanceDao().getAll().first { instanceDatabaseEntity ->
instanceDatabaseEntity.uri.contains(instance_uri)
}
} ?: InstanceDatabaseEntity("", "")
val photoData = intent.getParcelableArrayListExtra<PhotoData>(PHOTO_DATA) as ArrayList<PhotoData>?
val _model: PostSubmissionViewModel by viewModels {
PostSubmissionViewModelFactory(
application,
photoData!!,
instance
)
}
model = _model
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
model.uiState.collect { uiState ->
uiState.userMessage?.let {
AlertDialog.Builder(binding.root.context).apply {
setMessage(it)
setNegativeButton(android.R.string.ok) { _, _ -> }
}.show()
// Notify the ViewModel the message is displayed
model.userMessageShown()
}
enableButton(uiState.postCreationSendButtonEnabled)
binding.uploadProgressBar.visibility =
if (uiState.uploadProgressBarVisible) VISIBLE else INVISIBLE
binding.uploadProgressBar.progress = uiState.uploadProgress
binding.uploadCompletedTextview.visibility =
if (uiState.uploadCompletedTextviewVisible) VISIBLE else INVISIBLE
binding.uploadError.visibility =
if (uiState.uploadErrorVisible) VISIBLE else INVISIBLE
binding.uploadErrorTextExplanation.visibility =
if (uiState.uploadErrorExplanationVisible) VISIBLE else INVISIBLE
binding.uploadErrorTextExplanation.text = uiState.uploadErrorExplanationText
}
}
}
binding.newPostDescriptionInputField.doAfterTextChanged {
model.newPostDescriptionChanged(binding.newPostDescriptionInputField.text)
}
val existingDescription: String? = intent.getStringExtra(PICTURE_DESCRIPTION)
binding.newPostDescriptionInputField.setText(
// Set description from redraft if any, otherwise from the template
existingDescription ?: model.uiState.value.newPostDescriptionText
)
binding.postTextInputLayout.counterMaxLength = instance.maxStatusChars
val firstPostImage = photoData!![0]
setSquareImageFromURL(View(applicationContext), firstPostImage.imageUri.toString(), binding.postPreview)
// get the description and send the post
binding.postCreationSendButton.setOnClickListener {
if (validatePost()) model.upload()
}
// Button to retry image upload when it fails
binding.retryUploadButton.setOnClickListener {
model.resetUploadStatus()
model.upload()
}
// Clean up temporary files, if any
val tempFiles = intent.getStringArrayExtra(TEMP_FILES)
tempFiles?.asList()?.forEach {
val file = File(binding.root.context.cacheDir, it)
model.trackTempFile(file)
}
}
override fun onBackPressed() {
val redraft = intent.getBooleanExtra(POST_REDRAFT, false)
if (redraft) {
val builder = AlertDialog.Builder(binding.root.context)
builder.apply {
setMessage(R.string.redraft_dialog_cancel)
setPositiveButton(android.R.string.ok) { _, _ ->
super.onBackPressed()
}
setNegativeButton(android.R.string.cancel) { _, _ -> }
show()
}
} else {
super.onBackPressed()
}
}
private fun validatePost(): Boolean {
binding.postTextInputLayout.run {
val content = editText?.length() ?: 0
if (content > counterMaxLength) {
// error, too many characters
error = resources.getQuantityString(R.plurals.description_max_characters, counterMaxLength, counterMaxLength)
return false
}
}
return true
}
private fun enableButton(enable: Boolean = true){
binding.postCreationSendButton.isEnabled = enable
if(enable){
binding.postingProgressBar.visibility = GONE
binding.postCreationSendButton.visibility = VISIBLE
} else {
binding.postingProgressBar.visibility = VISIBLE
binding.postCreationSendButton.visibility = GONE
}
}
}

View File

@ -0,0 +1,415 @@
package org.pixeldroid.app.postCreation
import android.app.Application
import android.content.ClipData
import android.content.Intent
import android.net.Uri
import android.provider.OpenableColumns
import android.text.Editable
import android.util.Log
import android.widget.Toast
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.exifinterface.media.ExifInterface
import androidx.lifecycle.*
import androidx.preference.PreferenceManager
import com.jarsilio.android.scrambler.exceptions.UnsupportedFileFormatException
import com.jarsilio.android.scrambler.stripMetadata
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import okhttp3.MultipartBody
import org.pixeldroid.app.MainActivity
import org.pixeldroid.app.R
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity.RelativeCropPosition
import org.pixeldroid.app.utils.PixelDroidApplication
import org.pixeldroid.app.utils.api.objects.Attachment
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
import org.pixeldroid.app.utils.fileExtension
import org.pixeldroid.app.utils.getMimeType
import retrofit2.HttpException
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.net.URI
import javax.inject.Inject
import kotlin.math.ceil
// Models the UI state for the PostCreationActivity
data class PostSubmissionActivityUiState(
val userMessage: String? = null,
val postCreationSendButtonEnabled: Boolean = true,
val newPostDescriptionText: String = "",
val uploadProgressBarVisible: Boolean = false,
val uploadProgress: Int = 0,
val uploadCompletedTextviewVisible: Boolean = false,
val uploadErrorVisible: Boolean = false,
val uploadErrorExplanationText: String = "",
val uploadErrorExplanationVisible: Boolean = false,
)
class PostSubmissionViewModel(application: Application, clipdata: ClipData? = null, val instance: InstanceDatabaseEntity? = null) : AndroidViewModel(application) {
private val photoData: MutableLiveData<MutableList<PhotoData>> by lazy {
MutableLiveData<MutableList<PhotoData>>().also {
it.value = clipdata?.let { it1 -> addPossibleImages(it1, mutableListOf()) }
}
}
@Inject
lateinit var apiHolder: PixelfedAPIHolder
private val _uiState: MutableStateFlow<PostCreationActivityUiState>
init {
(application as PixelDroidApplication).getAppComponent().inject(this)
val sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(application)
val initialDescription = sharedPreferences.getString("prefill_description", "") ?: ""
_uiState = MutableStateFlow(PostCreationActivityUiState(newPostDescriptionText = initialDescription))
}
val uiState: StateFlow<PostCreationActivityUiState> = _uiState
// Map photoData indexes to FFmpeg Session IDs
private val sessionMap: MutableMap<Uri, Long> = mutableMapOf()
// Keep track of temporary files to delete them (avoids filling cache super fast with videos)
private val tempFiles: java.util.ArrayList<File> = java.util.ArrayList()
fun userMessageShown() {
_uiState.update { currentUiState ->
currentUiState.copy(userMessage = null)
}
}
fun getPhotoData(): LiveData<MutableList<PhotoData>> = photoData
/**
* Will add as many images as possible to [photoData], from the [clipData], and if
* ([photoData].size + [clipData].itemCount) > [InstanceDatabaseEntity.albumLimit] then it will only add as many images
* as are legal (if any) and a dialog will be shown to the user alerting them of this fact.
*/
fun addPossibleImages(clipData: ClipData, previousList: MutableList<PhotoData>? = photoData.value): MutableList<PhotoData> {
val dataToAdd: ArrayList<PhotoData> = arrayListOf()
var count = clipData.itemCount
if(count + (previousList?.size ?: 0) > instance!!.albumLimit){
_uiState.update { currentUiState ->
currentUiState.copy(userMessage = getApplication<PixelDroidApplication>().getString(R.string.total_exceeds_album_limit).format(instance.albumLimit))
}
count = count.coerceAtMost(instance.albumLimit - (previousList?.size ?: 0))
}
if (count + (previousList?.size ?: 0) >= instance.albumLimit) {
// Disable buttons to add more images
_uiState.update { currentUiState ->
currentUiState.copy(addPhotoButtonEnabled = false)
}
}
for (i in 0 until count) {
clipData.getItemAt(i).let {
val sizeAndVideoPair: Pair<Long, Boolean> =
getSizeAndVideoValidate(it.uri, (previousList?.size ?: 0) + dataToAdd.size + 1)
dataToAdd.add(PhotoData(imageUri = it.uri, size = sizeAndVideoPair.first, video = sizeAndVideoPair.second, imageDescription = it.text?.toString()))
}
}
return previousList?.plus(dataToAdd)?.toMutableList() ?: mutableListOf()
}
fun setImages(addPossibleImages: MutableList<PhotoData>) {
photoData.value = addPossibleImages
}
/**
* Returns the size of the file of the Uri, and whether it is a video,
* and opens a dialog in case it is too big or in case the file is unsupported.
*/
fun getSizeAndVideoValidate(uri: Uri, editPosition: Int): Pair<Long, Boolean> {
val size: Long =
if (uri.scheme =="content") {
getApplication<PixelDroidApplication>().contentResolver.query(uri, null, null, null, null)
?.use { cursor ->
/* Get the column indexes of the data in the Cursor,
* move to the first row in the Cursor, get the data,
* and display it.
*/
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
cursor.moveToFirst()
cursor.getLong(sizeIndex)
} ?: 0
} else {
uri.toFile().length()
}
val sizeInkBytes = ceil(size.toDouble() / 1000).toLong()
val type = uri.getMimeType(getApplication<PixelDroidApplication>().contentResolver)
val isVideo = type.startsWith("video/")
if(isVideo && !instance!!.videoEnabled){
_uiState.update { currentUiState ->
currentUiState.copy(userMessage = getApplication<PixelDroidApplication>().getString(R.string.video_not_supported))
}
}
if (sizeInkBytes > instance!!.maxPhotoSize || sizeInkBytes > instance.maxVideoSize) {
val maxSize = if (isVideo) instance.maxVideoSize else instance.maxPhotoSize
_uiState.update { currentUiState ->
currentUiState.copy(
userMessage = getApplication<PixelDroidApplication>().getString(R.string.size_exceeds_instance_limit, editPosition, sizeInkBytes, maxSize)
)
}
}
return Pair(size, isVideo)
}
fun isNotEmpty(): Boolean = photoData.value?.isNotEmpty() ?: false
fun updateDescription(position: Int, description: String) {
photoData.value?.getOrNull(position)?.imageDescription = description
photoData.value = photoData.value
}
fun resetUploadStatus() {
photoData.value = photoData.value?.map { it.copy(uploadId = null, progress = null) }?.toMutableList()
}
fun setVideoEncodeAtPosition(uri: Uri, progress: Int?, stabilizationFirstPass: Boolean = false, error: Boolean = false) {
photoData.value?.indexOfFirst { it.imageUri == uri }?.let { position ->
photoData.value?.set(position,
photoData.value!![position].copy(
videoEncodeProgress = progress,
videoEncodeStabilizationFirstPass = stabilizationFirstPass,
videoEncodeError = error,
)
)
photoData.value = photoData.value
}
}
fun setUriAtPosition(uri: Uri, position: Int) {
photoData.value?.set(position, photoData.value!![position].copy(imageUri = uri))
photoData.value = photoData.value
}
fun setSizeAtPosition(imageSize: Long, position: Int) {
photoData.value?.set(position, photoData.value!![position].copy(size = imageSize))
photoData.value = photoData.value
}
fun removeAt(currentPosition: Int) {
photoData.value?.removeAt(currentPosition)
_uiState.update {
it.copy(
addPhotoButtonEnabled = true
)
}
photoData.value = photoData.value
}
/**
* Uploads the images that are in the [photoData] array.
* Keeps track of them in the [PhotoData.progress] (for the upload progress), and the
* [PhotoData.uploadId] (for the list of ids of the uploads).
*/
@OptIn(ExperimentalUnsignedTypes::class)
fun upload() {
_uiState.update { currentUiState ->
currentUiState.copy(
postCreationSendButtonEnabled = false,
addPhotoButtonEnabled = false,
editPhotoButtonEnabled = false,
removePhotoButtonEnabled = false,
uploadCompletedTextviewVisible = false,
uploadErrorVisible = false,
uploadProgressBarVisible = true
)
}
for (data: PhotoData in getPhotoData().value ?: emptyList()) {
val extension = data.imageUri.fileExtension(getApplication<PixelDroidApplication>().contentResolver)
val strippedImage = File.createTempFile("temp_img", ".$extension", getApplication<PixelDroidApplication>().cacheDir)
val imageUri = data.imageUri
val (strippedOrNot, size) = try {
val orientation = ExifInterface(getApplication<PixelDroidApplication>().contentResolver.openInputStream(imageUri)!!).getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
stripMetadata(imageUri, strippedImage, getApplication<PixelDroidApplication>().contentResolver)
// Restore EXIF orientation
val exifInterface = ExifInterface(strippedImage)
exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, orientation.toString())
exifInterface.saveAttributes()
Pair(strippedImage.inputStream(), strippedImage.length())
} catch (e: UnsupportedFileFormatException){
strippedImage.delete()
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
val imageInputStream = try {
getApplication<PixelDroidApplication>().contentResolver.openInputStream(imageUri)!!
} catch (e: FileNotFoundException){
_uiState.update { currentUiState ->
currentUiState.copy(
userMessage = getApplication<PixelDroidApplication>().getString(R.string.file_not_found,
data.imageUri)
)
}
return
}
Pair(imageInputStream, data.size)
} catch (e: IOException){
strippedImage.delete()
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
_uiState.update { currentUiState ->
currentUiState.copy(
userMessage = getApplication<PixelDroidApplication>().getString(R.string.file_not_found,
data.imageUri)
)
}
return
}
val type = data.imageUri.getMimeType(getApplication<PixelDroidApplication>().contentResolver)
val imagePart = ProgressRequestBody(strippedOrNot, size, type)
val requestBody = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("file", System.currentTimeMillis().toString(), imagePart)
.build()
val sub = imagePart.progressSubject
.subscribeOn(Schedulers.io())
.subscribe { percentage ->
data.progress = percentage.toInt()
_uiState.update { currentUiState ->
currentUiState.copy(
uploadProgress = getPhotoData().value!!.sumOf { it.progress ?: 0 } / getPhotoData().value!!.size
)
}
}
var postSub: Disposable? = null
val description = data.imageDescription?.let { MultipartBody.Part.createFormData("description", it) }
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
val inter = api.mediaUpload(description, requestBody.parts[0])
postSub = inter
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ attachment: Attachment ->
data.progress = 0
data.uploadId = attachment.id!!
},
{ e: Throwable ->
_uiState.update { currentUiState ->
currentUiState.copy(
uploadErrorVisible = true,
uploadErrorExplanationText = if(e is HttpException){
getApplication<PixelDroidApplication>().getString(R.string.upload_error, e.code())
} else "",
uploadErrorExplanationVisible = e is HttpException,
)
}
strippedImage.delete()
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
e.printStackTrace()
postSub?.dispose()
sub.dispose()
},
{
strippedImage.delete()
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
data.progress = 100
if (getPhotoData().value!!.all { it.progress == 100 && it.uploadId != null }) {
_uiState.update { currentUiState ->
currentUiState.copy(
uploadProgressBarVisible = false,
uploadCompletedTextviewVisible = true
)
}
post()
}
postSub?.dispose()
sub.dispose()
}
)
}
}
private fun post() {
val description = uiState.value.newPostDescriptionText
_uiState.update { currentUiState ->
currentUiState.copy(
postCreationSendButtonEnabled = false
)
}
viewModelScope.launch {
try {
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
api.postStatus(
statusText = description,
media_ids = getPhotoData().value!!.mapNotNull { it.uploadId }.toList()
)
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_success),
Toast.LENGTH_SHORT).show()
val intent = Intent(getApplication(), MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
//TODO make the activity launch this instead (and surrounding toasts too)
getApplication<PixelDroidApplication>().startActivity(intent)
} catch (exception: IOException) {
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_error),
Toast.LENGTH_SHORT).show()
Log.e(TAG, exception.toString())
_uiState.update { currentUiState ->
currentUiState.copy(
postCreationSendButtonEnabled = true
)
}
} catch (exception: HttpException) {
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_failed),
Toast.LENGTH_SHORT).show()
Log.e(TAG, exception.response().toString() + exception.message().toString())
_uiState.update { currentUiState ->
currentUiState.copy(
postCreationSendButtonEnabled = true
)
}
}
}
}
fun newPostDescriptionChanged(text: Editable?) {
_uiState.update { it.copy(newPostDescriptionText = text.toString()) }
}
fun trackTempFile(file: File) {
tempFiles.add(file)
}
override fun onCleared() {
super.onCleared()
VideoEditActivity.cancelEncoding()
tempFiles.forEach {
it.delete()
}
}
}
class PostSubmissionViewModelFactory(val application: Application, val photoData: ArrayList<PhotoData>, val instance: InstanceDatabaseEntity) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.getConstructor(Application::class.java, ArrayList::class.java, InstanceDatabaseEntity::class.java).newInstance(application, photoData, instance)
}
}

View File

@ -9,6 +9,7 @@ import org.pixeldroid.app.utils.BaseFragment
import dagger.Component import dagger.Component
import org.pixeldroid.app.postCreation.PostCreationViewModel import org.pixeldroid.app.postCreation.PostCreationViewModel
import org.pixeldroid.app.profile.EditProfileViewModel import org.pixeldroid.app.profile.EditProfileViewModel
import org.pixeldroid.app.postCreation.PostSubmissionViewModel
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker
import javax.inject.Singleton import javax.inject.Singleton
@ -22,6 +23,7 @@ interface ApplicationComponent {
fun inject(notificationsWorker: NotificationsWorker) fun inject(notificationsWorker: NotificationsWorker)
fun inject(postCreationViewModel: PostCreationViewModel) fun inject(postCreationViewModel: PostCreationViewModel)
fun inject(editProfileViewModel: EditProfileViewModel) fun inject(editProfileViewModel: EditProfileViewModel)
fun inject(postSubmissionViewModel: PostSubmissionViewModel)
val context: Context? val context: Context?
val application: Application? val application: Application?

View File

@ -6,60 +6,6 @@
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".postCreation.PostCreationActivity"> tools:context=".postCreation.PostCreationActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/upload_error"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="2dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible">
<TextView
android:id="@+id/upload_error_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/colorError"
android:text="@string/media_upload_failed"
android:textColor="?attr/colorOnError"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:drawableStartCompat="@drawable/cloud_off_24"
app:drawableTint="?attr/colorOnError" />
<TextView
android:id="@+id/upload_error_text_explanation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/colorError"
tools:text="Error code returned by server: 413"
android:textColor="?attr/colorOnError"
android:textSize="20sp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/upload_error_text_view"
tools:visibility="visible" />
<Button
android:id="@+id/retry_upload_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/retry"
app:layout_constraintEnd_toEndOf="@id/upload_error_text_view"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="@id/upload_error_text_view"
app:layout_constraintTop_toBottomOf="@+id/upload_error_text_explanation" />
</androidx.constraintlayout.widget.ConstraintLayout>
<org.pixeldroid.app.postCreation.carousel.ImageCarousel <org.pixeldroid.app.postCreation.carousel.ImageCarousel
android:id="@+id/carousel" android:id="@+id/carousel"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -68,20 +14,15 @@
app:layout_constraintBottom_toBottomOf="@+id/uploadProgressBar" app:layout_constraintBottom_toBottomOf="@+id/uploadProgressBar"
app:layout_constraintTop_toTopOf="parent"/> app:layout_constraintTop_toTopOf="parent"/>
<TextView <ProgressBar
android:id="@+id/upload_completed_textview" android:id="@+id/uploadProgressBar"
android:layout_width="wrap_content" style="?android:attr/progressBarStyleHorizontal"
android:layout_height="0dp" android:layout_width="0dp"
android:layout_marginEnd="8dp" android:layout_height="wrap_content"
android:text="@string/media_upload_completed"
android:textColor="@android:color/holo_green_light"
android:textSize="16sp"
android:visibility="invisible" android:visibility="invisible"
app:drawableStartCompat="@drawable/cloud_done_24" app:layout_constraintBottom_toTopOf="@id/buttonConstraints"
app:drawableTint="@android:color/holo_green_light"
app:layout_constraintBottom_toTopOf="@id/postTextInputLayout"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
tools:visibility="visible" /> app:layout_constraintStart_toStartOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/buttonConstraints" android:id="@+id/buttonConstraints"
@ -90,14 +31,14 @@
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/postTextInputLayout"> app:layout_constraintTop_toBottomOf="@id/carousel">
<Button <Button
android:id="@+id/post_creation_send_button" android:id="@+id/post_creation_send_button"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:enabled="true" android:enabled="true"
android:text="@string/post" android:text="@string/upload_next_step"
android:visibility="visible" android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@ -105,7 +46,7 @@
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<ProgressBar <ProgressBar
android:id="@+id/posting_progress_bar" android:id="@+id/submitting_progress_bar"
style="?android:attr/progressBarStyle" style="?android:attr/progressBarStyle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -116,39 +57,6 @@
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<ProgressBar
android:id="@+id/uploadProgressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@id/postTextInputLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/postTextInputLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/description"
app:counterEnabled="true"
android:paddingStart="15dp"
android:paddingEnd="15dp"
app:errorEnabled="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/buttonConstraints"
app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/new_post_description_input_field"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:ems="10"
android:inputType="textMultiLine" />
</com.google.android.material.textfield.TextInputLayout>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/toolbarPostCreation" android:id="@+id/toolbarPostCreation"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -198,7 +106,6 @@
app:layout_constraintStart_toEndOf="@+id/removePhotoButton" app:layout_constraintStart_toEndOf="@+id/removePhotoButton"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageButton <androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/addPhotoButton" android:id="@+id/addPhotoButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -0,0 +1,163 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".postCreation.PostSubmissionActivity">
<ImageView
android:id="@+id/profilePictureImageView"
android:layout_width="88dp"
android:layout_height="88dp"
android:layout_marginStart="20dp"
android:layout_marginTop="6dp"
android:contentDescription="@string/profile_picture"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/upload_error"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="2dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible">
<TextView
android:id="@+id/upload_error_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/colorError"
android:text="@string/media_upload_failed"
android:textColor="?attr/colorOnError"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:drawableStartCompat="@drawable/cloud_off_24"
app:drawableTint="?attr/colorOnError" />
<TextView
android:id="@+id/upload_error_text_explanation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/colorError"
tools:text="Error code returned by server: 413"
android:textColor="?attr/colorOnError"
android:textSize="20sp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/upload_error_text_view"
tools:visibility="visible" />
<Button
android:id="@+id/retry_upload_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/retry"
app:layout_constraintEnd_toEndOf="@id/upload_error_text_view"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="@id/upload_error_text_view"
app:layout_constraintTop_toBottomOf="@+id/upload_error_text_explanation" />
</androidx.constraintlayout.widget.ConstraintLayout>
<ImageView
android:id="@+id/post_preview"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/buttonConstraints"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/upload_completed_textview"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginEnd="8dp"
android:text="@string/media_upload_completed"
android:textColor="@android:color/holo_green_light"
android:textSize="16sp"
android:visibility="invisible"
app:drawableStartCompat="@drawable/cloud_done_24"
app:drawableTint="@android:color/holo_green_light"
app:layout_constraintBottom_toTopOf="@id/postTextInputLayout"
app:layout_constraintEnd_toEndOf="parent"
tools:visibility="visible" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/buttonConstraints"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/postTextInputLayout">
<Button
android:id="@+id/post_creation_send_button"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:enabled="true"
android:text="@string/post"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/posting_progress_bar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<ProgressBar
android:id="@+id/uploadProgressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@id/postTextInputLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/postTextInputLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/description"
app:counterEnabled="true"
android:paddingStart="15dp"
android:paddingEnd="15dp"
app:errorEnabled="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/buttonConstraints"
app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/new_post_description_input_field"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:ems="10"
android:inputType="textMultiLine" />
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -103,6 +103,7 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
<string name="upload_post_error">Post upload error</string> <string name="upload_post_error">Post upload error</string>
<string name="description">Description…</string> <string name="description">Description…</string>
<string name="post">Post</string> <string name="post">Post</string>
<string name="upload_next_step">Upload and go to next step</string>
<string name="add_photo">Add a photo</string> <string name="add_photo">Add a photo</string>
<string name="post_image">One of the images in the post</string> <string name="post_image">One of the images in the post</string>
<string name="switch_to_grid">Switch to grid view</string> <string name="switch_to_grid">Switch to grid view</string>