WIP profile feed

This commit is contained in:
mjaillot 2021-01-22 16:42:23 +01:00
parent fd91970c69
commit 567fa40c20
8 changed files with 302 additions and 56 deletions

View File

@ -0,0 +1,34 @@
package com.h.pixeldroid.posts.feeds.uncachedFeeds.profile
import androidx.paging.ExperimentalPagingApi
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.h.pixeldroid.posts.feeds.uncachedFeeds.UncachedContentRepository
import com.h.pixeldroid.utils.api.PixelfedAPI
import com.h.pixeldroid.utils.api.objects.Status
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class ProfileContentRepository @ExperimentalPagingApi
@Inject constructor(
private val api: PixelfedAPI,
private val accessToken: String,
private val accountId: String
) : UncachedContentRepository<Status> {
override fun getStream(): Flow<PagingData<Status>> {
return Pager(
config = PagingConfig(
initialLoadSize = NETWORK_PAGE_SIZE,
pageSize = NETWORK_PAGE_SIZE,
enablePlaceholders = false),
pagingSourceFactory = {
ProfilePagingSource(api, accessToken, accountId)
}
).flow
}
companion object {
private const val NETWORK_PAGE_SIZE = 20
}
}

View File

@ -0,0 +1,135 @@
package com.h.pixeldroid.profile
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.appcompat.content.res.AppCompatResources
import androidx.lifecycle.ViewModelProvider
import androidx.paging.ExperimentalPagingApi
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.h.pixeldroid.R
import com.h.pixeldroid.databinding.FragmentProfilePostsBinding
import com.h.pixeldroid.posts.PostActivity
import com.h.pixeldroid.posts.feeds.uncachedFeeds.FeedViewModel
import com.h.pixeldroid.posts.feeds.uncachedFeeds.UncachedFeedFragment
import com.h.pixeldroid.posts.feeds.uncachedFeeds.ViewModelFactory
import com.h.pixeldroid.posts.feeds.uncachedFeeds.profile.ProfileContentRepository
import com.h.pixeldroid.utils.ImageConverter
import com.h.pixeldroid.utils.api.objects.Account.Companion.ACCOUNT_ID_TAG
import com.h.pixeldroid.utils.api.objects.Status
/**
* Fragment to show all the posts of a user.
*/
class ProfileFeedFragment : UncachedFeedFragment<Status>() {
private lateinit var accountId : String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapter = ProfileAdapter()
accountId = arguments?.getSerializable(ACCOUNT_ID_TAG) as String
}
@ExperimentalPagingApi
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
// get the view model
@Suppress("UNCHECKED_CAST")
viewModel = ViewModelProvider(this, ViewModelFactory(
ProfileContentRepository(
apiHolder.setDomainToCurrentUser(db),
db.userDao().getActiveUser()!!.accessToken,
accountId
)
)
).get(FeedViewModel::class.java) as FeedViewModel<Status>
launch()
initSearch()
return view
}
}
class PostViewHolder(binding: FragmentProfilePostsBinding) : RecyclerView.ViewHolder(binding.root) {
private val postPreview: ImageView = binding.postPreview
private val albumIcon: ImageView = binding.albumIcon
fun bind(post: Status) {
if(post.sensitive!!) {
ImageConverter.setSquareImageFromDrawable(
itemView,
AppCompatResources.getDrawable(itemView.context, R.drawable.ic_sensitive),
postPreview
)
} else {
ImageConverter.setSquareImageFromURL(itemView, post.getPostPreviewURL(), postPreview)
}
if(post.media_attachments?.size ?: 0 > 1) {
albumIcon.visibility = View.VISIBLE
} else {
albumIcon.visibility = View.GONE
}
postPreview.setOnClickListener {
val intent = Intent(postPreview.context, PostActivity::class.java)
intent.putExtra(Status.POST_TAG, post)
postPreview.context.startActivity(intent)
}
}
companion object {
fun create(parent: ViewGroup): PostViewHolder {
val itemBinding = FragmentProfilePostsBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return PostViewHolder(itemBinding)
}
}
}
class ProfileAdapter : PagingDataAdapter<Status, RecyclerView.ViewHolder>(
UIMODEL_COMPARATOR
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return PostViewHolder.create(parent)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val post = getItem(position)
post?.let {
(holder as PostViewHolder).bind(it)
}
}
companion object {
private val UIMODEL_COMPARATOR = object : DiffUtil.ItemCallback<Status>() {
override fun areItemsTheSame(oldItem: Status, newItem: Status): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Status, newItem: Status): Boolean =
oldItem.content == newItem.content
}
}
}

View File

