More coroutinification

This commit is contained in:
Matthieu 2020-12-29 22:14:32 +01:00
parent 3a91b02e55
commit 1254a3566d
9 changed files with 276 additions and 216 deletions

View File

@ -12,10 +12,13 @@ import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.core.text.toSpanned
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleCoroutineScope
import com.h.pixeldroid.R
import com.h.pixeldroid.utils.api.PixelfedAPI
import com.h.pixeldroid.utils.api.objects.Account.Companion.getAccountFromId
import com.h.pixeldroid.utils.api.objects.Account.Companion.openAccountFromId
import com.h.pixeldroid.utils.api.objects.Mention
import kotlinx.coroutines.coroutineScope
import java.net.URI
import java.net.URISyntaxException
import java.text.ParseException
@ -51,7 +54,8 @@ fun parseHTMLText(
mentions: List<Mention>?,
api : PixelfedAPI,
context: Context,
credential: String
credential: String,
lifecycleScope: LifecycleCoroutineScope
) : Spanned {
//Convert text to spannable
val content = fromHtml(text)
@ -103,7 +107,9 @@ fun parseHTMLText(
override fun onClick(widget: View) {
Log.e("MENTION", "CLICKED")
//Retrieve the account for the given profile
getAccountFromId(accountId, api, context, credential)
lifecycleScope.launchWhenCreated {
openAccountFromId(accountId, api, context, credential)
}
}
}
}

View File

