Follow button and followers list (#109)

* wip follow button

* Fixed follow api typo

* Changed follow button text

* FollowButton click animated

* Fixed follow button

* FollowButton hidden if not ready

* Added toats

* Removed useless val

* Refactored follow onClickListener

* Merge with master

* Followers list WIP

* Distinguish followers and following

* Fixed typo

* Open followers list

* Follows list done

* Tests failing

* Fixed follow button test

* Completed url followers

* Fixed tests

* Added test
This commit is contained in:
mjaillot 2020-04-24 12:10:25 +02:00 committed by GitHub
parent 5decd6ae12
commit 3506f034d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 549 additions and 39 deletions

View File

@ -56,6 +56,83 @@ class MockedServerTest {
onView(withId(R.id.nbFollowersTextView)).check(matches(withText("68\nFollowers")))
onView(withId(R.id.accountNameTextView)).check(matches(withText("deerbard_photo")))
}
// WIP TEST
@Test
fun clickFollowButton() {
ActivityScenario.launch(MainActivity::class.java)
Thread.sleep(1000)
//Get initial like count
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<PostViewHolder>
(0, clickChildViewWithId(R.id.username)))
Thread.sleep(1000)
// Unfollow
onView(withId(R.id.followButton)).perform((ViewActions.click()))
Thread.sleep(1000)
onView(withId(R.id.followButton)).check(matches(withText("Follow")))
// Follow
onView(withId(R.id.followButton)).perform((ViewActions.click()))
Thread.sleep(1000)
onView(withId(R.id.followButton)).check(matches(withText("Unfollow")))
}
@Test
fun clickFollowers() {
ActivityScenario.launch(MainActivity::class.java).onActivity{
a -> a.findViewById<TabLayout>(R.id.tabs).getTabAt(4)?.select()
}
Thread.sleep(1000)
// Open followers list
onView(withId(R.id.nbFollowersTextView)).perform((ViewActions.click()))
Thread.sleep(1000)
// Open follower's profile
onView(withText("ete2")).perform((ViewActions.click()))
Thread.sleep(1000)
onView(withId(R.id.accountNameTextView)).check(matches(withText("ete2")))
}
@Test
fun clickOtherUserFollowers() {
ActivityScenario.launch(MainActivity::class.java)
Thread.sleep(1000)
//Get initial like count
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<PostViewHolder>
(0, clickChildViewWithId(R.id.username)))
Thread.sleep(1000)
// Open followers list
onView(withId(R.id.nbFollowersTextView)).perform((ViewActions.click()))
Thread.sleep(1000)
// Open follower's profile
onView(withText("ete2")).perform((ViewActions.click()))
Thread.sleep(1000)
onView(withId(R.id.accountNameTextView)).check(matches(withText("ete2")))
}
@Test
fun clickFollowing() {
ActivityScenario.launch(MainActivity::class.java).onActivity{
a -> a.findViewById<TabLayout>(R.id.tabs).getTabAt(4)?.select()
}
Thread.sleep(1000)
// Open followers list
onView(withId(R.id.nbFollowingTextView)).perform((ViewActions.click()))
Thread.sleep(1000)
// Open following's profile
onView(withText("Dobios")).perform((ViewActions.click()))
Thread.sleep(1000)
onView(withId(R.id.accountNameTextView)).check(matches(withText("Dobios")))
}
@Test
fun testNotificationsList() {

View File

@ -1,6 +1,5 @@
package com.h.pixeldroid.testUtility
import com.h.pixeldroid.objects.Account
import okhttp3.HttpUrl
import okhttp3.mockwebserver.Dispatcher
import okhttp3.mockwebserver.MockResponse
@ -128,6 +127,11 @@ class MockServer {
val reblogJson = """{"id":"156491373246287872","created_at":"2020-04-16T20:00:50.000000Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"public","language":"en","uri":"https:\/\/pixelfed.de\/p\/machintuck\/156491373246287872","url":"https:\/\/pixelfed.de\/p\/machintuck\/156491373246287872","replies_count":1,"reblogs_count":14,"favourites_count":2,"reblogged":true,"favourited":false,"muted":false,"bookmarked":false,"pinned":false,"content":"<a class=\"u-url mention\" href=\"https:\/\/pixelfed.de\/Dobios\" rel=\"external nofollow noopener\">@Dobios<\/a> <a class=\"u-url mention\" href=\"https:\/\/pixelfed.de\/Dante\" rel=\"external nofollow noopener\">@Dante<\/a>","reblog":null,"application":{"name":"web","website":null},"mentions":[{"id":"136800034732773376","url":"https:\/\/pixelfed.de\/Dobios","username":"Dobios","acct":"Dobios"},{"id":"136453537340198912","url":"https:\/\/pixelfed.de\/dante","username":"dante","acct":"dante"}],"tags":[{"name":"mushroom","url":"https:\/\/pixelfed.de\/discover\/tags\/mushroom"},{"name":"commentsstillbroken","url":"https:\/\/pixelfed.de\/discover\/tags\/commentsstillbroken"},{"name":"fixyourapi","url":"https:\/\/pixelfed.de\/discover\/tags\/fixyourapi"},{"name":"pls","url":"https:\/\/pixelfed.de\/discover\/tags\/pls"}],"emojis":[],"card":null,"poll":null,"account":{"id":"145183325781364736","username":"machintuck","acct":"machintuck","display_name":"Arthur","locked":false,"created_at":"2020-03-16T15:06:42.000000Z","followers_count":4,"following_count":4,"statuses_count":5,"note":"","url":"https:\/\/pixelfed.de\/machintuck","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/014\/518\/332\/578\/136\/473\/6\/gbdKtKOhTkNA5UxCzeAQ_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/014\/518\/332\/578\/136\/473\/6\/gbdKtKOhTkNA5UxCzeAQ_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},"media_attachments":[{"id":"19228","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/9b4393bfd32c643a265bd1c557b981f167d60969\/lbOqQOMeHLGmhYgehhZUBJ4JvjtKulh83BA97LoP.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/9b4393bfd32c643a265bd1c557b981f167d60969\/lbOqQOMeHLGmhYgehhZUBJ4JvjtKulh83BA97LoP_thumb.jpeg","text_url":null,"meta":null,"description":null}]}"""
val followRelationshipJson = """{"id":"136800034732773376","following":true,"followed_by":true,"blocking":false,"muting":false,"muting_notifications":null,"requested":false,"domain_blocking":null,"showing_reblogs":null,"endorsed":false}"""
val unfollowRelationshipJson = """{"id":"136800034732773376","following":false,"followed_by":true,"blocking":false,"muting":false,"muting_notifications":null,"requested":false,"domain_blocking":null,"showing_reblogs":null,"endorsed":false}"""
val relationshipJson = """[{"id":"136800034732773376","following":true,"followed_by":true,"blocking":false,"muting":false,"muting_notifications":null,"requested":false,"domain_blocking":null,"showing_reblogs":null,"endorsed":false}]"""
val followersJson = """[{"id":"118664651939647488","username":"ete2","acct":"ete2","display_name":"Christian","locked":false,"created_at":"2020-01-03T10:50:57.000000Z","followers_count":22,"following_count":3,"statuses_count":20,"note":"Nature lover - hobby photographer","url":"https:\/\/pixelfed.de\/ete2","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/011\/866\/465\/193\/964\/748\/8\/pUY03jBlOeOqjNDMWwYM_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/011\/866\/465\/193\/964\/748\/8\/pUY03jBlOeOqjNDMWwYM_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},{"id":"136800034732773376","username":"Dobios","acct":"Dobios","display_name":"Andrew Dobis","locked":false,"created_at":"2020-02-22T11:54:29.000000Z","followers_count":7,"following_count":5,"statuses_count":3,"note":"","url":"https:\/\/pixelfed.de\/Dobios","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/013\/680\/003\/473\/277\/337\/6\/AnBpDi92CAuuNjOYkyqg_avatar.jpeg?v=4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/013\/680\/003\/473\/277\/337\/6\/AnBpDi92CAuuNjOYkyqg_avatar.jpeg?v=4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},{"id":"144813993922531328","username":"Clement","acct":"Clement","display_name":"Andrea","locked":false,"created_at":"2020-03-15T14:39:06.000000Z","followers_count":2,"following_count":4,"statuses_count":0,"note":"","url":"https:\/\/pixelfed.de\/Clement","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false}]"""
val followersAfterJson = """[]"""
fun start() {
server.dispatcher = getDispatcher()
@ -223,6 +227,48 @@ class MockServer {
"application/json; charset=utf-8"
).setResponseCode(200).setBody(unlikeJson)
}
request.path?.matches("/api/v1/accounts/[0-9]*/follow".toRegex()) == true -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(followRelationshipJson)
}
request.path?.matches("/api/v1/accounts/[0-9]*/unfollow".toRegex()) == true -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(unfollowRelationshipJson)
}
request.path?.startsWith("/api/v1/accounts/relationships") == true -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(relationshipJson)
}
request.path?.matches("/api/v1/accounts/[0-9]*/followers\\?limit=[0-9]*".toRegex()) == true -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(followersJson)
}
request.path?.matches("/api/v1/accounts/[0-9]*/followers\\?since_id=[0-9]*&limit=[0-9]*".toRegex()) == true -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(followersAfterJson)
}
request.path?.matches("/api/v1/accounts/[0-9]*/following\\?limit=[0-9]*".toRegex()) == true -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(followersJson)
}
request.path?.matches("/api/v1/accounts/[0-9]*/following\\?since_id=[0-9]*&limit=[0-9]*".toRegex()) == true -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(followersAfterJson)
}
else -> return MockResponse().setResponseCode(404)
}
}

