Refactor profile feed to activity
This commit is contained in:
parent
7798523ad1
commit
bcb40a61ae
|
@ -1,141 +0,0 @@
|
|||
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.GridLayoutManager
|
||||
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? ?: db.userDao().getActiveUser()!!.user_id
|
||||
}
|
||||
|
||||
@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>
|
||||
|
||||
binding.list.layoutManager = GridLayoutManager(context, 3)
|
||||
|
||||
launch()
|
||||
initSearch()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
fun refresh(){
|
||||
//TODO implement refresh here
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -3,21 +3,44 @@ package com.h.pixeldroid.profile
|
|||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.size
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.h.pixeldroid.R
|
||||
import com.h.pixeldroid.databinding.ActivityProfileBinding
|
||||
import com.h.pixeldroid.databinding.FragmentProfilePostsBinding
|
||||
import com.h.pixeldroid.posts.PostActivity
|
||||
import com.h.pixeldroid.posts.feeds.ReposLoadStateAdapter
|
||||
import com.h.pixeldroid.posts.feeds.uncachedFeeds.FeedViewModel
|
||||
import com.h.pixeldroid.posts.feeds.uncachedFeeds.UncachedContentRepository
|
||||
import com.h.pixeldroid.posts.feeds.uncachedFeeds.profile.ProfileContentRepository
|
||||
import com.h.pixeldroid.posts.parseHTMLText
|
||||
import com.h.pixeldroid.utils.BaseActivity
|
||||
import com.h.pixeldroid.utils.ImageConverter
|
||||
import com.h.pixeldroid.utils.api.PixelfedAPI
|
||||
import com.h.pixeldroid.utils.api.objects.Account
|
||||
import com.h.pixeldroid.utils.api.objects.Status
|
||||
import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity
|
||||
import com.h.pixeldroid.utils.openUrl
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
|
@ -28,11 +51,17 @@ class ProfileActivity : BaseActivity() {
|
|||
private lateinit var accessToken : String
|
||||
private lateinit var domain : String
|
||||
|
||||
private lateinit var accountId : String
|
||||
|
||||
private var user: UserDatabaseEntity? = null
|
||||
private var postsFragment = ProfileFeedFragment()
|
||||
|
||||
private lateinit var activityBinding: ActivityProfileBinding
|
||||
|
||||
private lateinit var profileAdapter: PagingDataAdapter<Status, RecyclerView.ViewHolder>
|
||||
private lateinit var viewModel: FeedViewModel<Status>
|
||||
private var job: Job? = null
|
||||
|
||||
@ExperimentalPagingApi
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
activityBinding = ActivityProfileBinding.inflate(layoutInflater)
|
||||
|
@ -48,15 +77,122 @@ class ProfileActivity : BaseActivity() {
|
|||
|
||||
// Set profile according to given account
|
||||
val account = intent.getSerializableExtra(Account.ACCOUNT_TAG) as Account?
|
||||
accountId = account?.id ?: user!!.user_id
|
||||
|
||||
setContent(account)
|
||||
startFragment(account?.id)
|
||||
|
||||
profileAdapter = ProfilePostsAdapter()
|
||||
|
||||
initAdapter(activityBinding, profileAdapter)
|
||||
|
||||
// get the view model
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
viewModel = ViewModelProvider(this, ProfileViewModelFactory(
|
||||
ProfileContentRepository(
|
||||
apiHolder.setDomainToCurrentUser(db),
|
||||
db.userDao().getActiveUser()!!.accessToken,
|
||||
accountId
|
||||
)
|
||||
)
|
||||
).get(FeedViewModel::class.java) as FeedViewModel<Status>
|
||||
|
||||
activityBinding.profilePostsRecyclerView.layoutManager = GridLayoutManager(this, 3)
|
||||
|
||||
profileLaunch()
|
||||
profileInitSearch()
|
||||
|
||||
activityBinding.profileRefreshLayout.setOnRefreshListener {
|
||||
getAndSetAccount(account?.id ?: user!!.user_id)
|
||||
//It shouldn't be necessary to also retry() in addition to refresh(),
|
||||
//but if we don't do this, reloads after an error fail immediately...
|
||||
profileAdapter.retry()
|
||||
profileAdapter.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun profileLaunch() {
|
||||
// Make sure we cancel the previous job before creating a new one
|
||||
job?.cancel()
|
||||
job = lifecycleScope.launch {
|
||||
viewModel.flow().collectLatest {
|
||||
profileAdapter.submitData(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun profileInitSearch() {
|
||||
// Scroll to top when the list is refreshed from network.
|
||||
lifecycleScope.launch {
|
||||
profileAdapter.loadStateFlow
|
||||
// Only emit when REFRESH LoadState for RemoteMediator changes.
|
||||
.distinctUntilChangedBy { it.refresh }
|
||||
// Only react to cases where Remote REFRESH completes i.e., NotLoading.
|
||||
.filter { it.refresh is LoadState.NotLoading }
|
||||
.collect { activityBinding.profilePostsRecyclerView.scrollToPosition(0) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shows or hides the error in the different FeedFragments
|
||||
*/
|
||||
private fun showError(errorText: String = "Something went wrong while loading", show: Boolean = true){
|
||||
if(show){
|
||||
activityBinding.motionLayout.transitionToEnd()
|
||||
// binding.profileErrorLayout.errorText.text = errorText
|
||||
} else if(activityBinding.motionLayout.progress == 1F) {
|
||||
activityBinding.motionLayout.transitionToStart()
|
||||
}
|
||||
activityBinding.profileProgressBar.visibility = View.GONE
|
||||
activityBinding.profileRefreshLayout.isRefreshing = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialises the [RecyclerView] adapter for the different FeedFragments.
|
||||
*
|
||||
* Makes the UI respond to various [LoadState]s, including errors when an error message is shown.
|
||||
*/
|
||||
internal fun <T: Any> initAdapter(binding: ActivityProfileBinding, adapter: PagingDataAdapter<T, RecyclerView.ViewHolder>) {
|
||||
binding.profilePostsRecyclerView.adapter = adapter.withLoadStateFooter(
|
||||
footer = ReposLoadStateAdapter { adapter.retry() }
|
||||
)
|
||||
|
||||
adapter.addLoadStateListener { loadState ->
|
||||
|
||||
if(!binding.profileProgressBar.isVisible && binding.profileRefreshLayout.isRefreshing) {
|
||||
// Stop loading spinner when loading is done
|
||||
binding.profileRefreshLayout.isRefreshing = loadState.refresh is LoadState.Loading
|
||||
} else {
|
||||
// ProgressBar should stop showing as soon as the source stops loading ("source"
|
||||
// meaning the database, so don't wait on the network)
|
||||
val sourceLoading = loadState.source.refresh is LoadState.Loading
|
||||
if(!sourceLoading && binding.profilePostsRecyclerView.size > 0){
|
||||
binding.profilePostsRecyclerView.isVisible = true
|
||||
binding.profileProgressBar.isVisible = false
|
||||
} else if(binding.profilePostsRecyclerView.size == 0
|
||||
&& loadState.append is LoadState.NotLoading
|
||||
&& loadState.append.endOfPaginationReached){
|
||||
binding.profileProgressBar.isVisible = false
|
||||
showError(errorText = "Nothing to see here :(")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Toast on any error, regardless of whether it came from RemoteMediator or PagingSource
|
||||
val errorState = loadState.source.append as? LoadState.Error
|
||||
?: loadState.source.prepend as? LoadState.Error
|
||||
?: loadState.source.refresh as? LoadState.Error
|
||||
?: loadState.append as? LoadState.Error
|
||||
?: loadState.prepend as? LoadState.Error
|
||||
?: loadState.refresh as? LoadState.Error
|
||||
errorState?.let {
|
||||
showError(errorText = it.error.toString())
|
||||
}
|
||||
if (errorState == null) showError(show = false, errorText = "")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
onBackPressed()
|
||||
return true
|
||||
|
@ -91,10 +227,10 @@ class ProfileActivity : BaseActivity() {
|
|||
activityBinding.nbFollowingTextView.setOnClickListener{ onClickFollowing(account) }
|
||||
}
|
||||
|
||||
private fun getAndSetAccount(id: String){
|
||||
private fun getAndSetAccount(){
|
||||
lifecycleScope.launchWhenCreated {
|
||||
val account = try{
|
||||
pixelfedAPI.getAccount("Bearer $accessToken", id)
|
||||
pixelfedAPI.getAccount("Bearer $accessToken", accountId)
|
||||
} catch (exception: IOException) {
|
||||
Log.e("ProfileActivity:", exception.toString())
|
||||
return@launchWhenCreated showError()
|
||||
|
@ -105,17 +241,6 @@ class ProfileActivity : BaseActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun showError(@StringRes errorText: Int = R.string.loading_toast, show: Boolean = true){
|
||||
/*val motionLayout = activityBinding.motionLayout
|
||||
if(show){
|
||||
motionLayout.transitionToEnd()
|
||||
} else {
|
||||
motionLayout.transitionToStart()
|
||||
}
|
||||
activityBinding.profileProgressBar.visibility = View.GONE
|
||||
activityBinding.profileRefreshLayout.isRefreshing = false*/
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate profile page with user's data
|
||||
*/
|
||||
|
@ -152,15 +277,6 @@ class ProfileActivity : BaseActivity() {
|
|||
.format(account.following_count.toString())
|
||||
}
|
||||
|
||||
private fun startFragment(accountId: String?) {
|
||||
|
||||
val arguments = Bundle()
|
||||
arguments.putSerializable(Account.ACCOUNT_ID_TAG, accountId)
|
||||
postsFragment.arguments = arguments
|
||||
|
||||
supportFragmentManager.beginTransaction().add(R.id.fragment_profile_feed, postsFragment).commit()
|
||||
}
|
||||
|
||||
private fun onClickEditButton() {
|
||||
val url = "$domain/settings/home"
|
||||
|
||||
|
@ -275,4 +391,89 @@ class ProfileActivity : BaseActivity() {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ProfileViewModelFactory @ExperimentalPagingApi constructor(
|
||||
private val searchContentRepository: UncachedContentRepository<Status>
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
@ExperimentalPagingApi
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
if (modelClass.isAssignableFrom(FeedViewModel::class.java)) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return FeedViewModel(searchContentRepository) as T
|
||||
}
|
||||
throw IllegalArgumentException("Unknown ViewModel class")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ProfilePostsViewHolder(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): ProfilePostsViewHolder {
|
||||
val itemBinding = FragmentProfilePostsBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)
|
||||
return ProfilePostsViewHolder(itemBinding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ProfilePostsAdapter : PagingDataAdapter<Status, RecyclerView.ViewHolder>(
|
||||
UIMODEL_COMPARATOR
|
||||
) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return ProfilePostsViewHolder.create(parent)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val post = getItem(position)
|
||||
|
||||
post?.let {
|
||||
(holder as ProfilePostsViewHolder).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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -121,17 +121,49 @@
|
|||
app:layout_constraintStart_toStartOf="@+id/profilePictureImageView"
|
||||
app:layout_constraintTop_toBottomOf="@+id/descriptionTextView" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/fragment_profile_feed"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/profileProgressBar"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:nestedScrollingEnabled="false"
|
||||
tools:context=".profile.ProfileFeedFragment"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/followButton" />
|
||||
|
||||
|
||||
<androidx.constraintlayout.motion.widget.MotionLayout
|
||||
android:id="@+id/motionLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="8dp"
|
||||
android:visibility="visible"
|
||||
app:layoutDescription="@xml/error_layout_xml_error_scene"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/followButton"
|
||||
tools:visibility="visible">
|
||||
|
||||
<include
|
||||
layout="@layout/error_layout"
|
||||
tools:layout_editor_absoluteX="50dp" />
|
||||
|
||||
<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"
|
||||
app:layoutManager="LinearLayoutManager"
|
||||
app:layout_constraintTop_toBottomOf="@id/errorLayout"
|
||||
tools:listitem="@layout/fragment_profile_posts" />
|
||||
|
||||
</androidx.constraintlayout.motion.widget.MotionLayout>
|
||||
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
|
Loading…
Reference in New Issue