@ -3,6 +3,7 @@ package com.h.pixeldroid.posts
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.lifecycle.lifecycleScope
import com.h.pixeldroid.R
import com.h.pixeldroid.utils.api.objects.Report
import com.h.pixeldroid.utils.api.objects.Status
@ -10,7 +11,9 @@ import com.h.pixeldroid.utils.BaseActivity
import kotlinx.android.synthetic.main.activity_report.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.HttpException
import retrofit2.Response
import java.io.IOException
class ReportActivity : BaseActivity() {
@ -37,33 +40,34 @@ class ReportActivity : BaseActivity() {
val accessToken = user?.accessToken.orEmpty()
val api = apiHolder.api ?: apiHolder.setDomainToCurrentUser(db)
api.report("Bearer $accessToken", status?.account?.id!!, listOf(status), textInputLayout.editText?.text.toString())
.enqueue(object : Callback<Report> {
override fun onResponse(
call: Call<Report>,
response: Response<Report>
) {
if (response.body() == null || !response.isSuccessful) {
textInputLayout.error = getString(R.string.report_error)
reportButton.visibility = View.VISIBLE
textInputLayout.editText?.isEnabled = true
reportProgressBar.visibility = View.GONE
} else {
reportProgressBar.visibility = View.GONE
reportButton.isEnabled = false
reportButton.text = getString(R.string.reported)
reportButton.visibility = View.VISIBLE
}
}
override fun onFailure(call: Call<Report>, t: Throwable) {
Log.e("REPORT:", t.toString())
}
})
lifecycleScope.launchWhenCreated {
try {
api.report("Bearer $accessToken", status?.account?.id!!, listOf(status), textInputLayout.editText?.text.toString())
reportStatus(true)
} catch (exception: IOException) {
reportStatus(false)
} catch (exception: HttpException) {
reportStatus(false)
}
}
}
}
private fun reportStatus(success: Boolean){
if(success){
reportProgressBar.visibility = View.GONE
reportButton.isEnabled = false
reportButton.text = getString(R.string.reported)
reportButton.visibility = View.VISIBLE
} else {
textInputLayout.error = getString(R.string.report_error)
reportButton.visibility = View.VISIBLE
textInputLayout.editText?.isEnabled = true
reportProgressBar.visibility = View.GONE
}
}
override fun onSupportNavigateUp(): Boolean {
onBackPressed()

View File

@ -206,12 +206,24 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
}.attach()
}
private fun setDescription(rootView: View, api: PixelfedAPI, credential: String) {
private fun setDescription(
rootView: View,
api: PixelfedAPI,
credential: String,
lifecycleScope: LifecycleCoroutineScope
) {
rootView.findViewById<TextView>(R.id.description).apply {
if (status?.content.isNullOrBlank()) {
visibility = View.GONE
} else {
text = parseHTMLText(status?.content.orEmpty(), status?.mentions, api, rootView.context, credential)
text = parseHTMLText(
status?.content.orEmpty(),
status?.mentions,
api,
rootView.context,
credential,
lifecycleScope
)
movementMethod = LinkMovementMethod.getInstance()
}
}
@ -222,7 +234,7 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
val credential = "Bearer ${user.accessToken}"
//Set the special HTML text
setDescription(holder.view, api, credential)
setDescription(holder.view, api, credential, lifecycleScope)
//Activate onclickListeners
activateLiker(

View File

@ -10,7 +10,9 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.paging.ExperimentalPagingApi
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
@ -53,7 +55,10 @@ class NotificationsFragment : CachedFeedFragment<Notification>() {
// get the view model
@Suppress("UNCHECKED_CAST")
viewModel = ViewModelProvider(this, ViewModelFactory(db, db.notificationDao(), NotificationsRemoteMediator(apiHolder, db)))
viewModel = ViewModelProvider(
this,
ViewModelFactory(db, db.notificationDao(), NotificationsRemoteMediator(apiHolder, db))
)
.get(FeedViewModel::class.java) as FeedViewModel<Notification>
launch()
@ -62,149 +67,201 @@ class NotificationsFragment : CachedFeedFragment<Notification>() {
return view
}
}
/**
* View Holder for a [Notification] RecyclerView list item.
*/
class NotificationViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val notificationType: TextView = view.notification_type
private val notificationTime: TextView = view.notification_time
private val postDescription: TextView = view.notification_post_description
private val avatar: ImageView = view.notification_avatar
private val photoThumbnail: ImageView = view.notification_photo_thumbnail
/**
* View Holder for a [Notification] RecyclerView list item.
*/
class NotificationViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val notificationType: TextView = view.notification_type
private val notificationTime: TextView = view.notification_time
private val postDescription: TextView = view.notification_post_description
private val avatar: ImageView = view.notification_avatar
private val photoThumbnail: ImageView = view.notification_photo_thumbnail
private var notification: Notification? = null
private var notification: Notification? = null
init {
itemView.setOnClickListener {
init {
itemView.setOnClickListener {
notification?.openActivity()
}
}
private fun Notification.openActivity() {
val intent: Intent =
when (type){
Notification.NotificationType.mention, Notification.NotificationType.favourite,
Notification.NotificationType.poll, Notification.NotificationType.reblog -> {
openPostFromNotification()
private fun Notification.openActivity() {
val intent: Intent =
when (type) {
Notification.NotificationType.mention, Notification.NotificationType.favourite,
Notification.NotificationType.poll, Notification.NotificationType.reblog -> {
openPostFromNotification()
}
Notification.NotificationType.follow -> {
Intent(itemView.context, ProfileActivity::class.java).apply {
putExtra(Account.ACCOUNT_TAG, account)
}
}
null -> return //TODO show an error here?
}
itemView.context.startActivity(intent)
}
private fun Notification.openPostFromNotification(): Intent =
Intent(itemView.context, PostActivity::class.java).apply {
putExtra(Status.POST_TAG, status)
}
Notification.NotificationType.follow -> {
Intent(itemView.context, ProfileActivity::class.java).apply {
putExtra(Account.ACCOUNT_TAG, account)
private fun setNotificationType(
type: Notification.NotificationType,
username: String,
textView: TextView
) {
val context = textView.context
val (format: String, drawable: Drawable?) = when (type) {
Notification.NotificationType.follow -> {
getStringAndDrawable(
context,
R.string.followed_notification,
R.drawable.ic_follow
)
}
Notification.NotificationType.mention -> {
getStringAndDrawable(
context,
R.string.mention_notification,
R.drawable.mention_at_24dp
)
}
Notification.NotificationType.reblog -> {
getStringAndDrawable(
context,
R.string.shared_notification,
R.drawable.ic_reblog_blue
)
}
Notification.NotificationType.favourite -> {
getStringAndDrawable(
context,
R.string.liked_notification,
R.drawable.ic_like_full
)
}
Notification.NotificationType.poll -> {
getStringAndDrawable(context, R.string.poll_notification, R.drawable.poll)
}
}
null -> return //TODO show an error here?
}
itemView.context.startActivity(intent)
}
private fun Notification.openPostFromNotification(): Intent =
Intent(itemView.context, PostActivity::class.java).apply {
putExtra(Status.POST_TAG, status)
}
private fun setNotificationType(type: Notification.NotificationType,
username: String,
textView: TextView
){
val context = textView.context
val (format: String, drawable: Drawable?) = when(type) {
Notification.NotificationType.follow -> {
getStringAndDrawable(context, R.string.followed_notification, R.drawable.ic_follow)
}
Notification.NotificationType.mention -> {
getStringAndDrawable(context, R.string.mention_notification, R.drawable.mention_at_24dp)
}
Notification.NotificationType.reblog -> {
getStringAndDrawable(context, R.string.shared_notification, R.drawable.ic_reblog_blue)
}
Notification.NotificationType.favourite -> {
getStringAndDrawable(context, R.string.liked_notification, R.drawable.ic_like_full)
}
Notification.NotificationType.poll -> {
getStringAndDrawable(context, R.string.poll_notification, R.drawable.poll)
}
}
textView.text = format.format(username)
textView.setCompoundDrawablesWithIntrinsicBounds(
drawable,null,null,null
)
}
private fun getStringAndDrawable(context: Context, stringToFormat: Int, drawable: Int): Pair<String, Drawable?>
= Pair(context.getString(stringToFormat), ContextCompat.getDrawable(context, drawable))
fun bind(notification: Notification?, api: PixelfedAPI, accessToken: String) {
this.notification = notification
Glide.with(itemView).load(notification?.account?.avatar_static).circleCrop().into(avatar)
val previewUrl = notification?.status?.media_attachments?.getOrNull(0)?.preview_url
if(!previewUrl.isNullOrBlank()){
Glide.with(itemView).load(previewUrl)
.placeholder(R.drawable.ic_picture_fallback).into(photoThumbnail)
} else{
photoThumbnail.visibility = View.GONE
}
notification?.type?.let { notification.account?.username?.let { username -> setNotificationType(it, username, notificationType) } }
notification?.created_at?.let { setTextViewFromISO8601(it, notificationTime, false, itemView.context) }
//Convert HTML to clickable text
postDescription.text =
parseHTMLText(
notification?.status?.content ?: "",
notification?.status?.mentions,
api,
itemView.context,
"Bearer $accessToken"
textView.text = format.format(username)
textView.setCompoundDrawablesWithIntrinsicBounds(
drawable, null, null, null
)
}
companion object {
fun create(parent: ViewGroup): NotificationViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.fragment_notifications, parent, false)
return NotificationViewHolder(view)
}
}
}
private fun getStringAndDrawable(
context: Context,
stringToFormat: Int,
drawable: Int
): Pair<String, Drawable?> =
Pair(context.getString(stringToFormat), ContextCompat.getDrawable(context, drawable))
class NotificationsAdapter(private val apiHolder: PixelfedAPIHolder, private val db: AppDatabase) : PagingDataAdapter<Notification, RecyclerView.ViewHolder>(
UIMODEL_COMPARATOR
) {
fun bind(
notification: Notification?,
api: PixelfedAPI,
accessToken: String,
lifecycleScope: LifecycleCoroutineScope
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return NotificationViewHolder.create(parent)
}
this.notification = notification
override fun getItemViewType(position: Int): Int {
return R.layout.fragment_notifications
}
Glide.with(itemView).load(notification?.account?.avatar_static).circleCrop()
.into(avatar)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val uiModel = getItem(position)
uiModel.let {
(holder as NotificationViewHolder).bind(it, apiHolder.setDomainToCurrentUser(db), db.userDao().getActiveUser()!!.accessToken)
val previewUrl = notification?.status?.media_attachments?.getOrNull(0)?.preview_url
if (!previewUrl.isNullOrBlank()) {
Glide.with(itemView).load(previewUrl)
.placeholder(R.drawable.ic_picture_fallback).into(photoThumbnail)
} else {
photoThumbnail.visibility = View.GONE
}
notification?.type?.let {
notification.account?.username?.let { username ->
setNotificationType(
it,
username,
notificationType
)
}
}
notification?.created_at?.let {
setTextViewFromISO8601(
it,
notificationTime,
false,
itemView.context
)
}
//Convert HTML to clickable text
postDescription.text =
parseHTMLText(
notification?.status?.content ?: "",
notification?.status?.mentions,
api,
itemView.context,
"Bearer $accessToken",
lifecycleScope
)
}
companion object {
fun create(parent: ViewGroup): NotificationViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.fragment_notifications, parent, false)
return NotificationViewHolder(view)
}
}
}
companion object {
private val UIMODEL_COMPARATOR = object : DiffUtil.ItemCallback<Notification>() {
override fun areItemsTheSame(oldItem: Notification, newItem: Notification): Boolean {
inner class NotificationsAdapter(
private val apiHolder: PixelfedAPIHolder,
private val db: AppDatabase
) : PagingDataAdapter<Notification, RecyclerView.ViewHolder>(
object : DiffUtil.ItemCallback<Notification>() {
override fun areItemsTheSame(
oldItem: Notification,
newItem: Notification
): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Notification, newItem: Notification): Boolean =
override fun areContentsTheSame(
oldItem: Notification,
newItem: Notification
): Boolean =
oldItem == newItem
}
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return NotificationViewHolder.create(parent)
}
override fun getItemViewType(position: Int): Int {
return R.layout.fragment_notifications
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val uiModel = getItem(position)
uiModel.let {
(holder as NotificationViewHolder).bind(
it,
apiHolder.setDomainToCurrentUser(db),
db.userDao().getActiveUser()!!.accessToken,
lifecycleScope
)
}
}
}
}

View File

@ -107,23 +107,17 @@ class ProfileActivity : BaseActivity() {
}
private fun getAndSetAccount(id: String){
pixelfedAPI.getAccount("Bearer $accessToken", id)
.enqueue(object : Callback<Account> {
override fun onResponse(call: Call<Account>, response: Response<Account>) {
if (response.code() == 200) {
val account = response.body()!!
setContent(account)
} else {
showError()
}
}
override fun onFailure(call: Call<Account>, t: Throwable) {
Log.e("ProfileActivity:", t.toString())
showError()
}
})
lifecycleScope.launchWhenCreated {
val account = try{
pixelfedAPI.getAccount("Bearer $accessToken", id)
} catch (exception: IOException) {
Log.e("ProfileActivity:", exception.toString())
return@launchWhenCreated showError()
} catch (exception: HttpException) {
return@launchWhenCreated showError()
}
setContent(account)
}
}
private fun showError(@StringRes errorText: Int = R.string.loading_toast, show: Boolean = true){
@ -151,7 +145,8 @@ class ProfileActivity : BaseActivity() {
val description = findViewById<TextView>(R.id.descriptionTextView)
description.text = parseHTMLText(
account.note ?: "", emptyList(), pixelfedAPI,
applicationContext, "Bearer $accessToken"
applicationContext, "Bearer $accessToken",
lifecycleScope
)
val accountName = findViewById<TextView>(R.id.accountNameTextView)

View File

@ -12,6 +12,7 @@ import android.widget.*
import androidx.annotation.StringRes
import androidx.appcompat.widget.SearchView
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
@ -32,7 +33,9 @@ import com.mikepenz.iconics.utils.paddingDp
import com.mikepenz.iconics.utils.sizeDp
import retrofit2.Call
import retrofit2.Callback
import retrofit2.HttpException
import retrofit2.Response
import java.io.IOException
/**
* This fragment lets you search and use Pixelfed's Discover feature
@ -107,25 +110,17 @@ class SearchDiscoverFragment : BaseFragment() {
private fun getDiscover() {
api.discover("Bearer $accessToken")
.enqueue(object : Callback<DiscoverPosts> {
override fun onFailure(call: Call<DiscoverPosts>, t: Throwable) {
showError()
Log.e("SearchDiscoverFragment:", t.toString())
}
override fun onResponse(call: Call<DiscoverPosts>, response: Response<DiscoverPosts>) {
if(response.code() == 200) {
val discoverPosts = response.body()!!
adapter.addPosts(discoverPosts.posts)
showError(show = false)
}
else {
showError()
}
}
})
lifecycleScope.launchWhenCreated {
try {
val discoverPosts = api.discover("Bearer $accessToken")
adapter.addPosts(discoverPosts.posts)
showError(show = false)
} catch (exception: IOException) {
showError()
} catch (exception: HttpException) {
showError()
}
}
}
/**

View File

@ -245,10 +245,10 @@ interface PixelfedAPI {
) : Response<List<Account>>
@GET("/api/v1/accounts/{id}")
fun getAccount(
suspend fun getAccount(
@Header("Authorization") authorization: String,
@Path("id") accountId : String
): Call<Account>
): Account
@GET("/api/v1/statuses/{id}")
suspend fun getStatus(
@ -266,19 +266,19 @@ interface PixelfedAPI {
// get discover
@GET("/api/v2/discover/posts")
fun discover(
suspend fun discover(
@Header("Authorization") authorization: String
) : Call<DiscoverPosts>
) : DiscoverPosts
@FormUrlEncoded
@POST("/api/v1/reports")
@JvmSuppressWildcards
fun report(
suspend fun report(
@Header("Authorization") authorization: String,
@Field("account_id") account_id: String,
@Field("status_ids") status_ids: List<Status>,
@Field("comment") comment: String,
@Field("forward") forward: Boolean = true
) : Call<Report>
) : Report
}

View File

@ -6,9 +6,14 @@ import android.util.Log
import androidx.core.content.ContextCompat.startActivity
import com.h.pixeldroid.profile.ProfileActivity
import com.h.pixeldroid.utils.api.PixelfedAPI
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.supervisorScope
import retrofit2.Call
import retrofit2.Callback
import retrofit2.HttpException
import retrofit2.Response
import java.io.IOException
import java.io.Serializable
/*
@ -52,28 +57,19 @@ data class Account(
/**
* @brief Opens an activity of the profile with the given id
*/
fun getAccountFromId(id: String, api : PixelfedAPI, context: Context, credential: String) {
Log.e("ACCOUNT_ID", id)
api.getAccount(credential, id).enqueue( object : Callback<Account> {
override fun onFailure(call: Call<Account>, t: Throwable) {
Log.e("GET ACCOUNT ERROR", t.toString())
suspend fun openAccountFromId(id: String, api : PixelfedAPI, context: Context, credential: String) {
val account = try {
api.getAccount(credential, id)
} catch (exception: IOException) {
Log.e("GET ACCOUNT ERROR", exception.toString())
return
} catch (exception: HttpException) {
Log.e("ERROR CODE", exception.code().toString())
return
}
//Open the account page in a separate activity
account.openProfile(context)
override fun onResponse(
call: Call<Account>,
response: Response<Account>
) {
if(response.code() == 200) {
val account = response.body()!!
//Open the account page in a separate activity
account.openProfile(context)
} else {
Log.e("ERROR CODE", response.code().toString())
}
}
})
}
}

View File

@ -77,11 +77,6 @@ open class Status(
fun getProfilePicUrl() : String? = account?.avatar
fun getPostPreviewURL() : String? = media_attachments?.firstOrNull()?.preview_url
/**
* @brief returns the parsed version of the HTML description
*/
private fun getDescription(api: PixelfedAPI, context: Context, credential: String) : Spanned =
parseHTMLText(content ?: "", mentions, api, context, credential)
fun getNLikes(context: Context) : CharSequence {
return context.getString(R.string.likes).format(favourites_count.toString())