View File

@ -13,9 +13,9 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".PostActivity"/>
<activity android:name=".ProfileActivity"/>
<activity android:name=".FollowsActivity" />
<activity android:name=".PostActivity" />
<activity android:name=".ProfileActivity" />
<activity
android:name=".SettingsActivity"
android:label="@string/title_activity_settings2">
@ -34,8 +34,8 @@
</activity>
<activity
android:name=".LoginActivity"
android:windowSoftInputMode="adjustResize"
android:theme="@style/AppTheme.NoActionBar">
android:theme="@style/AppTheme.NoActionBar"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.VIEW" />

View File

@ -0,0 +1,61 @@
package com.h.pixeldroid
import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.fragments.feeds.FollowsFragment
import com.h.pixeldroid.objects.Account
import com.h.pixeldroid.objects.Account.Companion.ACCOUNT_ID_TAG
import com.h.pixeldroid.objects.Account.Companion.FOLLOWING_TAG
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class FollowsActivity : AppCompatActivity() {
var followsFragment = FollowsFragment()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_followers)
// Get account id
val id = intent.getSerializableExtra(ACCOUNT_ID_TAG) as String?
val following = intent.getSerializableExtra(FOLLOWING_TAG) as Boolean
if(id == null) {
val preferences = this.getSharedPreferences(
"${BuildConfig.APPLICATION_ID}.pref", Context.MODE_PRIVATE
)
val pixelfedAPI = PixelfedAPI.create("${preferences.getString("domain", "")}")
val accessToken = preferences.getString("accessToken", "")
pixelfedAPI.verifyCredentials("Bearer $accessToken").enqueue(object :
Callback<Account> {
override fun onFailure(call: Call<Account>, t: Throwable) {
Log.e("Cannot get account id", t.toString())
}
override fun onResponse(call: Call<Account>, response: Response<Account>) {
if(response.code() == 200) {
val id = response.body()!!.id
launchActivity(id, following)
}
}
})
} else {
launchActivity(id, following)
}
}
private fun launchActivity(id : String, following : Boolean) {val arguments = Bundle()
arguments.putSerializable(ACCOUNT_ID_TAG, id)
arguments.putSerializable(FOLLOWING_TAG, following)
followsFragment.arguments = arguments
supportFragmentManager.beginTransaction()
.add(R.id.followsFragment, followsFragment).commit()
}
}

