PixelDroid-App-Android/app/src/main/java/com/h/pixeldroid/posts/StatusViewHolder.kt

623 lines
25 KiB
Kotlin
Raw Normal View History

package com.h.pixeldroid.posts
import android.Manifest
import android.app.AlertDialog
import android.content.Intent
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.text.method.LinkMovementMethod
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder
import com.google.android.material.snackbar.Snackbar
import com.h.pixeldroid.R
import com.h.pixeldroid.databinding.AlbumImageViewBinding
import com.h.pixeldroid.databinding.PostFragmentBinding
2021-02-08 01:32:10 +01:00
import com.h.pixeldroid.utils.BlurHashDecoder
2020-12-30 18:13:39 +01:00
import com.h.pixeldroid.utils.ImageConverter
import com.h.pixeldroid.utils.api.PixelfedAPI
import com.h.pixeldroid.utils.api.objects.Attachment
import com.h.pixeldroid.utils.api.objects.Status
import com.h.pixeldroid.utils.api.objects.Status.Companion.POST_COMMENT_TAG
import com.h.pixeldroid.utils.api.objects.Status.Companion.POST_TAG
import com.h.pixeldroid.utils.api.objects.Status.Companion.VIEW_COMMENTS_TAG
2020-12-30 18:13:39 +01:00
import com.h.pixeldroid.utils.db.AppDatabase
import com.h.pixeldroid.utils.di.PixelfedAPIHolder
import com.karumi.dexter.Dexter
import com.karumi.dexter.listener.PermissionDeniedResponse
import com.karumi.dexter.listener.PermissionGrantedResponse
import com.karumi.dexter.listener.single.BasePermissionListener
import kotlinx.coroutines.launch
import retrofit2.HttpException
import java.io.IOException
import kotlin.math.roundToInt
/**
* View Holder for a [Status] RecyclerView list item.
*/
class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHolder(binding.root) {
private var status: Status? = null
fun bind(status: Status?, pixelfedAPI: PixelfedAPIHolder, db: AppDatabase, lifecycleScope: LifecycleCoroutineScope, displayDimensionsInPx: Pair<Int, Int>, isActivity: Boolean = false) {
this.itemView.visibility = View.VISIBLE
this.status = status
val maxImageRatio: Float = status?.media_attachments?.map {
if (it.meta?.original?.width == null || it.meta.original.height == null) {
1f
} else {
it.meta.original.width.toFloat() / it.meta.original.height.toFloat()
}
}?.maxOrNull() ?: 1f
val (displayWidth, displayHeight) = displayDimensionsInPx
2021-03-03 14:00:44 +01:00
if (displayWidth / maxImageRatio > displayHeight * 3/4f) {
binding.postPager.layoutParams.width = ((displayHeight * 3 / 4f) * maxImageRatio).roundToInt()
binding.postPager.layoutParams.height = (displayHeight * 3 / 4f).toInt()
} else {
binding.postPager.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT
binding.postPager.layoutParams.height = (displayWidth / maxImageRatio).toInt()
}
//Setup the post layout
val picRequest = Glide.with(itemView).asDrawable().fitCenter()
val user = db.userDao().getActiveUser()!!
setupPost(picRequest, user.instance_uri, isActivity)
activateButtons(pixelfedAPI, db, lifecycleScope, isActivity)
}
private fun setupPost(
request: RequestBuilder<Drawable>,
domain: String,
isActivity: Boolean
) {
//Setup username as a button that opens the profile
binding.username.apply {
text = status?.account?.getDisplayName() ?: ""
setTypeface(null, Typeface.BOLD)
2021-01-25 17:34:36 +01:00
setOnClickListener { status?.account?.openProfile(binding.root.context) }
}
binding.usernameDesc.apply {
text = status?.account?.getDisplayName() ?: ""
setTypeface(null, Typeface.BOLD)
}
binding.nlikes.apply {
2021-01-25 17:34:36 +01:00
text = status?.getNLikes(binding.root.context)
setTypeface(null, Typeface.BOLD)
}
binding.nshares.apply {
2021-01-25 17:34:36 +01:00
text = status?.getNShares(binding.root.context)
setTypeface(null, Typeface.BOLD)
}
//Convert the date to a readable string
setTextViewFromISO8601(
status?.created_at!!,
binding.postDate,
isActivity,
binding.root.context
)
binding.postDomain.text = status?.getStatusDomain(domain)
//Setup images
ImageConverter.setRoundImageFromURL(
binding.root,
status?.getProfilePicUrl(),
binding.profilePic
)
binding.profilePic.setOnClickListener { status?.account?.openProfile(binding.root.context) }
//Setup post pic only if there are media attachments
if(!status?.media_attachments.isNullOrEmpty()) {
setupPostPics(binding, request)
} else {
binding.postPager.visibility = View.GONE
2021-03-03 14:00:44 +01:00
binding.postIndicator.visibility = View.GONE
}
}
private fun setupPostPics(
binding: PostFragmentBinding,
request: RequestBuilder<Drawable>,
) {
// Standard layout
2021-03-03 14:00:44 +01:00
binding.postPager.visibility = View.VISIBLE
2021-03-03 14:00:44 +01:00
//Attach the given tabs to the view pager
binding.postPager.adapter = AlbumViewPagerAdapter(status?.media_attachments ?: emptyList(), status?.sensitive)
2021-03-03 14:00:44 +01:00
if(status?.media_attachments?.size ?: 0 > 1) {
binding.postIndicator.setViewPager(binding.postPager)
binding.postIndicator.visibility = View.VISIBLE
} else {
binding.postIndicator.visibility = View.GONE
}
2021-03-03 14:00:44 +01:00
if (status?.sensitive == true) {
setupSensitiveLayout()
} else {
// GONE is the default, but have to set it again because of how RecyclerViews work
binding.sensitiveWarning.visibility = View.GONE
}
}
2021-03-03 14:00:44 +01:00
private fun setupSensitiveLayout() {
// Set dark layout and warning message
binding.sensitiveWarning.visibility = View.VISIBLE
//binding.postPicture.colorFilter = ColorMatrixColorFilter(censorMatrix)
2021-03-03 14:00:44 +01:00
fun uncensorPicture(binding: PostFragmentBinding) {
binding.sensitiveWarning.visibility = View.GONE
(binding.postPager.adapter as AlbumViewPagerAdapter).uncensor()
}
binding.sensitiveWarning.setOnClickListener {
uncensorPicture(binding)
}
}
2020-12-29 22:14:32 +01:00
private fun setDescription(
apiHolder: PixelfedAPIHolder,
lifecycleScope: LifecycleCoroutineScope,
2020-12-29 22:14:32 +01:00
) {
2021-01-13 22:46:00 +01:00
binding.description.apply {
if (status?.content.isNullOrBlank()) {
visibility = View.GONE
} else {
2020-12-29 22:14:32 +01:00
text = parseHTMLText(
status?.content.orEmpty(),
status?.mentions,
apiHolder,
binding.root.context,
lifecycleScope
2020-12-29 22:14:32 +01:00
)
movementMethod = LinkMovementMethod.getInstance()
}
}
}
//region buttons
private fun activateButtons(
apiHolder: PixelfedAPIHolder,
db: AppDatabase,
lifecycleScope: LifecycleCoroutineScope,
isActivity: Boolean
){
//Set the special HTML text
setDescription(apiHolder, lifecycleScope)
//Activate onclickListeners
activateLiker(
apiHolder, status?.favourited ?: false, lifecycleScope
)
activateReblogger(
apiHolder, status?.reblogged ?: false, lifecycleScope
)
if(isActivity){
binding.commenter.visibility = View.INVISIBLE
}
else {
binding.commenter.setOnClickListener {
lifecycleScope.launchWhenCreated {
//Open status in activity
val intent = Intent(it.context, PostActivity::class.java)
intent.putExtra(POST_TAG, status)
intent.putExtra(POST_COMMENT_TAG, true)
it.context.startActivity(intent)
}
}
}
showComments(lifecycleScope, isActivity)
activateMoreButton(apiHolder, db, lifecycleScope)
}
private fun activateReblogger(
apiHolder: PixelfedAPIHolder,
isReblogged: Boolean,
lifecycleScope: LifecycleCoroutineScope,
) {
binding.reblogger.apply {
//Set initial button state
isChecked = isReblogged
//Activate the button
setEventListener { _, buttonState ->
2020-12-30 18:13:39 +01:00
lifecycleScope.launchWhenCreated {
val api: PixelfedAPI = apiHolder.api ?: apiHolder.setToCurrentUser()
2020-12-30 18:13:39 +01:00
if (buttonState) {
// Button is active
undoReblogPost(api)
2020-12-30 18:13:39 +01:00
} else {
// Button is inactive
reblogPost(api)
2020-12-30 18:13:39 +01:00
}
}
//show animation or not?
true
}
}
}
private suspend fun reblogPost(api: PixelfedAPI) {
//Call the api function
status?.id?.let {
2020-12-30 18:13:39 +01:00
try {
val resp = api.reblogStatus(it)
2020-12-30 18:13:39 +01:00
//Update shown share count
binding.nshares.text = resp.getNShares(binding.root.context)
binding.reblogger.isChecked = resp.reblogged!!
2020-12-30 18:13:39 +01:00
} catch (exception: HttpException) {
Log.e("RESPONSE_CODE", exception.code().toString())
binding.reblogger.isChecked = false
} catch (exception: IOException) {
Log.e("REBLOG ERROR", exception.toString())
binding.reblogger.isChecked = false
2020-12-30 18:13:39 +01:00
}
}
}
private suspend fun undoReblogPost(api: PixelfedAPI) {
//Call the api function
status?.id?.let {
2020-12-30 18:13:39 +01:00
try {
val resp = api.undoReblogStatus(it)
2020-12-30 18:13:39 +01:00
//Update shown share count
binding.nshares.text = resp.getNShares(binding.root.context)
binding.reblogger.isChecked = resp.reblogged!!
2020-12-30 18:13:39 +01:00
} catch (exception: HttpException) {
Log.e("RESPONSE_CODE", exception.code().toString())
binding.reblogger.isChecked = true
} catch (exception: IOException) {
Log.e("REBLOG ERROR", exception.toString())
binding.reblogger.isChecked = true
2020-12-30 18:13:39 +01:00
}
}
}
private fun activateMoreButton(apiHolder: PixelfedAPIHolder, db: AppDatabase, lifecycleScope: LifecycleCoroutineScope){
binding.statusMore.setOnClickListener {
PopupMenu(it.context, it).apply {
setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.post_more_menu_report -> {
val intent = Intent(it.context, ReportActivity::class.java)
intent.putExtra(Status.POST_TAG, status)
ContextCompat.startActivity(it.context, intent, null)
true
}
R.id.post_more_menu_share_link -> {
val share = Intent.createChooser(Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, status?.uri)
type = "text/plain"
putExtra(Intent.EXTRA_TITLE, status?.content)
}, null)
ContextCompat.startActivity(it.context, share, null)
true
}
R.id.post_more_menu_save_to_gallery -> {
Dexter.withContext(binding.root.context)
.withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.withListener(object : BasePermissionListener() {
override fun onPermissionDenied(p0: PermissionDeniedResponse?) {
Toast.makeText(
binding.root.context,
binding.root.context.getString(R.string.write_permission_download_pic),
Toast.LENGTH_SHORT
).show()
}
override fun onPermissionGranted(p0: PermissionGrantedResponse?) {
status?.downloadImage(
binding.root.context,
status?.media_attachments?.get(binding.postPager.currentItem)?.url
?: "",
binding.root
)
}
}).check()
true
}
R.id.post_more_menu_share_picture -> {
Dexter.withContext(binding.root.context)
.withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.withListener(object : BasePermissionListener() {
override fun onPermissionDenied(p0: PermissionDeniedResponse?) {
Toast.makeText(
binding.root.context,
binding.root.context.getString(R.string.write_permission_share_pic),
Toast.LENGTH_SHORT
).show()
}
override fun onPermissionGranted(p0: PermissionGrantedResponse?) {
status?.downloadImage(
binding.root.context,
status?.media_attachments?.get(binding.postPager.currentItem)?.url
?: "",
binding.root,
share = true,
)
}
}).check()
true
}
R.id.post_more_menu_delete -> {
val builder = AlertDialog.Builder(binding.root.context)
builder.apply {
setMessage(R.string.delete_dialog)
setPositiveButton(android.R.string.ok) { _, _ ->
lifecycleScope.launch {
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 {
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
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()
}
}
}
}
setNegativeButton(android.R.string.cancel) { _, _ -> }
show()
}
true
}
else -> false
}
}
inflate(R.menu.post_more_menu)
if(status?.media_attachments.isNullOrEmpty()) {
//make sure to disable image-related things if there aren't any
menu.setGroupVisible(R.id.post_more_group_picture, false)
}
if(status?.account?.id == db.userDao().getActiveUser()!!.user_id){
2020-12-19 21:13:30 +01:00
// Enable deleting post if it's the user's
menu.setGroupVisible(R.id.post_more_menu_group_delete, true)
2020-12-19 21:13:30 +01:00
// And disable reporting your own post (just delete it if you don't like it :P)
menu.setGroupVisible(R.id.post_more_menu_group_report, false)
}
show()
}
}
}
private fun activateLiker(
apiHolder: PixelfedAPIHolder,
isLiked: Boolean,
lifecycleScope: LifecycleCoroutineScope,
) {
binding.liker.apply {
//Set initial state
isChecked = isLiked
//Activate the liker
setEventListener { _, buttonState ->
2020-12-30 18:13:39 +01:00
lifecycleScope.launchWhenCreated {
val api: PixelfedAPI = apiHolder.api ?: apiHolder.setToCurrentUser()
2020-12-30 18:13:39 +01:00
if (buttonState) {
// Button is active, unlike
unLikePostCall(api)
2020-12-30 18:13:39 +01:00
} else {
// Button is inactive, like
likePostCall(api)
2020-12-30 18:13:39 +01:00
}
}
//show animation or not?
true
}
}
2020-12-30 18:13:39 +01:00
//Activate double tap liking
var clicked = false
2021-03-26 10:26:00 +01:00
binding.postPagerHost.doubleTapCallback = {
lifecycleScope.launchWhenCreated {
//Check that the post isn't hidden
if(binding.sensitiveWarning.visibility == View.GONE) {
//Check for double click
if(clicked) {
val api: PixelfedAPI = apiHolder.api ?: apiHolder.setToCurrentUser()
if (binding.liker.isChecked) {
// Button is active, unlike
binding.liker.isChecked = false
unLikePostCall(api)
2020-12-30 18:13:39 +01:00
} else {
// Button is inactive, like
binding.liker.playAnimation()
binding.liker.isChecked = true
likePostCall(api)
2020-12-30 18:13:39 +01:00
}
} else {
clicked = true
//Reset clicked to false after 500ms
2021-03-03 14:00:44 +01:00
binding.postPager.handler.postDelayed(fun() { clicked = false }, 500)
2020-12-30 18:13:39 +01:00
}
}
2020-12-30 18:13:39 +01:00
}
}
}
private suspend fun likePostCall(api: PixelfedAPI) {
//Call the api function
status?.id?.let {
2020-12-30 18:13:39 +01:00
try {
val resp = api.likePost(it)
2020-12-30 18:13:39 +01:00
//Update shown like count and internal like toggle
binding.nlikes.text = resp.getNLikes(binding.root.context)
binding.liker.isChecked = resp.favourited ?: false
2020-12-30 18:13:39 +01:00
} catch (exception: IOException) {
Log.e("LIKE ERROR", exception.toString())
binding.liker.isChecked = false
2020-12-30 18:13:39 +01:00
} catch (exception: HttpException) {
Log.e("RESPONSE_CODE", exception.code().toString())
binding.liker.isChecked = false
2020-12-30 18:13:39 +01:00
}
}
}
private suspend fun unLikePostCall(api: PixelfedAPI) {
//Call the api function
status?.id?.let {
2020-12-30 18:13:39 +01:00
try {
val resp = api.unlikePost(it)
2020-12-30 18:13:39 +01:00
//Update shown like count and internal like toggle
binding.nlikes.text = resp.getNLikes(binding.root.context)
binding.liker.isChecked = resp.favourited ?: false
2020-12-30 18:13:39 +01:00
} catch (exception: IOException) {
Log.e("UNLIKE ERROR", exception.toString())
binding.liker.isChecked = true
2020-12-30 18:13:39 +01:00
} catch (exception: HttpException) {
Log.e("RESPONSE_CODE", exception.code().toString())
binding.liker.isChecked = true
2020-12-30 18:13:39 +01:00
}
}
}
//endregion
private fun showComments(
lifecycleScope: LifecycleCoroutineScope,
isActivity: Boolean
) {
//Show number of comments on the post
if (status?.replies_count == 0) {
binding.viewComments.text = binding.root.context.getString(R.string.NoCommentsToShow)
} else {
binding.viewComments.apply {
2021-01-25 11:52:27 +01:00
text = resources.getQuantityString(R.plurals.number_comments,
status?.replies_count ?: 0,
status?.replies_count ?: 0
)
if(!isActivity) {
setOnClickListener {
lifecycleScope.launchWhenCreated {
//Open status in activity
val intent = Intent(context, PostActivity::class.java)
intent.putExtra(POST_TAG, status)
intent.putExtra(VIEW_COMMENTS_TAG, true)
context.startActivity(intent)
}
2020-12-30 18:13:39 +01:00
}
}
}
}
}
2020-12-29 22:35:45 +01:00
companion object {
fun create(parent: ViewGroup): StatusViewHolder {
val itemBinding = PostFragmentBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return StatusViewHolder(itemBinding)
}
}
}
2021-03-03 14:00:44 +01:00
private class AlbumViewPagerAdapter(private val media_attachments: List<Attachment>, private var sensitive: Boolean?) :
RecyclerView.Adapter<AlbumViewPagerAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val itemBinding = AlbumImageViewBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return ViewHolder(itemBinding)
}
override fun getItemCount() = media_attachments.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
2021-03-03 14:00:44 +01:00
media_attachments[position].apply {
val blurhashBitMap = blurhash?.let {
BlurHashDecoder.blurHashBitmap(
holder.binding.root.resources,
it,
meta?.original?.width,
meta?.original?.height
2021-02-08 01:32:10 +01:00
)
2021-03-03 14:00:44 +01:00
}
if (sensitive == false) {
Glide.with(holder.binding.root)
.asDrawable().fitCenter()
.placeholder(blurhashBitMap)
.load(url).into(holder.image)
} else {
Glide.with(holder.binding.root)
.asDrawable().fitCenter()
.load(blurhashBitMap).into(holder.image)
}
2021-03-03 14:00:44 +01:00
val description = description
.orEmpty()
.ifEmpty { holder.binding.root.context.getString(R.string.no_description) }
2021-03-03 14:00:44 +01:00
holder.image.setOnLongClickListener {
Snackbar.make(it, description, Snackbar.LENGTH_SHORT).show()
true
}
holder.image.contentDescription = description
}
2021-03-03 14:00:44 +01:00
}
2021-03-03 14:00:44 +01:00
fun uncensor(){
sensitive = false
notifyDataSetChanged()
}
class ViewHolder(val binding: AlbumImageViewBinding) : RecyclerView.ViewHolder(binding.root){
val image: ImageView = binding.imageImageView
}
}