Merge branch 'bookmarks' into 'master'

Bookmarks and Profile tabs

See merge request pixeldroid/PixelDroid!480
This commit is contained in:
Matthieu 2022-10-24 09:54:08 +00:00
commit 96c14ef289
16 changed files with 425 additions and 220 deletions

View File

@ -11,6 +11,7 @@ import android.net.Uri
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.* import android.widget.*
@ -317,7 +318,41 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
} }
} }
private suspend fun bookmarkPost(api: PixelfedAPI, db: AppDatabase, menu: Menu, bookmarked: Boolean) : Boolean? {
//Call the api function
status?.id?.let { id ->
try {
if(bookmarked) {
api.bookmarkStatus(id)
} else {
api.undoBookmarkStatus(id)
}
val user = db.userDao().getActiveUser()!!
db.homePostDao().bookmarkStatus(id, user.user_id, user.instance_uri, bookmarked)
db.publicPostDao().bookmarkStatus(id, user.user_id, user.instance_uri, bookmarked)
menu.setGroupVisible(R.id.post_more_menu_group_bookmark, !bookmarked)
menu.setGroupVisible(R.id.post_more_menu_group_unbookmark, bookmarked)
return bookmarked
} catch (exception: HttpException) {
Toast.makeText(
binding.root.context,
binding.root.context.getString(R.string.bookmark_post_failed_error, exception.code()),
Toast.LENGTH_SHORT
).show()
} catch (exception: IOException) {
Toast.makeText(
binding.root.context,
binding.root.context.getString(R.string.bookmark_post_failed_io_except),
Toast.LENGTH_SHORT
).show()
}
}
return null
}
private fun activateMoreButton(apiHolder: PixelfedAPIHolder, db: AppDatabase, lifecycleScope: LifecycleCoroutineScope){ private fun activateMoreButton(apiHolder: PixelfedAPIHolder, db: AppDatabase, lifecycleScope: LifecycleCoroutineScope){
var bookmarked: Boolean? = null
binding.statusMore.setOnClickListener { binding.statusMore.setOnClickListener {
PopupMenu(it.context, it).apply { PopupMenu(it.context, it).apply {
setOnMenuItemClickListener { item -> setOnMenuItemClickListener { item ->
@ -341,6 +376,18 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
true true
} }
R.id.post_more_menu_bookmark -> {
lifecycleScope.launch {
bookmarked = bookmarkPost(apiHolder.api ?: apiHolder.setToCurrentUser(), db, menu, true)
}
true
}
R.id.post_more_menu_unbookmark -> {
lifecycleScope.launch {
bookmarked = bookmarkPost(apiHolder.api ?: apiHolder.setToCurrentUser(), db, menu, false)
}
true
}
R.id.post_more_menu_save_to_gallery -> { R.id.post_more_menu_save_to_gallery -> {
Dexter.withContext(binding.root.context) Dexter.withContext(binding.root.context)
.withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) .withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
@ -429,6 +476,11 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
} }
} }
inflate(R.menu.post_more_menu) inflate(R.menu.post_more_menu)
if(bookmarked == true || status?.bookmarked == true) {
menu.setGroupVisible(R.id.post_more_menu_group_bookmark, false)
} else if(bookmarked == false || status?.bookmarked != true) {
menu.setGroupVisible(R.id.post_more_menu_group_unbookmark, false)
}
if(status?.media_attachments.isNullOrEmpty()) { if(status?.media_attachments.isNullOrEmpty()) {
//make sure to disable image-related things if there aren't any //make sure to disable image-related things if there aren't any
menu.setGroupVisible(R.id.post_more_group_picture, false) menu.setGroupVisible(R.id.post_more_group_picture, false)

View File

@ -10,6 +10,7 @@ import androidx.lifecycle.LifecycleCoroutineScope
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.paging.LoadStateAdapter import androidx.paging.LoadStateAdapter
import androidx.paging.PagingDataAdapter import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.gson.Gson import com.google.gson.Gson
@ -21,6 +22,7 @@ import org.pixeldroid.app.databinding.ErrorLayoutBinding
import org.pixeldroid.app.databinding.LoadStateFooterViewItemBinding import org.pixeldroid.app.databinding.LoadStateFooterViewItemBinding
import org.pixeldroid.app.posts.feeds.uncachedFeeds.FeedViewModel import org.pixeldroid.app.posts.feeds.uncachedFeeds.FeedViewModel
import org.pixeldroid.app.utils.api.objects.FeedContent import org.pixeldroid.app.utils.api.objects.FeedContent
import org.pixeldroid.app.utils.api.objects.Status
import retrofit2.HttpException import retrofit2.HttpException
/** /**
@ -169,3 +171,12 @@ class ReposLoadStateViewHolder(
} }
} }
} }
val UIMODEL_STATUS_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.id == newItem.id
}

View File

@ -8,10 +8,10 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.paging.ExperimentalPagingApi import androidx.paging.ExperimentalPagingApi
import androidx.paging.PagingDataAdapter import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.posts.StatusViewHolder import org.pixeldroid.app.posts.StatusViewHolder
import org.pixeldroid.app.posts.feeds.UIMODEL_STATUS_COMPARATOR
import org.pixeldroid.app.posts.feeds.uncachedFeeds.hashtags.HashTagContentRepository import org.pixeldroid.app.posts.feeds.uncachedFeeds.hashtags.HashTagContentRepository
import org.pixeldroid.app.posts.feeds.uncachedFeeds.search.SearchContentRepository import org.pixeldroid.app.posts.feeds.uncachedFeeds.search.SearchContentRepository
import org.pixeldroid.app.utils.api.objects.Results import org.pixeldroid.app.utils.api.objects.Results
@ -74,16 +74,8 @@ class UncachedPostsFragment : UncachedFeedFragment<Status>() {
return view return view
} }
inner class PostsAdapter(private val displayDimensionsInPx: Pair<Int, Int>) : PagingDataAdapter<Status, RecyclerView.ViewHolder>( inner class PostsAdapter(private val displayDimensionsInPx: Pair<Int, Int>)
object : DiffUtil.ItemCallback<Status>() { : PagingDataAdapter<Status, RecyclerView.ViewHolder>(UIMODEL_STATUS_COMPARATOR) {
override fun areItemsTheSame(oldItem: Status, newItem: Status): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Status, newItem: Status): Boolean =
oldItem.id == newItem.id
}
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return StatusViewHolder.create(parent) return StatusViewHolder.create(parent)

View File

@ -13,7 +13,8 @@ import javax.inject.Inject
class ProfileContentRepository @ExperimentalPagingApi class ProfileContentRepository @ExperimentalPagingApi
@Inject constructor( @Inject constructor(
private val api: PixelfedAPI, private val api: PixelfedAPI,
private val accountId: String private val accountId: String,
private val bookmarks: Boolean
) : UncachedContentRepository<Status> { ) : UncachedContentRepository<Status> {
override fun getStream(): Flow<PagingData<Status>> { override fun getStream(): Flow<PagingData<Status>> {
return Pager( return Pager(
@ -21,7 +22,7 @@ class ProfileContentRepository @ExperimentalPagingApi
initialLoadSize = NETWORK_PAGE_SIZE, initialLoadSize = NETWORK_PAGE_SIZE,
pageSize = NETWORK_PAGE_SIZE), pageSize = NETWORK_PAGE_SIZE),
pagingSourceFactory = { pagingSourceFactory = {
ProfilePagingSource(api, accountId) ProfilePagingSource(api, accountId, bookmarks)
} }
).flow ).flow
} }

View File

@ -9,16 +9,25 @@ import java.io.IOException
class ProfilePagingSource( class ProfilePagingSource(
private val api: PixelfedAPI, private val api: PixelfedAPI,
private val accountId: String private val accountId: String,
private val bookmarks: Boolean
) : PagingSource<String, Status>() { ) : PagingSource<String, Status>() {
override suspend fun load(params: LoadParams<String>): LoadResult<String, Status> { override suspend fun load(params: LoadParams<String>): LoadResult<String, Status> {
val position = params.key val position = params.key
return try { return try {
val posts = api.accountPosts( val posts =
account_id = accountId, if(bookmarks) {
max_id = position, api.bookmarks(
limit = params.loadSize limit = params.loadSize,
) max_id = position
)
} else {
api.accountPosts(
account_id = accountId,
max_id = position,
limit = params.loadSize
)
}
val nextKey = posts.lastOrNull()?.id val nextKey = posts.lastOrNull()?.id

View File

@ -10,28 +10,20 @@ import android.widget.ImageView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModel import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.paging.ExperimentalPagingApi
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Job import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityProfileBinding import org.pixeldroid.app.databinding.ActivityProfileBinding
import org.pixeldroid.app.databinding.FragmentProfilePostsBinding import org.pixeldroid.app.databinding.FragmentProfilePostsBinding
import org.pixeldroid.app.posts.PostActivity import org.pixeldroid.app.posts.PostActivity
import org.pixeldroid.app.posts.feeds.initAdapter
import org.pixeldroid.app.posts.feeds.launch
import org.pixeldroid.app.posts.feeds.uncachedFeeds.FeedViewModel
import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedContentRepository
import org.pixeldroid.app.posts.feeds.uncachedFeeds.profile.ProfileContentRepository
import org.pixeldroid.app.posts.parseHTMLText import org.pixeldroid.app.posts.parseHTMLText
import org.pixeldroid.app.utils.* import org.pixeldroid.app.utils.*
import org.pixeldroid.app.utils.api.PixelfedAPI import org.pixeldroid.app.utils.api.PixelfedAPI
@ -47,13 +39,9 @@ class ProfileActivity : BaseThemedWithBarActivity() {
private lateinit var domain : String private lateinit var domain : String
private lateinit var accountId : String private lateinit var accountId : String
private lateinit var binding: ActivityProfileBinding private lateinit var binding: ActivityProfileBinding
private lateinit var profileAdapter: PagingDataAdapter<Status, RecyclerView.ViewHolder>
private lateinit var viewModel: FeedViewModel<Status>
private var user: UserDatabaseEntity? = null private var user: UserDatabaseEntity? = null
private var job: Job? = null
@OptIn(ExperimentalPagingApi::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityProfileBinding.inflate(layoutInflater) binding = ActivityProfileBinding.inflate(layoutInflater)
@ -69,46 +57,82 @@ class ProfileActivity : BaseThemedWithBarActivity() {
val account = intent.getSerializableExtra(Account.ACCOUNT_TAG) as Account? val account = intent.getSerializableExtra(Account.ACCOUNT_TAG) as Account?
accountId = account?.id ?: user!!.user_id accountId = account?.id ?: user!!.user_id
// get the view model val tabs = createProfileTabs(account)
@Suppress("UNCHECKED_CAST") setupTabs(tabs)
viewModel = ViewModelProvider(this, ProfileViewModelFactory(
ProfileContentRepository(
apiHolder.setToCurrentUser(),
accountId
)
)
)[FeedViewModel::class.java] as FeedViewModel<Status>
profileAdapter = ProfilePostsAdapter()
initAdapter(binding.profileProgressBar, binding.profileRefreshLayout,
binding.profilePostsRecyclerView, binding.motionLayout, binding.errorLayout,
profileAdapter)
binding.profilePostsRecyclerView.layoutManager = GridLayoutManager(this, 3)
binding.profileRefreshLayout.setOnRefreshListener {
setContent(account)
profileAdapter.refresh()
}
setContent(account) setContent(account)
job = launch(job, lifecycleScope, viewModel, profileAdapter)
} }
/** private fun createProfileTabs(account: Account?): Array<Fragment>{
* Shows or hides the error in the profile
*/ val profileFeedFragment = ProfileFeedFragment()
private fun showError(errorText: String = getString(R.string.profile_error), show: Boolean = true){ val argumentsFeed = Bundle().apply {
if(show){ putSerializable(Account.ACCOUNT_TAG, account)
binding.profileProgressBar.visibility = View.GONE putSerializable(ProfileFeedFragment.PROFILE_GRID, false)
binding.motionLayout.transitionToEnd() putSerializable(ProfileFeedFragment.BOOKMARKS, false)
binding.errorLayout.errorText.text = errorText
} else if(binding.motionLayout.progress == 1F) {
binding.motionLayout.transitionToStart()
} }
binding.profileRefreshLayout.isRefreshing = false profileFeedFragment.arguments = argumentsFeed
val profileGridFragment = ProfileFeedFragment()
val argumentsGrid = Bundle().apply {
putSerializable(Account.ACCOUNT_TAG, account)
putSerializable(ProfileFeedFragment.PROFILE_GRID, true)
putSerializable(ProfileFeedFragment.BOOKMARKS, false)
}
profileGridFragment.arguments = argumentsGrid
// If we are viewing our own account, show bookmarks
if(account == null || account.id == user?.user_id) {
val profileBookmarksFragment = ProfileFeedFragment()
val argumentsBookmarks = Bundle().apply {
putSerializable(Account.ACCOUNT_TAG, account)
putSerializable(ProfileFeedFragment.PROFILE_GRID, true)
putSerializable(ProfileFeedFragment.BOOKMARKS, true)
}
profileBookmarksFragment.arguments = argumentsBookmarks
return arrayOf(
profileGridFragment,
profileFeedFragment,
profileBookmarksFragment
)
}
return arrayOf(
profileGridFragment,
profileFeedFragment
)
} }
private fun setupTabs(
tabs: Array<Fragment>
){
binding.viewPager.adapter = object : FragmentStateAdapter(this) {
override fun createFragment(position: Int): Fragment {
return tabs[position]
}
override fun getItemCount(): Int {
return tabs.size
}
}
TabLayoutMediator(binding.profileTabs, binding.viewPager) { tab, position ->
tab.tabLabelVisibility = TabLayout.TAB_LABEL_VISIBILITY_UNLABELED
when (position) {
0 -> {
tab.setText("Grid view")
tab.setIcon(R.drawable.grid_on_black_24dp)
}
1 -> {
tab.setText("Feed view")
tab.setIcon(R.drawable.feed_view)
}
2 -> {
tab.setText("Bookmarks")
tab.setIcon(R.drawable.bookmark)
}
}
}.attach()
}
private fun setContent(account: Account?) { private fun setContent(account: Account?) {
if(account != null) { if(account != null) {
setViews(account) setViews(account)
@ -120,9 +144,17 @@ class ProfileActivity : BaseThemedWithBarActivity() {
api.verifyCredentials() api.verifyCredentials()
} catch (exception: IOException) { } catch (exception: IOException) {
Log.e("ProfileActivity:", exception.toString()) Log.e("ProfileActivity:", exception.toString())
return@launchWhenResumed showError() Toast.makeText(
applicationContext, "Could not get your profile",
Toast.LENGTH_SHORT
).show()
return@launchWhenResumed
} catch (exception: HttpException) { } catch (exception: HttpException) {
return@launchWhenResumed showError() Toast.makeText(
applicationContext, "Could not get your profile",
Toast.LENGTH_SHORT
).show()
return@launchWhenResumed
} }
setViews(myAccount) setViews(myAccount)
} }
@ -325,107 +357,3 @@ class ProfileActivity : BaseThemedWithBarActivity() {
} }
} }
} }
class ProfileViewModelFactory @ExperimentalPagingApi constructor(
private val searchContentRepository: UncachedContentRepository<Status>
) : ViewModelProvider.Factory {
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
private val videoIcon: ImageView = binding.videoIcon
fun bind(post: Status) {
if ((post.media_attachments?.size ?: 0) == 0){
//No media in this post, so put a little icon there
postPreview.scaleX = 0.3f
postPreview.scaleY = 0.3f
Glide.with(postPreview).load(R.drawable.ic_comment_empty).into(postPreview)
albumIcon.visibility = View.GONE
videoIcon.visibility = View.GONE
} else {
postPreview.scaleX = 1f
postPreview.scaleY = 1f
if (post.sensitive != false) {
Glide.with(postPreview)
.load(post.media_attachments?.firstOrNull()?.blurhash?.let {
BlurHashDecoder.blurHashBitmap(itemView.resources, it, 32, 32)
}
).placeholder(R.drawable.ic_sensitive).apply(RequestOptions().centerCrop())
.into(postPreview)
} else {
setSquareImageFromURL(postPreview,
post.getPostPreviewURL(),
postPreview,
post.media_attachments?.firstOrNull()?.blurhash)
}
if ((post.media_attachments?.size ?: 0) > 1) {
albumIcon.visibility = View.VISIBLE
videoIcon.visibility = View.GONE
} else {
albumIcon.visibility = View.GONE
if (post.media_attachments?.getOrNull(0)?.type == Attachment.AttachmentType.video) {
videoIcon.visibility = View.VISIBLE
} else videoIcon.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
}
}
}

View File

@ -0,0 +1,189 @@
package org.pixeldroid.app.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.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.paging.ExperimentalPagingApi
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.FragmentProfilePostsBinding
import org.pixeldroid.app.posts.PostActivity
import org.pixeldroid.app.posts.StatusViewHolder
import org.pixeldroid.app.posts.feeds.UIMODEL_STATUS_COMPARATOR
import org.pixeldroid.app.posts.feeds.uncachedFeeds.*
import org.pixeldroid.app.posts.feeds.uncachedFeeds.profile.ProfileContentRepository
import org.pixeldroid.app.utils.BlurHashDecoder
import org.pixeldroid.app.utils.api.objects.Account
import org.pixeldroid.app.utils.api.objects.Attachment
import org.pixeldroid.app.utils.api.objects.Status
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import org.pixeldroid.app.utils.displayDimensionsInPx
import org.pixeldroid.app.utils.setSquareImageFromURL
/**
* Fragment to show a list of [Account]s, as a result of a search.
*/
class ProfileFeedFragment : UncachedFeedFragment<Status>() {
companion object {
const val PROFILE_GRID = "ProfileGrid"
const val BOOKMARKS = "Bookmarks"
}
private lateinit var accountId : String
private var user: UserDatabaseEntity? = null
private var grid: Boolean = true
private var bookmarks: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
grid = arguments?.getSerializable(PROFILE_GRID) as Boolean
bookmarks = arguments?.getSerializable(BOOKMARKS) as Boolean
adapter = ProfilePostsAdapter()
//get the currently active user
user = db.userDao().getActiveUser()
// Set profile according to given account
val account = arguments?.getSerializable(Account.ACCOUNT_TAG) as Account?
accountId = account?.id ?: user!!.user_id
}
@OptIn(ExperimentalPagingApi::class)
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
if(grid || bookmarks) {
binding.list.layoutManager = GridLayoutManager(context, 3)
}
// Get the view model
@Suppress("UNCHECKED_CAST")
viewModel = ViewModelProvider(requireActivity(), ProfileViewModelFactory(
ProfileContentRepository(
apiHolder.setToCurrentUser(),
accountId,
bookmarks
)
)
)[if(bookmarks) "Bookmarks" else "Profile", FeedViewModel::class.java] as FeedViewModel<Status>
launch()
initSearch()
return view
}
inner class ProfilePostsAdapter() : PagingDataAdapter<Status, RecyclerView.ViewHolder>(
UIMODEL_STATUS_COMPARATOR
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if(grid || bookmarks) {
ProfilePostsViewHolder.create(parent)
} else {
StatusViewHolder.create(parent)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val post = getItem(position)
post?.let {
if(grid || bookmarks) {
(holder as ProfilePostsViewHolder).bind(it)
} else {
(holder as StatusViewHolder).bind(it, apiHolder, db,
lifecycleScope, requireContext().displayDimensionsInPx())
}
}
}
}
}
class ProfilePostsViewHolder(binding: FragmentProfilePostsBinding) : RecyclerView.ViewHolder(binding.root) {
private val postPreview: ImageView = binding.postPreview
private val albumIcon: ImageView = binding.albumIcon
private val videoIcon: ImageView = binding.videoIcon
fun bind(post: Status) {
if ((post.media_attachments?.size ?: 0) == 0){
//No media in this post, so put a little icon there
postPreview.scaleX = 0.3f
postPreview.scaleY = 0.3f
Glide.with(postPreview).load(R.drawable.ic_comment_empty).into(postPreview)
albumIcon.visibility = View.GONE
videoIcon.visibility = View.GONE
} else {
postPreview.scaleX = 1f
postPreview.scaleY = 1f
if (post.sensitive != false) {
Glide.with(postPreview)
.load(post.media_attachments?.firstOrNull()?.blurhash?.let {
BlurHashDecoder.blurHashBitmap(itemView.resources, it, 32, 32)
}
).placeholder(R.drawable.ic_sensitive).apply(RequestOptions().centerCrop())
.into(postPreview)
} else {
setSquareImageFromURL(postPreview,
post.getPostPreviewURL(),
postPreview,
post.media_attachments?.firstOrNull()?.blurhash)
}
if ((post.media_attachments?.size ?: 0) > 1) {
albumIcon.visibility = View.VISIBLE
videoIcon.visibility = View.GONE
} else {
albumIcon.visibility = View.GONE
if (post.media_attachments?.getOrNull(0)?.type == Attachment.AttachmentType.video) {
videoIcon.visibility = View.VISIBLE
} else videoIcon.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 ProfileViewModelFactory @ExperimentalPagingApi constructor(
private val searchContentRepository: UncachedContentRepository<Status>
) : ViewModelProvider.Factory {
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")
}
}

View File

@ -183,6 +183,25 @@ interface PixelfedAPI {
@Path("id") statusId: String, @Path("id") statusId: String,
) : Status ) : Status
@GET("/api/v1/bookmarks")
suspend fun bookmarks(
@Query("limit") limit: Number? = null,
@Query("max_id") max_id: String? = null,
@Query("since_id") since_id: String? = null,
@Query("min_id") min_id: String? = null
) : List<Status>
@POST("/api/v1/statuses/{id}/bookmark")
suspend fun bookmarkStatus(
@Path("id") statusId: String
) : Status
@POST("/api/v1/statuses/{id}/unbookmark")
suspend fun undoBookmarkStatus(
@Path("id") statusId: String
) : Status
//Used in our case to retrieve comments for a given status //Used in our case to retrieve comments for a given status
@GET("/api/v1/statuses/{id}/context") @GET("/api/v1/statuses/{id}/context")
suspend fun statusComments( suspend fun statusComments(

View File

@ -21,8 +21,8 @@ interface UserDao {
@Update @Update
fun updateUser(user: UserDatabaseEntity) fun updateUser(user: UserDatabaseEntity)
@Query("UPDATE users SET accessToken = :accessToken, refreshToken = :refreshToken WHERE user_id = :id and instance_uri = :instance_uri") @Query("UPDATE users SET accessToken = :accessToken, refreshToken = :refreshToken WHERE user_id = :id and instance_uri = :instanceUri")
fun updateAccessToken(accessToken: String, refreshToken: String, id: String, instance_uri: String) fun updateAccessToken(accessToken: String, refreshToken: String, id: String, instanceUri: String)
@Query("SELECT * FROM users") @Query("SELECT * FROM users")
fun getAll(): List<UserDatabaseEntity> fun getAll(): List<UserDatabaseEntity>
@ -33,12 +33,12 @@ interface UserDao {
@Query("UPDATE users SET isActive=0") @Query("UPDATE users SET isActive=0")
fun deActivateActiveUsers() fun deActivateActiveUsers()
@Query("UPDATE users SET isActive=1 WHERE user_id=:id AND instance_uri=:instance_uri") @Query("UPDATE users SET isActive=1 WHERE user_id=:id AND instance_uri=:instanceUri")
fun activateUser(id: String, instance_uri: String) fun activateUser(id: String, instanceUri: String)
@Query("DELETE FROM users WHERE isActive=1") @Query("DELETE FROM users WHERE isActive=1")
fun deleteActiveUsers() fun deleteActiveUsers()
@Query("SELECT * FROM users WHERE user_id=:id AND instance_uri=:instance_uri") @Query("SELECT * FROM users WHERE user_id=:id AND instance_uri=:instanceUri")
fun getUserWithId(id: String, instance_uri: String): UserDatabaseEntity fun getUserWithId(id: String, instanceUri: String): UserDatabaseEntity
} }

View File

@ -18,4 +18,7 @@ interface HomePostDao: FeedContentDao<HomeStatusDatabaseEntity> {
@Query("DELETE FROM homePosts WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id") @Query("DELETE FROM homePosts WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id")
override suspend fun delete(id: String, userId: String, instanceUri: String) override suspend fun delete(id: String, userId: String, instanceUri: String)
@Query("UPDATE homePosts SET bookmarked=:bookmarked WHERE user_id=:id AND instance_uri=:instanceUri AND id=:statusId")
fun bookmarkStatus(id: String, instanceUri: String, statusId: String, bookmarked: Boolean)
} }

View File

@ -18,4 +18,7 @@ interface PublicPostDao: FeedContentDao<PublicFeedStatusDatabaseEntity> {
@Query("DELETE FROM publicPosts WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id") @Query("DELETE FROM publicPosts WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id")
override suspend fun delete(id: String, userId: String, instanceUri: String) override suspend fun delete(id: String, userId: String, instanceUri: String)
@Query("UPDATE homePosts SET bookmarked=:bookmarked WHERE user_id=:id AND instance_uri=:instanceUri AND id=:statusId")
fun bookmarkStatus(id: String, instanceUri: String, statusId: String, bookmarked: Boolean)
} }

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M17,3H7c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3V5c0,-1.1 -0.9,-2 -2,-2z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M2,21h19v-3H2v3zM20,8H3c-0.55,0 -1,0.45 -1,1v6c0,0.55 0.45,1 1,1h17c0.55,0 1,-0.45 1,-1V9c0,-0.55 -0.45,-1 -1,-1zM2,3v3h19V3H2z"/>
</vector>

View File

@ -46,7 +46,6 @@
android:gravity="center" android:gravity="center"
android:text="@string/default_nposts" android:text="@string/default_nposts"
android:textStyle="bold" android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/descriptionTextView" /> app:layout_constraintTop_toBottomOf="@+id/descriptionTextView" />
@ -89,7 +88,6 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/profilePictureImageView" /> app:layout_constraintTop_toBottomOf="@id/profilePictureImageView" />
<TextView <TextView
android:id="@+id/descriptionTextView" android:id="@+id/descriptionTextView"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -123,49 +121,26 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/profilePictureImageView" app:layout_constraintStart_toEndOf="@+id/profilePictureImageView"
app:layout_constraintTop_toTopOf="@+id/profilePictureImageView" /> app:layout_constraintTop_toTopOf="@+id/profilePictureImageView" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/profileTabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/nbPostsTextView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.CollapsingToolbarLayout> </com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<ProgressBar
android:id="@+id/profileProgressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="16dp" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.viewpager2.widget.ViewPager2
android:id="@+id/profileRefreshLayout" android:id="@+id/view_pager"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"> app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<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">
<include
android:id="@+id/errorLayout"
layout="@layout/error_layout"
tools:layout_editor_absoluteX="50dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/profilePostsRecyclerView"
android:visibility="visible"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layoutManager="LinearLayoutManager"
app:layout_constraintTop_toBottomOf="@id/errorLayout"
tools:listitem="@layout/fragment_profile_posts" />
</androidx.constraintlayout.motion.widget.MotionLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -9,6 +9,15 @@
<item android:id="@+id/post_more_menu_share_link" <item android:id="@+id/post_more_menu_share_link"
android:title="@string/share_link" /> android:title="@string/share_link" />
<group android:id="@+id/post_more_menu_group_bookmark">
<item android:id="@+id/post_more_menu_bookmark"
android:title="@string/bookmark" />
</group>
<group android:id="@+id/post_more_menu_group_unbookmark">
<item android:id="@+id/post_more_menu_unbookmark"
android:title="@string/unbookmark" />
</group>
<!-- Group that should only be shown if there are pictures in the post --> <!-- Group that should only be shown if there are pictures in the post -->
<group android:id="@+id/post_more_group_picture"> <group android:id="@+id/post_more_group_picture">

View File

@ -232,6 +232,8 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
<string name="search_empty_error">Search query can\'t be empty</string> <string name="search_empty_error">Search query can\'t be empty</string>
<string name="status_more_options">More options</string> <string name="status_more_options">More options</string>
<string name="report">Report</string> <string name="report">Report</string>
<string name="bookmark">Bookmark</string>
<string name="unbookmark">Unbookmark</string>
<string name="share_link">Share Link</string> <string name="share_link">Share Link</string>
<string name="optional_report_comment">Optional message for mods/admins</string> <string name="optional_report_comment">Optional message for mods/admins</string>
<string name="report_target">Report @%1$s\'s post</string> <string name="report_target">Report @%1$s\'s post</string>
@ -252,6 +254,8 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
<string name="mascot_description">Image showing a red panda, Pixelfed\'s mascot, using a phone</string> <string name="mascot_description">Image showing a red panda, Pixelfed\'s mascot, using a phone</string>
<string name="delete_post_failed_error">Could not delete the post, error %1$d</string> <string name="delete_post_failed_error">Could not delete the post, error %1$d</string>
<string name="delete_post_failed_io_except">Could not delete the post, check your connection?</string> <string name="delete_post_failed_io_except">Could not delete the post, check your connection?</string>
<string name="bookmark_post_failed_error">Could not (un)bookmark the post, error %1$d</string>
<string name="bookmark_post_failed_io_except">Could not (un)bookmark the post, check your connection?</string>
<!-- Error message when a selected file can not be found --> <!-- Error message when a selected file can not be found -->
<string name="file_not_found">File %1$s was not found</string> <string name="file_not_found">File %1$s was not found</string>