View File

@ -18,7 +18,7 @@ class ProfileActivity : AppCompatActivity() {
profileFragment = ProfileFragment()
val arguments = Bundle()
arguments.putSerializable("profileTag", account)
arguments.putSerializable(ACCOUNT_TAG, account)
profileFragment.arguments = arguments
supportFragmentManager.beginTransaction()

View File

@ -38,11 +38,28 @@ interface PixelfedAPI {
@Field("grant_type") grant_type: String? = null
): Call<Token>
@FormUrlEncoded
@POST("/api/v1/accounts/{id}/follow")
fun follow(
//The authorization header needs to be of the form "Bearer <token>"
@Path("id") statusId: String,
@Header("Authorization") authorization: String,
@Field("reblogs") reblogs : Boolean = true
) : Call<Relationship>
@POST("/api/v1/accounts/{id}/unfollow")
fun unfollow(
//The authorization header needs to be of the form "Bearer <token>"
@Path("id") statusId: String,
@Header("Authorization") authorization: String
) : Call<Relationship>
@POST("api/v1/statuses/{id}/favourite")
fun likePost(
//The authorization header needs to be of the form "Bearer <token>"
@Header("Authorization") authorization: String,
@Path("id") statusId: String
) : Call<Status>
@POST("/api/v1/statuses/{id}/unfavourite")
@ -147,6 +164,30 @@ interface PixelfedAPI {
@Path("id") account_id: String? = null
): Call<List<Status>>
@GET("/api/v1/accounts/relationships")
fun checkRelationships(
@Header("Authorization") authorization : String,
@Query("id[]") account_ids : List<String>
) : Call<List<Relationship>>
@GET("/api/v1/accounts/{id}/followers")
fun followers(
@Path("id") account_id: String,
@Header("Authorization") authorization: String,
@Query("max_id") max_id: String? = null,
@Query("since_id") since_id: String? = null,
@Query("limit") limit: Number? = null
) : Call<List<Account>>
@GET("/api/v1/accounts/{id}/following")
fun following(
@Path("id") account_id: String,
@Header("Authorization") authorization: String,
@Query("max_id") max_id: String? = null,
@Query("since_id") since_id: String? = null,
@Query("limit") limit: Number? = 40
) : Call<List<Account>>
@GET("/api/v1/accounts/{id}")
fun getAccount(
@Header("Authorization") authorization: String,

View File

@ -11,22 +11,30 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.h.pixeldroid.BuildConfig
import com.h.pixeldroid.FollowsActivity
import com.h.pixeldroid.R
import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.objects.Account
import com.h.pixeldroid.objects.Account.Companion.ACCOUNT_ID_TAG
import com.h.pixeldroid.objects.Account.Companion.ACCOUNT_TAG
import com.h.pixeldroid.objects.Account.Companion.FOLLOWING_TAG
import com.h.pixeldroid.objects.Status
import kotlinx.android.synthetic.main.profile_fragment.view.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class ProfileFragment : Fragment() {
private lateinit var preferences: SharedPreferences
private lateinit var preferences : SharedPreferences
private lateinit var pixelfedAPI : PixelfedAPI
private lateinit var adapter : ProfilePostsRecyclerViewAdapter
private lateinit var recycler : RecyclerView
private var accessToken : String? = null
private var account: Account? = null
override fun onCreateView(
@ -36,19 +44,9 @@ class ProfileFragment : Fragment() {
preferences = this.requireActivity().getSharedPreferences(
"${BuildConfig.APPLICATION_ID}.pref", Context.MODE_PRIVATE
)
account = arguments?.getSerializable("profileTag") as Account?
account = arguments?.getSerializable(ACCOUNT_TAG) as Account?
val view = inflater.inflate(R.layout.profile_fragment, container, false)
if(account == null) {
// Edit button redirects to Pixelfed's "edit account" page
val editButton: Button = view.findViewById(R.id.editButton)
editButton.visibility = View.VISIBLE
val followButton: Button = view.findViewById(R.id.followButton)
followButton.visibility = View.GONE
editButton.setOnClickListener((View.OnClickListener { onClickEditButton() }))
}
// Set RecyclerView as a grid with 3 columns
recycler = view.findViewById(R.id.profilePostsRecyclerView)
recycler.layoutManager = GridLayoutManager(context, 3)
@ -61,8 +59,13 @@ class ProfileFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val pixelfedAPI = PixelfedAPI.create("${preferences.getString("domain", "")}")
val accessToken = preferences.getString("accessToken", "")
pixelfedAPI = PixelfedAPI.create("${preferences.getString("domain", "")}")
accessToken = preferences.getString("accessToken", "")
// On click open followers list
view.nbFollowersTextView.setOnClickListener{ onClickFollowers() }
// On click open followers list
view.nbFollowingTextView.setOnClickListener{ onClickFollowing() }
if(account == null) {
pixelfedAPI.verifyCredentials("Bearer $accessToken")
@ -82,16 +85,20 @@ class ProfileFragment : Fragment() {
Log.e("ProfileFragment:", t.toString())
}
})
// Edit button redirects to Pixelfed's "edit account" page
val editButton: Button = view.findViewById(R.id.editButton)
editButton.visibility = View.VISIBLE
editButton.setOnClickListener{ onClickEditButton() }
} else {
account!!.activateFollow(view, requireContext(), pixelfedAPI, accessToken)
account!!.setContent(view)
setPosts(account!!)
}
}
// Populate profile page with user's posts
private fun setPosts(account: Account) {
val pixelfedAPI = PixelfedAPI.create("${preferences.getString("domain", "")}")
val accessToken = preferences.getString("accessToken", "")
pixelfedAPI.accountPosts("Bearer $accessToken", account_id = account.id).enqueue(object :
Callback<List<Status>> {
override fun onFailure(call: Call<List<Status>>, t: Throwable) {
@ -124,4 +131,20 @@ class ProfileFragment : Fragment() {
Log.e("ProfileFragment", text)
}
}
private fun onClickFollowers() {
val intent = Intent(context, FollowsActivity::class.java)
intent.putExtra(FOLLOWING_TAG, true)
intent.putExtra(ACCOUNT_ID_TAG, account?.id)
ContextCompat.startActivity(requireContext(), intent, null)
}
private fun onClickFollowing() {
val intent = Intent(context, FollowsActivity::class.java)
intent.putExtra(FOLLOWING_TAG, false)
intent.putExtra(ACCOUNT_ID_TAG, account?.id)
ContextCompat.startActivity(requireContext(), intent, null)
}
}

View File

@ -0,0 +1,125 @@
package com.h.pixeldroid.fragments.feeds
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.lifecycle.LiveData
import androidx.recyclerview.widget.RecyclerView
import androidx.lifecycle.Observer
import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList
import com.bumptech.glide.Glide
import com.bumptech.glide.ListPreloader
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.util.ViewPreloadSizeProvider
import com.h.pixeldroid.R
import com.h.pixeldroid.objects.Account
import com.h.pixeldroid.objects.Account.Companion.ACCOUNT_ID_TAG
import com.h.pixeldroid.objects.Account.Companion.FOLLOWING_TAG
import kotlinx.android.synthetic.main.fragment_follows.view.*
import retrofit2.Call
class FollowsFragment : FeedFragment<Account, FollowsFragment.FollowsRecyclerViewAdapter.ViewHolder>() {
lateinit var profilePicRequest: RequestBuilder<Drawable>
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
//RequestBuilder that is re-used for every image
profilePicRequest = Glide.with(this)
.asDrawable().apply(RequestOptions().circleCrop())
.placeholder(R.drawable.ic_default_user)
adapter = FollowsRecyclerViewAdapter()
list.adapter = adapter
//Make Glide be aware of the recyclerview and pre-load images
val sizeProvider: ListPreloader.PreloadSizeProvider<Account> = ViewPreloadSizeProvider()
val preloader: RecyclerViewPreloader<Account> = RecyclerViewPreloader(
Glide.with(this), adapter, sizeProvider, 4
)
list.addOnScrollListener(preloader)
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val id = arguments?.getSerializable(ACCOUNT_ID_TAG) as String
val following = arguments?.getSerializable(FOLLOWING_TAG) as Boolean
content = makeContent(id, following)
content.observe(viewLifecycleOwner,
Observer { c ->
adapter.submitList(c)
//after a refresh is done we need to stop the pull to refresh spinner
swipeRefreshLayout.isRefreshing = false
})
}
private fun makeContent(id : String, following : Boolean) : LiveData<PagedList<Account>> {
fun makeInitialCall(requestedLoadSize: Int): Call<List<Account>> {
if(following) {
return pixelfedAPI.followers(id, "Bearer $accessToken",
limit = requestedLoadSize)
} else {
return pixelfedAPI.following(id, "Bearer $accessToken",
limit = requestedLoadSize)
}
}
fun makeAfterCall(requestedLoadSize: Int, key: String): Call<List<Account>> {
if(following) {
return pixelfedAPI.followers(id, "Bearer $accessToken",
since_id = key, limit = requestedLoadSize)
} else {
return pixelfedAPI.following(id, "Bearer $accessToken",
since_id = key, limit = requestedLoadSize)
}
}
val config: PagedList.Config = PagedList.Config.Builder().setPageSize(10).build()
factory = FeedDataSourceFactory(::makeInitialCall, ::makeAfterCall)
return LivePagedListBuilder(factory, config).build()
}
inner class FollowsRecyclerViewAdapter : FeedsRecyclerViewAdapter<Account,FollowsRecyclerViewAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.fragment_follows, parent, false)
context = view.context
return ViewHolder(view)
}
override fun onBindViewHolder(holder : ViewHolder, position : Int) {
val account = getItem(position) ?: return
profilePicRequest.load(account.avatar_static).into(holder.avatar)
holder.username.text = account.username
holder.mView.setOnClickListener { account.openProfile(context) }
}
inner class ViewHolder(val mView : View) : RecyclerView.ViewHolder(mView) {
val avatar : ImageView = mView.follows_avatar
val username : TextView = mView.follows_username
}
override fun getPreloadItems(position : Int) : MutableList<Account> {
val account = getItem(position) ?: return mutableListOf()
return mutableListOf(account)
}
override fun getPreloadRequestBuilder(item : Account) : RequestBuilder<*>? {
return profilePicRequest.load(item.avatar_static)
}
}
}

