Merge branch 'redraft' into 'master'

Delete and Redraft

See merge request pixeldroid/PixelDroid!483
This commit is contained in:
Matthieu 2022-10-27 13:04:34 +00:00
commit 9bcf587f49
7 changed files with 314 additions and 35 deletions

View File

@ -64,6 +64,12 @@ data class PhotoData(
class PostCreationActivity : BaseThemedWithoutBarActivity() { class PostCreationActivity : BaseThemedWithoutBarActivity() {
companion object {
internal const val PICTURE_DESCRIPTION = "picture_description"
internal const val TEMP_FILES = "temp_files"
internal const val POST_REDRAFT = "post_redraft"
}
private var user: UserDatabaseEntity? = null private var user: UserDatabaseEntity? = null
private lateinit var instance: InstanceDatabaseEntity private lateinit var instance: InstanceDatabaseEntity
@ -87,7 +93,13 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
} }
} ?: InstanceDatabaseEntity("", "") } ?: InstanceDatabaseEntity("", "")
val _model: PostCreationViewModel by viewModels { PostCreationViewModelFactory(application, intent.clipData!!, instance) } val _model: PostCreationViewModel by viewModels {
PostCreationViewModelFactory(
application,
intent.clipData!!,
instance
)
}
model = _model model = _model
model.getPhotoData().observe(this) { newPhotoData -> model.getPhotoData().observe(this) { newPhotoData ->
@ -99,9 +111,6 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
) )
} }
//Get initial text value from model (for template)
binding.newPostDescriptionInputField.setText(model.uiState.value.newPostDescriptionText)
lifecycleScope.launch { lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { repeatOnLifecycle(Lifecycle.State.STARTED) {
model.uiState.collect { uiState -> model.uiState.collect { uiState ->
@ -116,15 +125,20 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
} }
binding.addPhotoButton.isEnabled = uiState.addPhotoButtonEnabled binding.addPhotoButton.isEnabled = uiState.addPhotoButtonEnabled
enableButton(uiState.postCreationSendButtonEnabled) enableButton(uiState.postCreationSendButtonEnabled)
binding.uploadProgressBar.visibility = if(uiState.uploadProgressBarVisible) VISIBLE else INVISIBLE binding.uploadProgressBar.visibility =
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.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.uploadError.visibility =
binding.uploadErrorTextExplanation.visibility = if(uiState.uploadErrorExplanationVisible) VISIBLE else INVISIBLE if (uiState.uploadErrorVisible) VISIBLE else INVISIBLE
binding.uploadErrorTextExplanation.visibility =
if (uiState.uploadErrorExplanationVisible) VISIBLE else INVISIBLE
binding.toolbarPostCreation.visibility = if(uiState.isCarousel) VISIBLE else INVISIBLE binding.toolbarPostCreation.visibility =
if (uiState.isCarousel) VISIBLE else INVISIBLE
binding.carousel.layoutCarousel = uiState.isCarousel binding.carousel.layoutCarousel = uiState.isCarousel
@ -155,6 +169,15 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
binding.newPostDescriptionInputField.doAfterTextChanged { binding.newPostDescriptionInputField.doAfterTextChanged {
model.newPostDescriptionChanged(binding.newPostDescriptionInputField.text) 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.postTextInputLayout.counterMaxLength = instance.maxStatusChars
binding.carousel.apply { binding.carousel.apply {
@ -201,6 +224,30 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
model.cancelEncode(currentPosition) model.cancelEncode(currentPosition)
} }
} }
// 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 val addPhotoResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> private val addPhotoResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->

View File

@ -1,6 +1,5 @@
package org.pixeldroid.app.postCreation package org.pixeldroid.app.postCreation
import android.R.attr.orientation
import android.app.Application import android.app.Application
import android.content.ClipData import android.content.ClipData
import android.content.Intent import android.content.Intent
@ -144,10 +143,10 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
} }
} }
for (i in 0 until count) { for (i in 0 until count) {
clipData.getItemAt(i).uri.let { clipData.getItemAt(i).let {
val sizeAndVideoPair: Pair<Long, Boolean> = val sizeAndVideoPair: Pair<Long, Boolean> =
getSizeAndVideoValidate(it, (previousList?.size ?: 0) + dataToAdd.size + 1) getSizeAndVideoValidate(it.uri, (previousList?.size ?: 0) + dataToAdd.size + 1)
dataToAdd.add(PhotoData(imageUri = it, size = sizeAndVideoPair.first, video = sizeAndVideoPair.second)) dataToAdd.add(PhotoData(imageUri = it.uri, size = sizeAndVideoPair.first, video = sizeAndVideoPair.second, imageDescription = it.text?.toString()))
} }
} }
return previousList?.plus(dataToAdd)?.toMutableList() ?: mutableListOf() return previousList?.plus(dataToAdd)?.toMutableList() ?: mutableListOf()

