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.util.Log
import android.view.LayoutInflater
import android.view.Menu
import android.view.View
import android.view.ViewGroup
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){
var bookmarked: Boolean? = null
binding.statusMore.setOnClickListener {
PopupMenu(it.context, it).apply {
setOnMenuItemClickListener { item ->
@ -341,6 +376,18 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
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 -> {
Dexter.withContext(binding.root.context)
.withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
@ -429,6 +476,11 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
}
}
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()) {
//make sure to disable image-related things if there aren't any
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.LoadStateAdapter
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
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.posts.feeds.uncachedFeeds.FeedViewModel
import org.pixeldroid.app.utils.api.objects.FeedContent
import org.pixeldroid.app.utils.api.objects.Status
import retrofit2.HttpException
/**
@ -168,4 +170,13 @@ class ReposLoadStateViewHolder(
return ReposLoadStateViewHolder(binding, retry)
}
}
}
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.paging.ExperimentalPagingApi
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import org.pixeldroid.app.R
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.search.SearchContentRepository
import org.pixeldroid.app.utils.api.objects.Results
@ -74,16 +74,8 @@ class UncachedPostsFragment : UncachedFeedFragment<Status>() {
return view
}
inner class PostsAdapter(private val displayDimensionsInPx: Pair<Int, Int>) : PagingDataAdapter<Status, RecyclerView.ViewHolder>(
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
}
) {
inner class PostsAdapter(private val displayDimensionsInPx: Pair<Int, Int>)
: PagingDataAdapter<Status, RecyclerView.ViewHolder>(UIMODEL_STATUS_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return StatusViewHolder.create(parent)

View File

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

View File

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

View File

@ -10,28 +10,20 @@ import android.widget.ImageView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.fragment.app.Fragment
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.viewpager2.adapter.FragmentStateAdapter
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
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 org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityProfileBinding
import org.pixeldroid.app.databinding.FragmentProfilePostsBinding
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.utils.*
import org.pixeldroid.app.utils.api.PixelfedAPI
@ -47,13 +39,9 @@ class ProfileActivity : BaseThemedWithBarActivity() {
private lateinit var domain : String
private lateinit var accountId : String
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 job: Job? = null
@OptIn(ExperimentalPagingApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityProfileBinding.inflate(layoutInflater)
@ -69,46 +57,82 @@ class ProfileActivity : BaseThemedWithBarActivity() {
val account = intent.getSerializableExtra(Account.ACCOUNT_TAG) as Account?
accountId = account?.id ?: user!!.user_id
// get the view model
@Suppress("UNCHECKED_CAST")
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()
}
val tabs = createProfileTabs(account)
setupTabs(tabs)
setContent(account)
job = launch(job, lifecycleScope, viewModel, profileAdapter)
}
/**
* Shows or hides the error in the profile
*/
private fun showError(errorText: String = getString(R.string.profile_error), show: Boolean = true){
if(show){
binding.profileProgressBar.visibility = View.GONE
binding.motionLayout.transitionToEnd()
binding.errorLayout.errorText.text = errorText
} else if(binding.motionLayout.progress == 1F) {
binding.motionLayout.transitionToStart()
private fun createProfileTabs(account: Account?): Array<Fragment>{
val profileFeedFragment = ProfileFeedFragment()
val argumentsFeed = Bundle().apply {
putSerializable(Account.ACCOUNT_TAG, account)
putSerializable(ProfileFeedFragment.PROFILE_GRID, false)
putSerializable(ProfileFeedFragment.BOOKMARKS, false)
}
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?) {
if(account != null) {
setViews(account)
@ -120,9 +144,17 @@ class ProfileActivity : BaseThemedWithBarActivity() {
api.verifyCredentials()
} catch (exception: IOException) {
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) {
return@launchWhenResumed showError()
Toast.makeText(
applicationContext, "Could not get your profile",
Toast.LENGTH_SHORT
).show()
return@launchWhenResumed
}
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,
) : 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
@GET("/api/v1/statuses/{id}/context")
suspend fun statusComments(

View File

@ -21,8 +21,8 @@ interface UserDao {
@Update
fun updateUser(user: UserDatabaseEntity)
@Query("UPDATE users SET accessToken = :accessToken, refreshToken = :refreshToken WHERE user_id = :id and instance_uri = :instance_uri")
fun updateAccessToken(accessToken: String, refreshToken: String, id: String, instance_uri: String)
@Query("UPDATE users SET accessToken = :accessToken, refreshToken = :refreshToken WHERE user_id = :id and instance_uri = :instanceUri")
fun updateAccessToken(accessToken: String, refreshToken: String, id: String, instanceUri: String)
@Query("SELECT * FROM users")
fun getAll(): List<UserDatabaseEntity>
@ -33,12 +33,12 @@ interface UserDao {
@Query("UPDATE users SET isActive=0")
fun deActivateActiveUsers()
@Query("UPDATE users SET isActive=1 WHERE user_id=:id AND instance_uri=:instance_uri")
fun activateUser(id: String, instance_uri: String)
@Query("UPDATE users SET isActive=1 WHERE user_id=:id AND instance_uri=:instanceUri")
fun activateUser(id: String, instanceUri: String)
@Query("DELETE FROM users WHERE isActive=1")
fun deleteActiveUsers()
@Query("SELECT * FROM users WHERE user_id=:id AND instance_uri=:instance_uri")
fun getUserWithId(id: String, instance_uri: String): UserDatabaseEntity
@Query("SELECT * FROM users WHERE user_id=:id AND instance_uri=:instanceUri")
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")
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")
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:text="@string/default_nposts"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/descriptionTextView" />
@ -89,7 +88,6 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/profilePictureImageView" />
<TextView
android:id="@+id/descriptionTextView"
android:layout_width="match_parent"
@ -123,49 +121,26 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+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>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</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
android:id="@+id/profileRefreshLayout"
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="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"
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 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="status_more_options">More options</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="optional_report_comment">Optional message for mods/admins</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="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="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 -->
<string name="file_not_found">File %1$s was not found</string>