From 567fa40c20a27c67d7bcee3dcecf2b724741aa1c Mon Sep 17 00:00:00 2001 From: mjaillot Date: Fri, 22 Jan 2021 16:42:23 +0100 Subject: [PATCH 01/11] WIP profile feed --- .../profile/ProfileContentRepository.kt | 34 +++++ .../profile/ProfileFeedFragment.kt | 135 ++++++++++++++++++ .../profile/ProfilePagingSource.kt | 36 +++++ .../h/pixeldroid/profile/ProfileActivity.kt | 116 ++++++++------- .../ProfilePostsRecyclerViewAdapter.kt | 14 +- .../com/h/pixeldroid/utils/api/PixelfedAPI.kt | 4 +- .../main/res/layout/fragment_profile_feed.xml | 17 +++ app/src/main/res/values/strings.xml | 2 + 8 files changed, 302 insertions(+), 56 deletions(-) create mode 100644 app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/profile/ProfileContentRepository.kt create mode 100644 app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/profile/ProfileFeedFragment.kt create mode 100644 app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/profile/ProfilePagingSource.kt create mode 100644 app/src/main/res/layout/fragment_profile_feed.xml diff --git a/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/profile/ProfileContentRepository.kt b/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/profile/ProfileContentRepository.kt new file mode 100644 index 00000000..292c8b36 --- /dev/null +++ b/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/profile/ProfileContentRepository.kt @@ -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 { + override fun getStream(): Flow> { + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/profile/ProfileFeedFragment.kt b/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/profile/ProfileFeedFragment.kt new file mode 100644 index 00000000..670bf4b3 --- /dev/null +++ b/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/profile/ProfileFeedFragment.kt @@ -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() { + + 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 + + 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( + 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() { + 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 + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/profile/ProfilePagingSource.kt b/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/profile/ProfilePagingSource.kt new file mode 100644 index 00000000..2cefee2a --- /dev/null +++ b/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/profile/ProfilePagingSource.kt @@ -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() { + override suspend fun load(params: LoadParams): LoadResult { + 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/h/pixeldroid/profile/ProfileActivity.kt b/app/src/main/java/com/h/pixeldroid/profile/ProfileActivity.kt index 505e3831..9dbf6c13 100644 --- a/app/src/main/java/com/h/pixeldroid/profile/ProfileActivity.kt +++ b/app/src/main/java/com/h/pixeldroid/profile/ProfileActivity.kt @@ -15,6 +15,8 @@ import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.h.pixeldroid.R 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.utils.BaseActivity import com.h.pixeldroid.utils.ImageConverter @@ -32,17 +34,20 @@ import java.io.IOException class ProfileActivity : BaseActivity() { private lateinit var pixelfedAPI : PixelfedAPI - private lateinit var adapter : ProfilePostsRecyclerViewAdapter +// private lateinit var adapter : ProfilePostsRecyclerViewAdapter private lateinit var accessToken : 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?) { super.onCreate(savedInstanceState) - binding = ActivityProfileBinding.inflate(layoutInflater) - setContentView(binding.root) + activityBinding = ActivityProfileBinding.inflate(layoutInflater) + setContentView(activityBinding.root) supportActionBar?.setDisplayHomeAsUpEnabled(true) @@ -53,16 +58,16 @@ class ProfileActivity : BaseActivity() { accessToken = user?.accessToken.orEmpty() // Set posts RecyclerView as a grid with 3 columns - binding.profilePostsRecyclerView.layoutManager = GridLayoutManager(applicationContext, 3) - adapter = ProfilePostsRecyclerViewAdapter() - binding.profilePostsRecyclerView.adapter = adapter + feedFragmentBinding.profilePostsRecyclerView.layoutManager = GridLayoutManager(applicationContext, 3) +// adapter = ProfilePostsRecyclerViewAdapter() +// binding.profilePostsRecyclerView.adapter = adapter // Set profile according to given account val account = intent.getSerializableExtra(Account.ACCOUNT_TAG) as Account? setContent(account) - binding.profileRefreshLayout.setOnRefreshListener { + activityBinding.profileRefreshLayout.setOnRefreshListener { getAndSetAccount(account?.id ?: user!!.user_id) } } @@ -73,9 +78,10 @@ class ProfileActivity : BaseActivity() { } private fun setContent(account: Account?) { - if(account != null){ + if(account != null) { setViews(account) - setPosts(account) +// setPosts(account) + startFragment(account) } else { lifecycleScope.launchWhenResumed { val myAccount: Account = try { @@ -88,7 +94,8 @@ class ProfileActivity : BaseActivity() { } setViews(myAccount) // 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 - binding.nbFollowersTextView.setOnClickListener{ onClickFollowers(account) } + activityBinding.nbFollowersTextView.setOnClickListener{ onClickFollowers(account) } // On click open followers list - binding.nbFollowingTextView.setOnClickListener{ onClickFollowing(account) } + activityBinding.nbFollowingTextView.setOnClickListener{ onClickFollowing(account) } } 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){ - val motionLayout = binding.motionLayout + val motionLayout = activityBinding.motionLayout if(show){ motionLayout.transitionToEnd() } else { motionLayout.transitionToStart() } - binding.profileProgressBar.visibility = View.GONE - binding.profileRefreshLayout.isRefreshing = false + activityBinding.profileProgressBar.visibility = View.GONE + activityBinding.profileRefreshLayout.isRefreshing = false } /** * Populate profile page with user's data */ private fun setViews(account: Account) { - val profilePicture = binding.profilePictureImageView + val profilePicture = activityBinding.profilePictureImageView ImageConverter.setRoundImageFromURL( View(applicationContext), account.avatar, profilePicture ) - binding.descriptionTextView.text = parseHTMLText( + activityBinding.descriptionTextView.text = parseHTMLText( account.note ?: "", emptyList(), pixelfedAPI, applicationContext, "Bearer $accessToken", lifecycleScope @@ -148,49 +155,58 @@ class ProfileActivity : BaseActivity() { val displayName = account.getDisplayName() - binding.accountNameTextView.text = displayName + activityBinding.accountNameTextView.text = displayName supportActionBar?.title = displayName if(displayName != "@${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()) - binding.nbFollowersTextView.text = applicationContext.getString(R.string.nb_followers) + activityBinding.nbFollowersTextView.text = applicationContext.getString(R.string.nb_followers) .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()) } + 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 */ - private fun setPosts(account: Account) { - pixelfedAPI.accountPosts("Bearer $accessToken", account_id = account.id) - .enqueue(object : Callback> { - - override fun onFailure(call: Call>, t: Throwable) { - showError() - Log.e("ProfileActivity.Posts:", t.toString()) - } - - override fun onResponse( - call: Call>, - response: Response> - ) { - if (response.code() == 200) { - val statuses = response.body()!! - adapter.addPosts(statuses) - showError(show = false) - } else { - showError() - } - } - }) - } +// private fun setPosts(account: Account) { +// pixelfedAPI.accountPosts("Bearer $accessToken", account_id = account.id) +// .enqueue(object : Callback> { +// +// override fun onFailure(call: Call>, t: Throwable) { +// showError() +// Log.e("ProfileActivity.Posts:", t.toString()) +// } +// +// override fun onResponse( +// call: Call>, +// response: Response> +// ) { +// if (response.code() == 200) { +// val statuses = response.body()!! +// adapter.addPosts(statuses) +// showError(show = false) +// } else { +// showError() +// } +// } +// }) +// } private fun onClickEditButton() { val url = "$domain/settings/home" @@ -216,7 +232,7 @@ class ProfileActivity : BaseActivity() { private fun activateEditButton() { // Edit button redirects to Pixelfed's "edit account" page - binding.editButton.apply { + activityBinding.editButton.apply { visibility = View.VISIBLE setOnClickListener{ onClickEditButton() } } @@ -239,7 +255,7 @@ class ProfileActivity : BaseActivity() { } else { setOnClickFollow(account) } - binding.followButton.visibility = View.VISIBLE + activityBinding.followButton.visibility = View.VISIBLE } } catch (exception: IOException) { Log.e("FOLLOW ERROR", exception.toString()) @@ -257,7 +273,7 @@ class ProfileActivity : BaseActivity() { } private fun setOnClickFollow(account: Account) { - binding.followButton.apply { + activityBinding.followButton.apply { setText(R.string.follow) setOnClickListener { lifecycleScope.launchWhenResumed { @@ -282,7 +298,7 @@ class ProfileActivity : BaseActivity() { } private fun setOnClickUnfollow(account: Account) { - binding.followButton.apply { + activityBinding.followButton.apply { setText(R.string.unfollow) setOnClickListener { diff --git a/app/src/main/java/com/h/pixeldroid/profile/ProfilePostsRecyclerViewAdapter.kt b/app/src/main/java/com/h/pixeldroid/profile/ProfilePostsRecyclerViewAdapter.kt index 6a970f05..ce9a8d6e 100644 --- a/app/src/main/java/com/h/pixeldroid/profile/ProfilePostsRecyclerViewAdapter.kt +++ b/app/src/main/java/com/h/pixeldroid/profile/ProfilePostsRecyclerViewAdapter.kt @@ -34,12 +34,17 @@ class ProfilePostsRecyclerViewAdapter: RecyclerView.Adapter 1){ + if(post.media_attachments?.size ?: 0 > 1) { holder.albumIcon.visibility = View.VISIBLE } else { holder.albumIcon.visibility = View.GONE @@ -55,6 +60,7 @@ class ProfilePostsRecyclerViewAdapter: RecyclerView.Adapter> + ) : Response> @GET("/api/v1/accounts/relationships") suspend fun checkRelationships( diff --git a/app/src/main/res/layout/fragment_profile_feed.xml b/app/src/main/res/layout/fragment_profile_feed.xml new file mode 100644 index 00000000..5c0249af --- /dev/null +++ b/app/src/main/res/layout/fragment_profile_feed.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index be252cfc..745c905b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -158,5 +158,7 @@ No, cancel edit Switch to grid view Switch to carousel + + Hello blank fragment \ No newline at end of file From b221808f9cce3b465689e1cc5d1589e815460256 Mon Sep 17 00:00:00 2001 From: Matthieu <24-artectrex@users.noreply.shinice.net> Date: Fri, 22 Jan 2021 19:25:57 +0100 Subject: [PATCH 02/11] some progress on fixing profile stuff --- app/build.gradle | 2 +- .../uncachedFeeds/UncachedFeedFragment.kt | 2 +- .../profile/ProfileFeedFragment.kt | 10 +++- .../profile/ProfilePagingSource.kt | 14 ++--- .../search/SearchAccountFragment.kt | 3 +- .../h/pixeldroid/profile/ProfileActivity.kt | 59 ++----------------- .../com/h/pixeldroid/utils/api/PixelfedAPI.kt | 11 ++-- app/src/main/res/layout/activity_profile.xml | 39 ++---------- .../main/res/layout/fragment_profile_feed.xml | 17 ------ 9 files changed, 33 insertions(+), 124 deletions(-) delete mode 100644 app/src/main/res/layout/fragment_profile_feed.xml diff --git a/app/build.gradle b/app/build.gradle index 9291c188..4537d64b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -80,7 +80,7 @@ dependencies { implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation 'androidx.navigation:navigation-fragment-ktx:2.3.2' implementation 'androidx.navigation:navigation-ui-ktx:2.3.2' - implementation 'androidx.paging:paging-runtime-ktx:3.0.0-alpha11' + implementation 'androidx.paging:paging-runtime-ktx:3.0.0-alpha12' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.2.0' implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0" diff --git a/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/UncachedFeedFragment.kt b/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/UncachedFeedFragment.kt index f49343d0..087477aa 100644 --- a/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/UncachedFeedFragment.kt +++ b/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/UncachedFeedFragment.kt @@ -30,7 +30,7 @@ open class UncachedFeedFragment : BaseFragment() { internal lateinit var viewModel: FeedViewModel internal lateinit var adapter: PagingDataAdapter - private lateinit var binding: FragmentFeedBinding + lateinit var binding: FragmentFeedBinding private var job: Job? = null diff --git a/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/profile/ProfileFeedFragment.kt b/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/profile/ProfileFeedFragment.kt index 670bf4b3..14617227 100644 --- a/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/profile/ProfileFeedFragment.kt +++ b/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/profile/ProfileFeedFragment.kt @@ -11,6 +11,7 @@ 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 @@ -34,8 +35,7 @@ class ProfileFeedFragment : UncachedFeedFragment() { super.onCreate(savedInstanceState) adapter = ProfileAdapter() - accountId = arguments?.getSerializable(ACCOUNT_ID_TAG) as String - + accountId = arguments?.getSerializable(ACCOUNT_ID_TAG) as String? ?: db.userDao().getActiveUser()!!.user_id } @ExperimentalPagingApi @@ -57,11 +57,17 @@ class ProfileFeedFragment : UncachedFeedFragment() { ) ).get(FeedViewModel::class.java) as FeedViewModel + binding.list.layoutManager = GridLayoutManager(context, 3) + launch() initSearch() return view } + + fun refresh(){ + //TODO implement refresh here + } } diff --git a/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/profile/ProfilePagingSource.kt b/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/profile/ProfilePagingSource.kt index 2cefee2a..c351205c 100644 --- a/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/profile/ProfilePagingSource.kt +++ b/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/profile/ProfilePagingSource.kt @@ -14,18 +14,16 @@ class ProfilePagingSource( override suspend fun load(params: LoadParams): LoadResult { 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) - } + val posts = api.accountPosts("Bearer $accessToken", + account_id = accountId, + min_id = position?.toString(), + limit = params.loadSize + ) LoadResult.Page( data = posts, prevKey = null, - nextKey = if(posts.isEmpty()) null else (position ?: 0) + posts.size + nextKey = posts.lastOrNull()?.id?.toIntOrNull() ) } catch (exception: IOException) { LoadResult.Error(exception) diff --git a/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/search/SearchAccountFragment.kt b/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/search/SearchAccountFragment.kt index c11fc275..68ace7cc 100644 --- a/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/search/SearchAccountFragment.kt +++ b/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/search/SearchAccountFragment.kt @@ -45,8 +45,7 @@ class SearchAccountFragment : UncachedFeedFragment() { query ) ) - ) - .get(FeedViewModel::class.java) as FeedViewModel + ).get(FeedViewModel::class.java) as FeedViewModel launch() initSearch() diff --git a/app/src/main/java/com/h/pixeldroid/profile/ProfileActivity.kt b/app/src/main/java/com/h/pixeldroid/profile/ProfileActivity.kt index 9dbf6c13..d9c42b72 100644 --- a/app/src/main/java/com/h/pixeldroid/profile/ProfileActivity.kt +++ b/app/src/main/java/com/h/pixeldroid/profile/ProfileActivity.kt @@ -1,40 +1,30 @@ package com.h.pixeldroid.profile import android.content.Intent -import android.graphics.Typeface import android.os.Bundle import android.util.Log import android.view.View import android.widget.* import androidx.annotation.StringRes -import androidx.constraintlayout.motion.widget.MotionLayout import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.RecyclerView -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.h.pixeldroid.R 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.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.launch -import retrofit2.Call -import retrofit2.Callback import retrofit2.HttpException -import retrofit2.Response import java.io.IOException class ProfileActivity : BaseActivity() { private lateinit var pixelfedAPI : PixelfedAPI -// private lateinit var adapter : ProfilePostsRecyclerViewAdapter + private lateinit var accessToken : String private lateinit var domain : String @@ -42,7 +32,6 @@ class ProfileActivity : BaseActivity() { private var postsFragment = ProfileFeedFragment() private lateinit var activityBinding: ActivityProfileBinding - private lateinit var feedFragmentBinding: FragmentProfileFeedBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -57,15 +46,11 @@ class ProfileActivity : BaseActivity() { pixelfedAPI = apiHolder.api ?: apiHolder.setDomainToCurrentUser(db) accessToken = user?.accessToken.orEmpty() - // Set posts RecyclerView as a grid with 3 columns - feedFragmentBinding.profilePostsRecyclerView.layoutManager = GridLayoutManager(applicationContext, 3) -// adapter = ProfilePostsRecyclerViewAdapter() -// binding.profilePostsRecyclerView.adapter = adapter - // Set profile according to given account val account = intent.getSerializableExtra(Account.ACCOUNT_TAG) as Account? setContent(account) + startFragment(account?.id) activityBinding.profileRefreshLayout.setOnRefreshListener { getAndSetAccount(account?.id ?: user!!.user_id) @@ -80,8 +65,6 @@ class ProfileActivity : BaseActivity() { private fun setContent(account: Account?) { if(account != null) { setViews(account) -// setPosts(account) - startFragment(account) } else { lifecycleScope.launchWhenResumed { val myAccount: Account = try { @@ -93,9 +76,6 @@ class ProfileActivity : BaseActivity() { return@launchWhenResumed showError() } setViews(myAccount) - // Populate profile page with user's posts -// setPosts(myAccount) - startFragment(myAccount) } } @@ -126,14 +106,14 @@ class ProfileActivity : BaseActivity() { } private fun showError(@StringRes errorText: Int = R.string.loading_toast, show: Boolean = true){ - val motionLayout = activityBinding.motionLayout + /*val motionLayout = activityBinding.motionLayout if(show){ motionLayout.transitionToEnd() } else { motionLayout.transitionToStart() } activityBinding.profileProgressBar.visibility = View.GONE - activityBinding.profileRefreshLayout.isRefreshing = false + activityBinding.profileRefreshLayout.isRefreshing = false*/ } /** @@ -172,42 +152,15 @@ class ProfileActivity : BaseActivity() { .format(account.following_count.toString()) } - private fun startFragment(account: Account) { + private fun startFragment(accountId: String?) { val arguments = Bundle() - arguments.putSerializable(Account.ACCOUNT_ID_TAG, account.id) + arguments.putSerializable(Account.ACCOUNT_ID_TAG, accountId) postsFragment.arguments = arguments supportFragmentManager.beginTransaction().add(R.id.fragment_profile_feed, postsFragment).commit() } - /** - * Populate profile page with user's posts - */ -// private fun setPosts(account: Account) { -// pixelfedAPI.accountPosts("Bearer $accessToken", account_id = account.id) -// .enqueue(object : Callback> { -// -// override fun onFailure(call: Call>, t: Throwable) { -// showError() -// Log.e("ProfileActivity.Posts:", t.toString()) -// } -// -// override fun onResponse( -// call: Call>, -// response: Response> -// ) { -// if (response.code() == 200) { -// val statuses = response.body()!! -// adapter.addPosts(statuses) -// showError(show = false) -// } else { -// showError() -// } -// } -// }) -// } - private fun onClickEditButton() { val url = "$domain/settings/home" diff --git a/app/src/main/java/com/h/pixeldroid/utils/api/PixelfedAPI.kt b/app/src/main/java/com/h/pixeldroid/utils/api/PixelfedAPI.kt index 85bf1cb7..6a7b45e2 100644 --- a/app/src/main/java/com/h/pixeldroid/utils/api/PixelfedAPI.kt +++ b/app/src/main/java/com/h/pixeldroid/utils/api/PixelfedAPI.kt @@ -2,10 +2,7 @@ package com.h.pixeldroid.utils.api import com.h.pixeldroid.utils.api.objects.* import io.reactivex.Observable -import io.reactivex.Single -import kotlinx.coroutines.Deferred import okhttp3.MultipartBody -import retrofit2.Call import retrofit2.Response import retrofit2.Retrofit import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory @@ -209,9 +206,11 @@ interface PixelfedAPI { @GET("/api/v1/accounts/{id}/statuses") suspend fun accountPosts( - @Header("Authorization") authorization: String, - @Path("id") account_id: String? = null - ) : Response> + @Header("Authorization") authorization: String, + @Path("id") account_id: String, + @Query("min_id") min_id: String?, + @Query("limit") limit: Int + ) : List @GET("/api/v1/accounts/relationships") suspend fun checkRelationships( diff --git a/app/src/main/res/layout/activity_profile.xml b/app/src/main/res/layout/activity_profile.xml index de0e7b41..cc907bbe 100644 --- a/app/src/main/res/layout/activity_profile.xml +++ b/app/src/main/res/layout/activity_profile.xml @@ -10,6 +10,7 @@ - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_profile_feed.xml b/app/src/main/res/layout/fragment_profile_feed.xml deleted file mode 100644 index 5c0249af..00000000 --- a/app/src/main/res/layout/fragment_profile_feed.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - \ No newline at end of file From 7798523ad1d142611758ab22914e552a74d1b1b2 Mon Sep 17 00:00:00 2001 From: Matthieu <24-artectrex@users.noreply.shinice.net> Date: Fri, 22 Jan 2021 19:42:56 +0100 Subject: [PATCH 03/11] use max instead of min_id --- .../feeds/uncachedFeeds/profile/ProfilePagingSource.kt | 8 ++++---- .../main/java/com/h/pixeldroid/utils/api/PixelfedAPI.kt | 3 ++- app/src/main/res/layout/activity_profile.xml | 1 + build.gradle | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/profile/ProfilePagingSource.kt b/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/profile/ProfilePagingSource.kt index c351205c..fba34d11 100644 --- a/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/profile/ProfilePagingSource.kt +++ b/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/profile/ProfilePagingSource.kt @@ -10,20 +10,20 @@ class ProfilePagingSource( private val api: PixelfedAPI, private val accessToken: String, private val accountId: String -) : PagingSource() { - override suspend fun load(params: LoadParams): LoadResult { +) : PagingSource() { + override suspend fun load(params: LoadParams): LoadResult { val position = params.key return try { val posts = api.accountPosts("Bearer $accessToken", account_id = accountId, - min_id = position?.toString(), + max_id = position, limit = params.loadSize ) LoadResult.Page( data = posts, prevKey = null, - nextKey = posts.lastOrNull()?.id?.toIntOrNull() + nextKey = posts.lastOrNull()?.id ) } catch (exception: IOException) { LoadResult.Error(exception) diff --git a/app/src/main/java/com/h/pixeldroid/utils/api/PixelfedAPI.kt b/app/src/main/java/com/h/pixeldroid/utils/api/PixelfedAPI.kt index 6a7b45e2..cbbda235 100644 --- a/app/src/main/java/com/h/pixeldroid/utils/api/PixelfedAPI.kt +++ b/app/src/main/java/com/h/pixeldroid/utils/api/PixelfedAPI.kt @@ -208,7 +208,8 @@ interface PixelfedAPI { suspend fun accountPosts( @Header("Authorization") authorization: String, @Path("id") account_id: String, - @Query("min_id") min_id: String?, + @Query("min_id") min_id: String? = null, + @Query("max_id") max_id: String?, @Query("limit") limit: Int ) : List diff --git a/app/src/main/res/layout/activity_profile.xml b/app/src/main/res/layout/activity_profile.xml index cc907bbe..0beccd4c 100644 --- a/app/src/main/res/layout/activity_profile.xml +++ b/app/src/main/res/layout/activity_profile.xml @@ -125,6 +125,7 @@ android:id="@+id/fragment_profile_feed" android:layout_width="match_parent" android:layout_height="wrap_content" + android:nestedScrollingEnabled="false" tools:context=".profile.ProfileFeedFragment" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/build.gradle b/build.gradle index 1b6312c6..efd6d745 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.1' + classpath 'com.android.tools.build:gradle:4.1.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong From bcb40a61ae26e0f1340421b36dfdf7751ca252c7 Mon Sep 17 00:00:00 2001 From: mjaillot Date: Mon, 8 Feb 2021 15:07:23 +0100 Subject: [PATCH 04/11] Refactor profile feed to activity --- .../profile/ProfileFeedFragment.kt | 141 ---------- .../h/pixeldroid/profile/ProfileActivity.kt | 255 ++++++++++++++++-- app/src/main/res/layout/activity_profile.xml | 42 ++- 3 files changed, 265 insertions(+), 173 deletions(-) delete mode 100644 app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/profile/ProfileFeedFragment.kt diff --git a/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/profile/ProfileFeedFragment.kt b/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/profile/ProfileFeedFragment.kt deleted file mode 100644 index 14617227..00000000 --- a/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/profile/ProfileFeedFragment.kt +++ /dev/null @@ -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() { - - 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 - - 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( - 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() { - 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 - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/h/pixeldroid/profile/ProfileActivity.kt b/app/src/main/java/com/h/pixeldroid/profile/ProfileActivity.kt index d9c42b72..68404497 100644 --- a/app/src/main/java/com/h/pixeldroid/profile/ProfileActivity.kt +++ b/app/src/main/java/com/h/pixeldroid/profile/ProfileActivity.kt @@ -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 + private lateinit var viewModel: FeedViewModel + 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 + + 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 initAdapter(binding: ActivityProfileBinding, adapter: PagingDataAdapter) { + 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() { } } } -} \ No newline at end of file +} + + +class ProfileViewModelFactory @ExperimentalPagingApi constructor( + private val searchContentRepository: UncachedContentRepository +) : ViewModelProvider.Factory { + + @ExperimentalPagingApi + override fun create(modelClass: Class): 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( + 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() { + 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 + } + } + +} diff --git a/app/src/main/res/layout/activity_profile.xml b/app/src/main/res/layout/activity_profile.xml index 0beccd4c..314f759a 100644 --- a/app/src/main/res/layout/activity_profile.xml +++ b/app/src/main/res/layout/activity_profile.xml @@ -121,17 +121,49 @@ app:layout_constraintStart_toStartOf="@+id/profilePictureImageView" app:layout_constraintTop_toBottomOf="@+id/descriptionTextView" /> - + + + + + + + + + + From 483fcd3a7ea1bfac478c6c4f1213c98aea224f01 Mon Sep 17 00:00:00 2001 From: mjaillot Date: Wed, 10 Feb 2021 14:12:30 +0100 Subject: [PATCH 05/11] Fix profile layout --- app/src/main/res/layout/activity_profile.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/res/layout/activity_profile.xml b/app/src/main/res/layout/activity_profile.xml index 314f759a..90c961d2 100644 --- a/app/src/main/res/layout/activity_profile.xml +++ b/app/src/main/res/layout/activity_profile.xml @@ -10,7 +10,6 @@ Date: Fri, 26 Feb 2021 11:21:04 +0100 Subject: [PATCH 06/11] Rearrange profile layout --- .../h/pixeldroid/profile/ProfileActivity.kt | 108 ++++++++---------- app/src/main/res/layout/activity_profile.xml | 52 ++++----- 2 files changed, 69 insertions(+), 91 deletions(-) diff --git a/app/src/main/java/com/h/pixeldroid/profile/ProfileActivity.kt b/app/src/main/java/com/h/pixeldroid/profile/ProfileActivity.kt index 68404497..39bf763a 100644 --- a/app/src/main/java/com/h/pixeldroid/profile/ProfileActivity.kt +++ b/app/src/main/java/com/h/pixeldroid/profile/ProfileActivity.kt @@ -46,26 +46,23 @@ import retrofit2.HttpException import java.io.IOException class ProfileActivity : BaseActivity() { - private lateinit var pixelfedAPI : PixelfedAPI + private lateinit var pixelfedAPI : PixelfedAPI private lateinit var accessToken : String private lateinit var domain : String - private lateinit var accountId : String - - private var user: UserDatabaseEntity? = null - - private lateinit var activityBinding: ActivityProfileBinding - + private lateinit var binding: ActivityProfileBinding private lateinit var profileAdapter: PagingDataAdapter private lateinit var viewModel: FeedViewModel + + private var user: UserDatabaseEntity? = null private var job: Job? = null @ExperimentalPagingApi override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - activityBinding = ActivityProfileBinding.inflate(layoutInflater) - setContentView(activityBinding.root) + binding = ActivityProfileBinding.inflate(layoutInflater) + setContentView(binding.root) supportActionBar?.setDisplayHomeAsUpEnabled(true) @@ -79,12 +76,6 @@ class ProfileActivity : BaseActivity() { val account = intent.getSerializableExtra(Account.ACCOUNT_TAG) as Account? accountId = account?.id ?: user!!.user_id - setContent(account) - - profileAdapter = ProfilePostsAdapter() - - initAdapter(activityBinding, profileAdapter) - // get the view model @Suppress("UNCHECKED_CAST") viewModel = ViewModelProvider(this, ProfileViewModelFactory( @@ -96,19 +87,22 @@ class ProfileActivity : BaseActivity() { ) ).get(FeedViewModel::class.java) as FeedViewModel - activityBinding.profilePostsRecyclerView.layoutManager = GridLayoutManager(this, 3) + profileAdapter = ProfilePostsAdapter() + initAdapter(binding, profileAdapter) - profileLaunch() - profileInitSearch() + binding.profilePostsRecyclerView.layoutManager = GridLayoutManager(this, 3) - activityBinding.profileRefreshLayout.setOnRefreshListener { + binding.profileRefreshLayout.setOnRefreshListener { //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() } - } + setContent(account) + profileLaunch() + profileInitSearch() + } private fun profileLaunch() { // Make sure we cancel the previous job before creating a new one @@ -128,23 +122,21 @@ class ProfileActivity : BaseActivity() { .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) } + .collect { binding.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() + binding.profileProgressBar.visibility = View.GONE + binding.motionLayout.transitionToEnd() + } else if(binding.motionLayout.progress == 1F) { + binding.motionLayout.transitionToStart() } - activityBinding.profileProgressBar.visibility = View.GONE - activityBinding.profileRefreshLayout.isRefreshing = false + binding.profileRefreshLayout.isRefreshing = false } /** @@ -215,44 +207,32 @@ class ProfileActivity : BaseActivity() { } } - //if we aren't viewing our own account, activate follow button - if(account != null && account.id != user?.user_id) activateFollow(account) - //if we *are* viewing our own account, activate the edit button - else activateEditButton() - - - // On click open followers list - activityBinding.nbFollowersTextView.setOnClickListener{ onClickFollowers(account) } - // On click open followers list - activityBinding.nbFollowingTextView.setOnClickListener{ onClickFollowing(account) } - } - - private fun getAndSetAccount(){ - lifecycleScope.launchWhenCreated { - val account = try{ - pixelfedAPI.getAccount("Bearer $accessToken", accountId) - } catch (exception: IOException) { - Log.e("ProfileActivity:", exception.toString()) - return@launchWhenCreated showError() - } catch (exception: HttpException) { - return@launchWhenCreated showError() - } - setContent(account) + if(account != null && account.id != user?.user_id) { + //if we aren't viewing our own account, activate follow button + activateFollow(account) + } else { + //if we *are* viewing our own account, activate the edit button + activateEditButton() } + + // On click open followers list + binding.nbFollowersTextView.setOnClickListener{ onClickFollowers(account) } + // On click open followers list + binding.nbFollowingTextView.setOnClickListener{ onClickFollowing(account) } } /** * Populate profile page with user's data */ private fun setViews(account: Account) { - val profilePicture = activityBinding.profilePictureImageView + val profilePicture = binding.profilePictureImageView ImageConverter.setRoundImageFromURL( View(applicationContext), account.avatar, profilePicture ) - activityBinding.descriptionTextView.text = parseHTMLText( + binding.descriptionTextView.text = parseHTMLText( account.note ?: "", emptyList(), pixelfedAPI, applicationContext, "Bearer $accessToken", lifecycleScope @@ -260,27 +240,29 @@ class ProfileActivity : BaseActivity() { val displayName = account.getDisplayName() - activityBinding.accountNameTextView.text = displayName + binding.accountNameTextView.text = displayName supportActionBar?.title = displayName - if(displayName != "@${account.acct}"){ + if(displayName != "@${account.acct}") { supportActionBar?.subtitle = "@${account.acct}" } - activityBinding.nbPostsTextView.text = applicationContext.getString(R.string.nb_posts) + binding.nbPostsTextView.text = applicationContext.getString(R.string.nb_posts) .format(account.statuses_count.toString()) - activityBinding.nbFollowersTextView.text = applicationContext.getString(R.string.nb_followers) + binding.nbFollowersTextView.text = applicationContext.getString(R.string.nb_followers) .format(account.followers_count.toString()) - activityBinding.nbFollowingTextView.text = applicationContext.getString(R.string.nb_following) + binding.nbFollowingTextView.text = applicationContext.getString(R.string.nb_following) .format(account.following_count.toString()) } private fun onClickEditButton() { val url = "$domain/settings/home" - if (!openUrl(url)) Log.e("ProfileActivity", "Cannot open this link") + if(!openUrl(url)) { + Log.e("ProfileActivity", "Cannot open this link") + } } private fun onClickFollowers(account: Account?) { @@ -301,7 +283,7 @@ class ProfileActivity : BaseActivity() { private fun activateEditButton() { // Edit button redirects to Pixelfed's "edit account" page - activityBinding.editButton.apply { + binding.editButton.apply { visibility = View.VISIBLE setOnClickListener{ onClickEditButton() } } @@ -324,7 +306,7 @@ class ProfileActivity : BaseActivity() { } else { setOnClickFollow(account) } - activityBinding.followButton.visibility = View.VISIBLE + binding.followButton.visibility = View.VISIBLE } } catch (exception: IOException) { Log.e("FOLLOW ERROR", exception.toString()) @@ -342,7 +324,7 @@ class ProfileActivity : BaseActivity() { } private fun setOnClickFollow(account: Account) { - activityBinding.followButton.apply { + binding.followButton.apply { setText(R.string.follow) setOnClickListener { lifecycleScope.launchWhenResumed { @@ -367,7 +349,7 @@ class ProfileActivity : BaseActivity() { } private fun setOnClickUnfollow(account: Account) { - activityBinding.followButton.apply { + binding.followButton.apply { setText(R.string.unfollow) setOnClickListener { diff --git a/app/src/main/res/layout/activity_profile.xml b/app/src/main/res/layout/activity_profile.xml index 90c961d2..1e300bf5 100644 --- a/app/src/main/res/layout/activity_profile.xml +++ b/app/src/main/res/layout/activity_profile.xml @@ -18,27 +18,26 @@ + tools:srcCompat="@tools:sample/avatars" /> + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/descriptionTextView" /> + app:layout_constraintTop_toBottomOf="@id/accountNameTextView" />