@ -0,0 +1,36 @@
package com.h.pixeldroid.posts.feeds.uncachedFeeds.profile
import androidx.paging.PagingSource
import com.h.pixeldroid.utils.api.PixelfedAPI
import com.h.pixeldroid.utils.api.objects.Status
import retrofit2.HttpException
import java.io.IOException
class ProfilePagingSource(
private val api: PixelfedAPI,
private val accessToken: String,
private val accountId: String
) : PagingSource<Int, Status>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Status> {
val position = params.key
return try {
val response = api.accountPosts("Bearer $accessToken", account_id = accountId)
val posts = if(response.isSuccessful){
response.body().orEmpty()
} else {
throw HttpException(response)
}
LoadResult.Page(
data = posts,
prevKey = null,
nextKey = if(posts.isEmpty()) null else (position ?: 0) + posts.size
)
} catch (exception: IOException) {
LoadResult.Error(exception)
} catch (exception: HttpException) {
LoadResult.Error(exception)
}
}
}

View File

@ -15,6 +15,8 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.h.pixeldroid.R import com.h.pixeldroid.R
import com.h.pixeldroid.databinding.ActivityProfileBinding import com.h.pixeldroid.databinding.ActivityProfileBinding
import com.h.pixeldroid.databinding.FragmentProfileFeedBinding
import com.h.pixeldroid.databinding.FragmentProfilePostsBinding
import com.h.pixeldroid.posts.parseHTMLText import com.h.pixeldroid.posts.parseHTMLText
import com.h.pixeldroid.utils.BaseActivity import com.h.pixeldroid.utils.BaseActivity
import com.h.pixeldroid.utils.ImageConverter import com.h.pixeldroid.utils.ImageConverter
@ -32,17 +34,20 @@ import java.io.IOException
class ProfileActivity : BaseActivity() { class ProfileActivity : BaseActivity() {
private lateinit var pixelfedAPI : PixelfedAPI private lateinit var pixelfedAPI : PixelfedAPI
private lateinit var adapter : ProfilePostsRecyclerViewAdapter // private lateinit var adapter : ProfilePostsRecyclerViewAdapter
private lateinit var accessToken : String private lateinit var accessToken : String
private lateinit var domain : String private lateinit var domain : String
private var user: UserDatabaseEntity? = null
private lateinit var binding: ActivityProfileBinding private var user: UserDatabaseEntity? = null
private var postsFragment = ProfileFeedFragment()
private lateinit var activityBinding: ActivityProfileBinding
private lateinit var feedFragmentBinding: FragmentProfileFeedBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityProfileBinding.inflate(layoutInflater) activityBinding = ActivityProfileBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(activityBinding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
@ -53,16 +58,16 @@ class ProfileActivity : BaseActivity() {
accessToken = user?.accessToken.orEmpty() accessToken = user?.accessToken.orEmpty()
// Set posts RecyclerView as a grid with 3 columns // Set posts RecyclerView as a grid with 3 columns
binding.profilePostsRecyclerView.layoutManager = GridLayoutManager(applicationContext, 3) feedFragmentBinding.profilePostsRecyclerView.layoutManager = GridLayoutManager(applicationContext, 3)
adapter = ProfilePostsRecyclerViewAdapter() // adapter = ProfilePostsRecyclerViewAdapter()
binding.profilePostsRecyclerView.adapter = adapter // binding.profilePostsRecyclerView.adapter = adapter
// Set profile according to given account // Set profile according to given account
val account = intent.getSerializableExtra(Account.ACCOUNT_TAG) as Account? val account = intent.getSerializableExtra(Account.ACCOUNT_TAG) as Account?
setContent(account) setContent(account)
binding.profileRefreshLayout.setOnRefreshListener { activityBinding.profileRefreshLayout.setOnRefreshListener {
getAndSetAccount(account?.id ?: user!!.user_id) getAndSetAccount(account?.id ?: user!!.user_id)
} }
} }
@ -75,7 +80,8 @@ class ProfileActivity : BaseActivity() {
private fun setContent(account: Account?) { private fun setContent(account: Account?) {
if(account != null) { if(account != null) {
setViews(account) setViews(account)
setPosts(account) // setPosts(account)
startFragment(account)
} else { } else {
lifecycleScope.launchWhenResumed { lifecycleScope.launchWhenResumed {
val myAccount: Account = try { val myAccount: Account = try {
@ -88,7 +94,8 @@ class ProfileActivity : BaseActivity() {
} }
setViews(myAccount) setViews(myAccount)
// Populate profile page with user's posts // Populate profile page with user's posts
setPosts(myAccount) // setPosts(myAccount)
startFragment(myAccount)
} }
} }
@ -99,9 +106,9 @@ class ProfileActivity : BaseActivity() {
// On click open followers list // On click open followers list
binding.nbFollowersTextView.setOnClickListener{ onClickFollowers(account) } activityBinding.nbFollowersTextView.setOnClickListener{ onClickFollowers(account) }
// On click open followers list // On click open followers list
binding.nbFollowingTextView.setOnClickListener{ onClickFollowing(account) } activityBinding.nbFollowingTextView.setOnClickListener{ onClickFollowing(account) }
} }
private fun getAndSetAccount(id: String){ private fun getAndSetAccount(id: String){
@ -119,28 +126,28 @@ class ProfileActivity : BaseActivity() {
} }
private fun showError(@StringRes errorText: Int = R.string.loading_toast, show: Boolean = true){ private fun showError(@StringRes errorText: Int = R.string.loading_toast, show: Boolean = true){
val motionLayout = binding.motionLayout val motionLayout = activityBinding.motionLayout
if(show){ if(show){
motionLayout.transitionToEnd() motionLayout.transitionToEnd()
} else { } else {
motionLayout.transitionToStart() motionLayout.transitionToStart()
} }
binding.profileProgressBar.visibility = View.GONE activityBinding.profileProgressBar.visibility = View.GONE
binding.profileRefreshLayout.isRefreshing = false activityBinding.profileRefreshLayout.isRefreshing = false
} }
/** /**
* Populate profile page with user's data * Populate profile page with user's data
*/ */
private fun setViews(account: Account) { private fun setViews(account: Account) {
val profilePicture = binding.profilePictureImageView val profilePicture = activityBinding.profilePictureImageView
ImageConverter.setRoundImageFromURL( ImageConverter.setRoundImageFromURL(
View(applicationContext), View(applicationContext),
account.avatar, account.avatar,
profilePicture profilePicture
) )
binding.descriptionTextView.text = parseHTMLText( activityBinding.descriptionTextView.text = parseHTMLText(
account.note ?: "", emptyList(), pixelfedAPI, account.note ?: "", emptyList(), pixelfedAPI,
applicationContext, "Bearer $accessToken", applicationContext, "Bearer $accessToken",
lifecycleScope lifecycleScope
@ -148,49 +155,58 @@ class ProfileActivity : BaseActivity() {
val displayName = account.getDisplayName() val displayName = account.getDisplayName()
binding.accountNameTextView.text = displayName activityBinding.accountNameTextView.text = displayName
supportActionBar?.title = displayName supportActionBar?.title = displayName
if(displayName != "@${account.acct}"){ if(displayName != "@${account.acct}"){
supportActionBar?.subtitle = "@${account.acct}" supportActionBar?.subtitle = "@${account.acct}"
} }
binding.nbPostsTextView.text = applicationContext.getString(R.string.nb_posts) activityBinding.nbPostsTextView.text = applicationContext.getString(R.string.nb_posts)
.format(account.statuses_count.toString()) .format(account.statuses_count.toString())
binding.nbFollowersTextView.text = applicationContext.getString(R.string.nb_followers) activityBinding.nbFollowersTextView.text = applicationContext.getString(R.string.nb_followers)
.format(account.followers_count.toString()) .format(account.followers_count.toString())
binding.nbFollowingTextView.text = applicationContext.getString(R.string.nb_following) activityBinding.nbFollowingTextView.text = applicationContext.getString(R.string.nb_following)
.format(account.following_count.toString()) .format(account.following_count.toString())
} }
private fun startFragment(account: Account) {
val arguments = Bundle()
arguments.putSerializable(Account.ACCOUNT_ID_TAG, account.id)
postsFragment.arguments = arguments
supportFragmentManager.beginTransaction().add(R.id.fragment_profile_feed, postsFragment).commit()
}
/** /**
* Populate profile page with user's posts * Populate profile page with user's posts
*/ */
private fun setPosts(account: Account) { // private fun setPosts(account: Account) {
pixelfedAPI.accountPosts("Bearer $accessToken", account_id = account.id) // pixelfedAPI.accountPosts("Bearer $accessToken", account_id = account.id)
.enqueue(object : Callback<List<Status>> { // .enqueue(object : Callback<List<Status>> {
//
override fun onFailure(call: Call<List<Status>>, t: Throwable) { // override fun onFailure(call: Call<List<Status>>, t: Throwable) {
showError() // showError()
Log.e("ProfileActivity.Posts:", t.toString()) // Log.e("ProfileActivity.Posts:", t.toString())
} // }
//
override fun onResponse( // override fun onResponse(
call: Call<List<Status>>, // call: Call<List<Status>>,
response: Response<List<Status>> // response: Response<List<Status>>
) { // ) {
if (response.code() == 200) { // if (response.code() == 200) {
val statuses = response.body()!! // val statuses = response.body()!!
adapter.addPosts(statuses) // adapter.addPosts(statuses)
showError(show = false) // showError(show = false)
} else { // } else {
showError() // showError()
} // }
} // }
}) // })
} // }
private fun onClickEditButton() { private fun onClickEditButton() {
val url = "$domain/settings/home" val url = "$domain/settings/home"
@ -216,7 +232,7 @@ class ProfileActivity : BaseActivity() {
private fun activateEditButton() { private fun activateEditButton() {
// Edit button redirects to Pixelfed's "edit account" page // Edit button redirects to Pixelfed's "edit account" page
binding.editButton.apply { activityBinding.editButton.apply {
visibility = View.VISIBLE visibility = View.VISIBLE
setOnClickListener{ onClickEditButton() } setOnClickListener{ onClickEditButton() }
} }
@ -239,7 +255,7 @@ class ProfileActivity : BaseActivity() {
} else { } else {
setOnClickFollow(account) setOnClickFollow(account)
} }
binding.followButton.visibility = View.VISIBLE activityBinding.followButton.visibility = View.VISIBLE
} }
} catch (exception: IOException) { } catch (exception: IOException) {
Log.e("FOLLOW ERROR", exception.toString()) Log.e("FOLLOW ERROR", exception.toString())
@ -257,7 +273,7 @@ class ProfileActivity : BaseActivity() {
} }
private fun setOnClickFollow(account: Account) { private fun setOnClickFollow(account: Account) {
binding.followButton.apply { activityBinding.followButton.apply {
setText(R.string.follow) setText(R.string.follow)
setOnClickListener { setOnClickListener {
lifecycleScope.launchWhenResumed { lifecycleScope.launchWhenResumed {
@ -282,7 +298,7 @@ class ProfileActivity : BaseActivity() {
} }
private fun setOnClickUnfollow(account: Account) { private fun setOnClickUnfollow(account: Account) {
binding.followButton.apply { activityBinding.followButton.apply {
setText(R.string.unfollow) setText(R.string.unfollow)
setOnClickListener { setOnClickListener {

View File

@ -34,10 +34,15 @@ class ProfilePostsRecyclerViewAdapter: RecyclerView.Adapter<ProfilePostViewHolde
override fun onBindViewHolder(holder: ProfilePostViewHolder, position: Int) { override fun onBindViewHolder(holder: ProfilePostViewHolder, position: Int) {
val post = posts[position] val post = posts[position]
if (post.sensitive!!) if(post.sensitive!!) {
setSquareImageFromDrawable(holder.postView, getDrawable(holder.postView.context, R.drawable.ic_sensitive), holder.postPreview) setSquareImageFromDrawable(
else holder.postView,
getDrawable(holder.postView.context, R.drawable.ic_sensitive),
holder.postPreview
)
} else {
setSquareImageFromURL(holder.postView, post.getPostPreviewURL(), holder.postPreview) setSquareImageFromURL(holder.postView, post.getPostPreviewURL(), holder.postPreview)
}
if(post.media_attachments?.size ?: 0 > 1) { if(post.media_attachments?.size ?: 0 > 1) {
holder.albumIcon.visibility = View.VISIBLE holder.albumIcon.visibility = View.VISIBLE
@ -55,6 +60,7 @@ class ProfilePostsRecyclerViewAdapter: RecyclerView.Adapter<ProfilePostViewHolde
override fun getItemCount(): Int = posts.size override fun getItemCount(): Int = posts.size
} }
class ProfilePostViewHolder(val postView: View) : RecyclerView.ViewHolder(postView) { class ProfilePostViewHolder(val postView: View) : RecyclerView.ViewHolder(postView) {
val postPreview: ImageView = postView.findViewById(R.id.postPreview) val postPreview: ImageView = postView.findViewById(R.id.postPreview)
val albumIcon: ImageView = postView.findViewById(R.id.albumIcon) val albumIcon: ImageView = postView.findViewById(R.id.albumIcon)

View File

@ -208,10 +208,10 @@ interface PixelfedAPI {
@GET("/api/v1/accounts/{id}/statuses") @GET("/api/v1/accounts/{id}/statuses")
fun accountPosts( suspend fun accountPosts(
@Header("Authorization") authorization: String, @Header("Authorization") authorization: String,
@Path("id") account_id: String? = null @Path("id") account_id: String? = null
): Call<List<Status>> ) : Response<List<Status>>
@GET("/api/v1/accounts/relationships") @GET("/api/v1/accounts/relationships")
suspend fun checkRelationships( suspend fun checkRelationships(

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/fragment_profile_feed"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".profile.ProfileFeedFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/profilePostsRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="5dp"
android:nestedScrollingEnabled="false"
tools:listitem="@layout/fragment_profile_posts" />
</FrameLayout>

View File

@ -158,5 +158,7 @@
<string name="no_cancel_edit">No, cancel edit</string> <string name="no_cancel_edit">No, cancel edit</string>
<string name="switch_to_grid">Switch to grid view</string> <string name="switch_to_grid">Switch to grid view</string>
<string name="switch_to_carousel">Switch to carousel</string> <string name="switch_to_carousel">Switch to carousel</string>
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
</resources> </resources>