View File

@ -7,11 +7,13 @@ import android.util.Log
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat.startActivity
import com.h.pixeldroid.ProfileActivity
import com.h.pixeldroid.R
import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.utils.ImageConverter
import kotlinx.android.synthetic.main.profile_fragment.view.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@ -24,7 +26,7 @@ https://docs.joinmastodon.org/entities/account/
data class Account(
//Base attributes
val id: String,
override val id: String,
val username: String,
val acct: String,
val url: String, //HTTPS URL
@ -48,9 +50,11 @@ data class Account(
val fields: List<Field>? = emptyList(),
val bot: Boolean = false,
val source: Source? = null
) : Serializable {
) : Serializable, FeedContent() {
companion object {
const val ACCOUNT_TAG = "AccountTag"
const val ACCOUNT_ID_TAG = "AccountIdTag"
const val FOLLOWING_TAG = "FollowingTag"
/**
* @brief Opens an activity of the profile withn the given id
@ -83,9 +87,10 @@ data class Account(
// Open profile activity with given account
fun openProfile(context: Context) {
val intent = Intent(context, ProfileActivity::class.java)
intent.putExtra(Account.ACCOUNT_TAG, this)
intent.putExtra(ACCOUNT_TAG, this)
startActivity(context, intent, null)
}
// Populate myProfile page with user's data
fun setContent(view: View) {
val profilePicture = view.findViewById<ImageView>(R.id.profilePictureImageView)
@ -110,5 +115,86 @@ data class Account(
nbFollowing.text = "${this.following_count}\nFollowing"
nbFollowing.setTypeface(null, Typeface.BOLD)
}
}
// Activate follow button
fun activateFollow(
view : View,
context : Context,
api : PixelfedAPI,
credential : String?
) {
// Get relationship between the two users (credential and this) and set followButton accordingly
api.checkRelationships("Bearer $credential", listOf(id)).enqueue(object : Callback<List<Relationship>> {
override fun onFailure(call: Call<List<Relationship>>, t: Throwable) {
Log.e("FOLLOW ERROR", t.toString())
Toast.makeText(context,"Could not get follow status", Toast.LENGTH_SHORT).show()
}
override fun onResponse(call: Call<List<Relationship>>, response: Response<List<Relationship>>) {
if(response.code() == 200) {
if(response.body()!!.isNotEmpty()) {
if (response.body()!![0].following) {
view.followButton.text = "Unfollow"
setOnClickUnfollow(view, api, context, credential)
} else {
view.followButton.text = "Follow"
setOnClickFollow(view, api, context, credential)
}
view.followButton.visibility = View.VISIBLE
}
} else {
Toast.makeText(context, "Could not display follow button", Toast.LENGTH_SHORT)
.show()
}
}
})
}
private fun setOnClickFollow(view: View, api: PixelfedAPI, context: Context, credential: String?) {
view.followButton.setOnClickListener {
api.follow(id, "Bearer $credential").enqueue(object : Callback<Relationship> {
override fun onFailure(call: Call<Relationship>, t: Throwable) {
Log.e("FOLLOW ERROR", t.toString())
Toast.makeText(context, "Could not follow", Toast.LENGTH_SHORT).show()
}
override fun onResponse(
call: Call<Relationship>,
response: Response<Relationship>
) {
if (response.code() == 200) {
view.followButton.text = "Unfollow"
setOnClickUnfollow(view, api, context, credential)
} else if (response.code() == 403) {
Toast.makeText(context, "This action is not allowed", Toast.LENGTH_SHORT)
.show()
}
}
})
}
}
private fun setOnClickUnfollow(view: View, api: PixelfedAPI, context: Context, credential: String?) {
view.followButton.setOnClickListener {
api.unfollow(id, "Bearer $credential").enqueue(object : Callback<Relationship> {
override fun onFailure(call: Call<Relationship>, t: Throwable) {
Log.e("UNFOLLOW ERROR", t.toString())
Toast.makeText(context, "Could not unfollow", Toast.LENGTH_SHORT).show()
}
override fun onResponse(
call: Call<Relationship>,
response: Response<Relationship>
) {
if (response.code() == 200) {
view.followButton.text = "Follow"
setOnClickFollow(view, api, context, credential)
} else if (response.code() == 401) {
Toast.makeText(context, "The access token is invalid", Toast.LENGTH_SHORT)
.show()
}
}
})
}
}
}

View File

@ -0,0 +1,18 @@
package com.h.pixeldroid.objects
import java.io.Serializable
data class Relationship(
// Required atributes
val id: String,
val following: Boolean,
val requested: Boolean,
val endorsed: Boolean,
val followed_by: Boolean,
val muting: Boolean,
val muting_notifications: Boolean,
val showing_reblogs: Boolean,
val blocking: Boolean,
val domain_blocking: Boolean,
val blocked_by: Boolean
) : Serializable

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/followsFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".FollowsActivity">
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<ImageView
android:id="@+id/follows_avatar"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:scaleType="centerCrop"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_default_user" />
<TextView
android:id="@+id/follows_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="28dp"
android:text="TextView"
app:layout_constraintStart_toEndOf="@+id/follows_avatar"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -6,8 +6,7 @@
android:name="com.h.pixeldroid.fragments.ProfilePostsFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_margin="16dp"
app:layoutManager="LinearLayoutManager"
tools:context=".fragments.ProfilePostsFragment"
tools:listitem="@layout/fragment_profile_posts" />

View File

@ -11,7 +11,6 @@
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/linearlayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
@ -35,7 +34,6 @@
tools:srcCompat="@tools:sample/avatars" />
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="10"
@ -110,11 +108,11 @@
<Button
android:id="@+id/followButton"
android:layout_width="100dp"
android:layout_height="30dp"
android:background="@color/browser_actions_divider_color"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Follow"
android:textColor="@color/colorPrimary"
android:visibility="invisible"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@ -135,7 +133,6 @@
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/linearLayout2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
@ -164,9 +161,7 @@
android:id="@+id/profilePostsRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="5dp"
android:layout_marginTop="16dp"
android:layout_marginRight="5dp"
android:layout_margin="5dp"
app:layoutManager="LinearLayoutManager"
tools:context=".fragments.ProfileFragment"
tools:listitem="@layout/fragment_profile_posts" />