View File

@ -3,11 +3,14 @@ package org.pixeldroid.app.posts
import android.Manifest import android.Manifest
import android.app.Activity import android.app.Activity
import android.app.AlertDialog import android.app.AlertDialog
import android.content.ClipData
import android.content.Intent import android.content.Intent
import android.graphics.Typeface import android.graphics.Typeface
import android.graphics.drawable.AnimatedVectorDrawable import android.graphics.drawable.AnimatedVectorDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.location.GnssAntennaInfo.Listener
import android.net.Uri import android.net.Uri
import android.os.Looper
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
@ -37,10 +40,17 @@ import com.karumi.dexter.listener.PermissionDeniedResponse
import com.karumi.dexter.listener.PermissionGrantedResponse import com.karumi.dexter.listener.PermissionGrantedResponse
import com.karumi.dexter.listener.single.BasePermissionListener import com.karumi.dexter.listener.single.BasePermissionListener
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import okhttp3.*
import okio.BufferedSink
import okio.buffer
import okio.sink
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.AlbumImageViewBinding import org.pixeldroid.app.databinding.AlbumImageViewBinding
import org.pixeldroid.app.databinding.OpenedAlbumBinding import org.pixeldroid.app.databinding.OpenedAlbumBinding
import org.pixeldroid.app.databinding.PostFragmentBinding import org.pixeldroid.app.databinding.PostFragmentBinding
import org.pixeldroid.app.postCreation.PostCreationActivity
import org.pixeldroid.app.postCreation.photoEdit.PhotoEditActivity
import org.pixeldroid.app.posts.MediaViewerActivity.Companion.openActivity import org.pixeldroid.app.posts.MediaViewerActivity.Companion.openActivity
import org.pixeldroid.app.utils.BlurHashDecoder import org.pixeldroid.app.utils.BlurHashDecoder
import org.pixeldroid.app.utils.api.PixelfedAPI import org.pixeldroid.app.utils.api.PixelfedAPI
@ -55,6 +65,8 @@ import org.pixeldroid.app.utils.setProfileImageFromURL
import retrofit2.HttpException import retrofit2.HttpException
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.net.URI
import java.util.concurrent.atomic.AtomicInteger
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -337,7 +349,10 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
} catch (exception: HttpException) { } catch (exception: HttpException) {
Toast.makeText( Toast.makeText(
binding.root.context, binding.root.context,
binding.root.context.getString(R.string.bookmark_post_failed_error, exception.code()), binding.root.context.getString(
R.string.bookmark_post_failed_error,
exception.code()
),
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} catch (exception: IOException) { } catch (exception: IOException) {
@ -442,28 +457,195 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
setPositiveButton(android.R.string.ok) { _, _ -> setPositiveButton(android.R.string.ok) { _, _ ->
lifecycleScope.launch { lifecycleScope.launch {
val user = db.userDao().getActiveUser()!! deletePost(apiHolder.api ?: apiHolder.setToCurrentUser(), db)
status?.id?.let { id -> }
db.homePostDao().delete(id, user.user_id, user.instance_uri) }
db.publicPostDao().delete(id, user.user_id, user.instance_uri) setNegativeButton(android.R.string.cancel) { _, _ -> }
show()
}
true
}
R.id.post_more_menu_redraft -> {
val builder = AlertDialog.Builder(binding.root.context)
builder.apply {
setMessage(R.string.redraft_dialog_launch)
setPositiveButton(android.R.string.ok) { _, _ ->
lifecycleScope.launch {
try { try {
val api = apiHolder.api ?: apiHolder.setToCurrentUser() // Create new post creation activity
api.deleteStatus(id) val intent =
binding.root.visibility = View.GONE Intent(context, PostCreationActivity::class.java)
// Get descriptions and images from original post
val postDescription = status?.content ?: ""
val postAttachments =
status?.media_attachments!! // Catch possible exception from !! (?)
val imageUris: MutableList<Uri> = mutableListOf()
val imageNames: MutableList<String> = mutableListOf()
val imageDescriptions: MutableList<String> =
mutableListOf()
for (currentAttachment in postAttachments) {
val imageUri = currentAttachment.url ?: ""
val imageName =
Uri.parse(imageUri).lastPathSegment.toString()
val imageDescription =
currentAttachment.description ?: ""
val downloadedFile =
File(context.cacheDir, imageName)
val downloadedUri = Uri.fromFile(downloadedFile)
imageUris.add(downloadedUri)
imageNames.add(imageName)
imageDescriptions.add(imageDescription)
}
val counter = AtomicInteger(0)
// Define callback function for after downloading the images
fun continuation() {
// Wait for all outstanding downloads to finish
if (counter.incrementAndGet() == imageUris.size) {
if (allFilesExist(imageNames)) {
val counterInt = counter.get()
Toast.makeText(
binding.root.context,
binding.root.context.resources.getQuantityString(
R.plurals.items_load_success,
counterInt,
counterInt
),
Toast.LENGTH_SHORT
).show()
// Pass downloaded images to new post creation activity
intent.apply {
assert(imageUris.size == imageDescriptions.size)
for (i in 0 until imageUris.size) {
val imageUri = imageUris[i]
val imageDescription =
fromHtml(imageDescriptions[i]).toString()
val imageItem = ClipData.Item(
imageDescription,
null,
imageUri
)
if (clipData == null) {
clipData = ClipData(
"",
emptyArray(),
imageItem
)
} else {
clipData!!.addItem(imageItem)
}
}
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
// Pass post description of existing post to new post creation activity
intent.putExtra(
PostCreationActivity.PICTURE_DESCRIPTION,
fromHtml(postDescription).toString()
)
if (imageNames.isNotEmpty()) {
intent.putExtra(
PostCreationActivity.TEMP_FILES,
imageNames.toTypedArray()
)
}
intent.putExtra(
PostCreationActivity.POST_REDRAFT,
true
)
// Launch post creation activity
binding.root.context.startActivity(intent)
}
}
}
if (!allFilesExist(imageNames)) {
// Track download progress
Toast.makeText(
binding.root.context,
binding.root.context.getString(R.string.image_download_downloading),
Toast.LENGTH_SHORT
).show()
}
// Iterate through all pictures of the original post
for (currentAttachment in postAttachments) {
val imageUri = currentAttachment.url ?: ""
val imageName =
Uri.parse(imageUri).lastPathSegment.toString()
val downloadedFile =
File(context.cacheDir, imageName)
val downloadRequest: Request =
Request.Builder().url(imageUri).build()
// Check whether image is in cache directory already (maybe rather do so using Glide in the future?)
if (!downloadedFile.exists()) {
OkHttpClient().newCall(downloadRequest)
.enqueue(object : Callback {
override fun onFailure(
call: Call,
e: IOException
) {
Looper.prepare()
downloadedFile.delete()
Toast.makeText(
binding.root.context,
binding.root.context.getString(R.string.redraft_post_failed_io_except),
Toast.LENGTH_SHORT
).show()
}
@Throws(IOException::class)
override fun onResponse(
call: Call,
response: Response
) {
val sink: BufferedSink =
downloadedFile.sink().buffer()
sink.writeAll(response.body!!.source())
sink.close()
Looper.prepare()
continuation()
}
})
} else {
continuation()
}
}
} catch (exception: HttpException) { } catch (exception: HttpException) {
Toast.makeText( Toast.makeText(
binding.root.context, binding.root.context,
binding.root.context.getString(R.string.delete_post_failed_error, exception.code()), binding.root.context.getString(
R.string.redraft_post_failed_error,
exception.code()
),
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} catch (exception: IOException) { } catch (exception: IOException) {
Toast.makeText( Toast.makeText(
binding.root.context, binding.root.context,
binding.root.context.getString(R.string.delete_post_failed_io_except), binding.root.context.getString(R.string.redraft_post_failed_io_except),
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
}
// Delete original post
deletePost(
apiHolder.api ?: apiHolder.setToCurrentUser(),
db
)
} }
} }
setNegativeButton(android.R.string.cancel) { _, _ -> } setNegativeButton(android.R.string.cancel) { _, _ -> }
@ -546,6 +728,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
status?.media_attachments?.let { binding.postPagerHost.images = ArrayList(it) } status?.media_attachments?.let { binding.postPagerHost.images = ArrayList(it) }
} }
private fun ImageView.animateView() { private fun ImageView.animateView() {
visibility = View.VISIBLE visibility = View.VISIBLE
when (val drawable = drawable) { when (val drawable = drawable) {
@ -627,8 +810,39 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
} }
} }
private suspend fun deletePost(api: PixelfedAPI, db: AppDatabase) {
val user = db.userDao().getActiveUser()!!
status?.id?.let { id ->
db.homePostDao().delete(id, user.user_id, user.instance_uri)
db.publicPostDao().delete(id, user.user_id, user.instance_uri)
try {
api.deleteStatus(id)
binding.root.visibility = View.GONE
} catch (exception: HttpException) {
Toast.makeText(
binding.root.context,
binding.root.context.getString(R.string.delete_post_failed_error, exception.code()),
Toast.LENGTH_SHORT
).show()
} catch (exception: IOException) {
Toast.makeText(
binding.root.context,
binding.root.context.getString(R.string.delete_post_failed_io_except),
Toast.LENGTH_SHORT
).show()
}
}
}
private fun allFilesExist(listOfNames: MutableList<String>): Boolean {
for (name in listOfNames) {
val file = File(binding.root.context.cacheDir, name)
if (!file.exists()) {
return false
}
}
return true
}
companion object { companion object {
fun create(parent: ViewGroup): StatusViewHolder { fun create(parent: ViewGroup): StatusViewHolder {

View File

@ -32,6 +32,10 @@
android:id="@+id/post_more_menu_group_delete" android:id="@+id/post_more_menu_group_delete"
android:visible="false"> android:visible="false">
<item android:id="@+id/post_more_menu_redraft"
android:title="@string/redraft" />
<item android:id="@+id/post_more_menu_delete" <item android:id="@+id/post_more_menu_delete"
android:title="@string/delete"/> android:title="@string/delete"/>

View File

@ -148,6 +148,10 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
<string name="image_download_failed">Download has failed, please try again</string> <string name="image_download_failed">Download has failed, please try again</string>
<string name="image_download_downloading">Downloading…</string> <string name="image_download_downloading">Downloading…</string>
<string name="image_download_success">Image downloaded successfully</string> <string name="image_download_success">Image downloaded successfully</string>
<plurals name="items_load_success">
<item quantity="one">%d item loaded successfully</item>
<item quantity="other">%d items loaded successfully</item>
</plurals>
<!-- Post attributes --> <!-- Post attributes -->
<string name="no_description">No description</string> <string name="no_description">No description</string>
<plurals name="likes"> <plurals name="likes">
@ -246,12 +250,17 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
<string name="discover_no_infinite_load">Discover doesn\'t load infinitely. Pull to refresh for other images.</string> <string name="discover_no_infinite_load">Discover doesn\'t load infinitely. Pull to refresh for other images.</string>
<string name="something_went_wrong">Something went wrong…</string> <string name="something_went_wrong">Something went wrong…</string>
<string name="panda_pull_to_refresh_to_try_again">This panda is not happy. Pull to refresh to try again.</string> <string name="panda_pull_to_refresh_to_try_again">This panda is not happy. Pull to refresh to try again.</string>
<string name="redraft">Redraft</string>
<string name="redraft_dialog_launch">Redrafting this post will allow you to edit the photo and its description, but it will delete all current comments and likes. Continue?</string>
<string name="redraft_dialog_cancel">If you cancel this redraft, the original post will no longer be on your account. Continue without reposting?</string>
<string name="delete">Delete</string> <string name="delete">Delete</string>
<string name="delete_dialog">Delete this post?</string> <string name="delete_dialog">Delete this post?</string>
<string name="language">Language</string> <string name="language">Language</string>
<string name="help_translate">Help translate PixelDroid to your language:</string> <string name="help_translate">Help translate PixelDroid to your language:</string>
<string name="issues_contribute">Report issues or contribute to the application:</string> <string name="issues_contribute">Report issues or contribute to the application:</string>
<string name="mascot_description">Image showing a red panda, Pixelfed\'s mascot, using a phone</string> <string name="mascot_description">Image showing a red panda, Pixelfed\'s mascot, using a phone</string>
<string name="redraft_post_failed_error">Could not redraft the post, error %1$d</string>
<string name="redraft_post_failed_io_except">Could not redraft the post, check your connection?</string>
<string name="delete_post_failed_error">Could not delete the post, error %1$d</string> <string name="delete_post_failed_error">Could not delete the post, error %1$d</string>
<string name="delete_post_failed_io_except">Could not delete the post, check your connection?</string> <string name="delete_post_failed_io_except">Could not delete the post, check your connection?</string>
<string name="bookmark_post_failed_error">Could not (un)bookmark the post, error %1$d</string> <string name="bookmark_post_failed_error">Could not (un)bookmark the post, error %1$d</string>

View File

@ -7,7 +7,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:8.0.0-alpha05' classpath 'com.android.tools.build:gradle:8.0.0-alpha06'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong

View File

@ -3463,6 +3463,9 @@
<artifact name="aapt2-7.3.0-rc01-8691043-linux.jar"> <artifact name="aapt2-7.3.0-rc01-8691043-linux.jar">
<sha256 value="28b7445d32544169c8c31fba72fb63e067c3608e1c61ea78f2fa89cde38794fa" origin="Generated by Gradle"/> <sha256 value="28b7445d32544169c8c31fba72fb63e067c3608e1c61ea78f2fa89cde38794fa" origin="Generated by Gradle"/>
</artifact> </artifact>
<artifact name="aapt2-7.3.0-rc01-8691043-osx.jar">
<sha256 value="c1aa071d7284bb771c93037494547f772aefae18da4ad7af1523a5b7ab4d915c" origin="Generated by Gradle"/>
</artifact>
<artifact name="aapt2-7.3.0-rc01-8691043.pom"> <artifact name="aapt2-7.3.0-rc01-8691043.pom">
<sha256 value="360cf64368acc9f9b1c0d0c9a82488a045e2780a5f7bfc16a57bfcc0d40cf200" origin="Generated by Gradle"/> <sha256 value="360cf64368acc9f9b1c0d0c9a82488a045e2780a5f7bfc16a57bfcc0d40cf200" origin="Generated by Gradle"/>
</artifact> </artifact>
@ -3495,6 +3498,9 @@
<artifact name="aapt2-8.0.0-alpha06-8841542-linux.jar"> <artifact name="aapt2-8.0.0-alpha06-8841542-linux.jar">
<sha256 value="3e6c747be8625506466585c3a327e7145a77d84796f6ec387eb855f9d4bd572e" origin="Generated by Gradle"/> <sha256 value="3e6c747be8625506466585c3a327e7145a77d84796f6ec387eb855f9d4bd572e" origin="Generated by Gradle"/>
</artifact> </artifact>
<artifact name="aapt2-8.0.0-alpha06-8841542-osx.jar">
<sha256 value="14b13c019a48aba57dbf6c5d315a4cafe1e502c22d0179124d16e9f7dc847f64" origin="Generated by Gradle"/>
</artifact>
<artifact name="aapt2-8.0.0-alpha06-8841542.pom"> <artifact name="aapt2-8.0.0-alpha06-8841542.pom">
<sha256 value="b9f6acccffdb833c1203650356699098b9dd9d9b0012d86b73bced1455f352de" origin="Generated by Gradle"/> <sha256 value="b9f6acccffdb833c1203650356699098b9dd9d9b0012d86b73bced1455f352de" origin="Generated by Gradle"/>
</artifact> </artifact>