* Search

* refactor

* add hashtags

* clean up xml, add some spacing to hashtag list

* Fix invalidation causing failures

* Move Tag's id to read-only member value

* Refactor

* Add test, rename things to be more sensible
This commit is contained in:
Wv5twkFEKh54vo4tta9yu7dHa3 2020-04-30 17:54:21 +02:00 committed by GitHub
parent 92c534ca1b
commit ce0b914d78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 844 additions and 73 deletions

View File

@ -106,7 +106,7 @@ class LoginCheckIntent {
onView(withId(R.id.whatsAnInstanceTextView)).perform(scrollTo()).perform(click())
Thread.sleep(10000)
Thread.sleep(3000)
intended(expectedIntent)

View File

@ -4,6 +4,7 @@ import android.content.Context
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition
import androidx.test.espresso.matcher.ViewMatchers.*
@ -48,6 +49,52 @@ class MockedServerTest {
activityScenario = ActivityScenario.launch(MainActivity::class.java)
}
@Test
fun searchPosts() {
activityScenario.onActivity{
a -> a.findViewById<TabLayout>(R.id.tabs).getTabAt(1)?.select()
}
Thread.sleep(1000)
onView(withId(R.id.searchEditText)).perform(ViewActions.replaceText("caturday"), ViewActions.closeSoftKeyboard())
onView(withId(R.id.searchButton)).perform(click())
Thread.sleep(3000)
onView(first(withId(R.id.username))).check(matches(withText("Memo")))
}
@Test
fun searchHashtags() {
activityScenario.onActivity{
a -> a.findViewById<TabLayout>(R.id.tabs).getTabAt(1)?.select()
}
Thread.sleep(1000)
onView(withId(R.id.searchEditText)).perform(ViewActions.replaceText("#caturday"), ViewActions.closeSoftKeyboard())
onView(withId(R.id.searchButton)).perform(click())
Thread.sleep(3000)
onView(first(withId(R.id.tag_name))).check(matches(withText("#caturday")))
}
@Test
fun searchAccounts() {
activityScenario.onActivity{
a -> a.findViewById<TabLayout>(R.id.tabs).getTabAt(1)?.select()
}
Thread.sleep(1000)
onView(withId(R.id.searchEditText)).perform(ViewActions.replaceText("@dansup"), ViewActions.closeSoftKeyboard())
onView(withId(R.id.searchButton)).perform(click())
Thread.sleep(3000)
onView(first(withId(R.id.account_entry_username))).check(matches(withText("dansup")))
}
@Test
fun testFollowersTextView() {
activityScenario.onActivity{
@ -58,7 +105,7 @@ 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)

File diff suppressed because one or more lines are too long

View File

@ -35,7 +35,6 @@
android:theme="@style/AppTheme.Launcher">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
@ -54,6 +53,17 @@
android:scheme="@string/auth_scheme" />
</intent-filter>
</activity>
<activity
android:name=".SearchActivity"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable" />
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.h.pixeldroid.fileprovider"

View File

@ -5,7 +5,7 @@ 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.fragments.feeds.AccountListFragment
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
@ -14,7 +14,7 @@ import retrofit2.Callback
import retrofit2.Response
class FollowsActivity : AppCompatActivity() {
var followsFragment = FollowsFragment()
var followsFragment = AccountListFragment()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View File

@ -16,17 +16,20 @@ import com.google.android.material.navigation.NavigationView
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import com.h.pixeldroid.fragments.NewPostFragment
import com.h.pixeldroid.fragments.feeds.HomeFragment
import com.h.pixeldroid.fragments.ProfileFragment
import com.h.pixeldroid.fragments.SearchDiscoverFragment
import com.h.pixeldroid.fragments.feeds.PostsFeedFragment
import com.h.pixeldroid.fragments.feeds.NotificationsFragment
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener {
private lateinit var drawerLayout: DrawerLayout
private lateinit var viewPager: ViewPager2
private lateinit var tabLayout: TabLayout
private lateinit var preferences: SharedPreferences
private val searchDiscoverFragment: SearchDiscoverFragment = SearchDiscoverFragment()
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.AppTheme_NoActionBar)
@ -47,8 +50,8 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
navigationView.setNavigationItemSelectedListener(this)
val tabs = arrayOf(
HomeFragment(),
Fragment(),
PostsFeedFragment(),
searchDiscoverFragment,
NewPostFragment(),
NotificationsFragment(),
ProfileFragment()

View File

@ -15,7 +15,7 @@ class PostActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_post)
val status = intent.getSerializableExtra(POST_TAG) as Status
val status = intent.getSerializableExtra(POST_TAG) as Status?
postFragment = PostFragment()
val arguments = Bundle()

View File

@ -0,0 +1,104 @@
package com.h.pixeldroid
import android.app.SearchManager
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import com.h.pixeldroid.fragments.feeds.search.SearchAccountFragment
import com.h.pixeldroid.fragments.feeds.search.SearchHashtagFragment
import com.h.pixeldroid.fragments.feeds.search.SearchPostsFragment
import com.h.pixeldroid.objects.Results
class SearchActivity : AppCompatActivity() {
private lateinit var preferences: SharedPreferences
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search)
var query = intent.getSerializableExtra("searchFeed") as String
query = query.trim()
val searchType = if (query.startsWith("#")){
Results.SearchType.hashtags
} else if(query.startsWith("@")){
Results.SearchType.accounts
} else Results.SearchType.statuses
if(searchType != Results.SearchType.statuses) query = query.drop(1)
val tabs = createSearchTabs(query)
setupTabs(tabs, searchType)
}
private fun createSearchTabs(query: String): Array<Fragment>{
val searchFeedFragment =
SearchPostsFragment()
val searchAccountListFragment =
SearchAccountFragment()
val searchHashtagFragment: Fragment = SearchHashtagFragment()
val arguments = Bundle()
arguments.putSerializable("searchFeed", query)
searchFeedFragment.arguments = arguments
searchAccountListFragment.arguments = arguments
searchHashtagFragment.arguments = arguments
return arrayOf(
searchFeedFragment,
searchAccountListFragment,
searchHashtagFragment
)
}
private fun setupTabs(
tabs: Array<Fragment>,
searchType: Results.SearchType
){
val viewPager = findViewById<ViewPager2>(R.id.search_view_pager)
viewPager.adapter = object : FragmentStateAdapter(this) {
override fun createFragment(position: Int): Fragment {
return tabs[position]
}
override fun getItemCount(): Int {
return 3
}
}
val tabLayout = findViewById<TabLayout>(R.id.search_tabs)
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
when(position){
0 -> tab.text = "POSTS"
1 -> tab.text = "ACCOUNTS"
2 -> tab.text = "HASHTAGS"
}
}.attach()
when(searchType){
Results.SearchType.statuses -> tabLayout.selectTab(tabLayout.getTabAt(0))
Results.SearchType.accounts -> tabLayout.selectTab(tabLayout.getTabAt(1))
Results.SearchType.hashtags -> tabLayout.selectTab(tabLayout.getTabAt(2))
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
if (intent.action == Intent.ACTION_SEARCH) {
intent.getStringExtra(SearchManager.QUERY)?.also { query ->
search(query)
}
}
}
private fun search(query: String){
Log.e("search", "")
}
}

View File

@ -135,6 +135,22 @@ interface PixelfedAPI {
@Query("local") local: Boolean? = null
): Call<List<Status>>
@GET("/api/v2/search")
fun search(
//The authorization header needs to be of the form "Bearer <token>"
@Header("Authorization") authorization: String,
@Query("account_id") account_id: String? = null,
@Query("max_id") max_id: String? = null,
@Query("min_id") min_id: String? = null,
@Query("type") type: Results.SearchType? = null,
@Query("exclude_unreviewed") exclude_unreviewed: Boolean? = null,
@Query("q") q: String,
@Query("resolve") resolve: Boolean? = null,
@Query("limit") limit: String? = null,
@Query("offset") offset: Int? = null,
@Query("following") following: Boolean? = null
): Call<Results>
/*
Note: as of 0.10.8, Pixelfed does not seem to respect the Mastodon API documentation,
you *need* to pass one of the so-called "optional" arguments. See:

View File

@ -0,0 +1,53 @@
package com.h.pixeldroid.fragments
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.EditText
import androidx.fragment.app.Fragment
import com.h.pixeldroid.BuildConfig
import com.h.pixeldroid.R
import com.h.pixeldroid.SearchActivity
import com.h.pixeldroid.api.PixelfedAPI
/**
* This fragment lets you search and use PixelFed's Discover feature
*/
class SearchDiscoverFragment : Fragment() {
lateinit var api: PixelfedAPI
private lateinit var preferences: SharedPreferences
private lateinit var accessToken: String
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_search, container, false)
val button = view.findViewById<Button>(R.id.searchButton)
val search = view.findViewById<EditText>(R.id.searchEditText)
button.setOnClickListener {
val intent = Intent(context, SearchActivity::class.java)
intent.putExtra("searchFeed", search.text.toString())
startActivity(intent)
}
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
preferences = requireActivity().getSharedPreferences(
"${BuildConfig.APPLICATION_ID}.pref", Context.MODE_PRIVATE
)
api = PixelfedAPI.create("${preferences.getString("domain", "")}")
accessToken = preferences.getString("accessToken", "") ?: ""
}
}

View File

@ -22,10 +22,10 @@ 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 kotlinx.android.synthetic.main.account_list_entry.view.*
import retrofit2.Call
class FollowsFragment : FeedFragment<Account, FollowsFragment.FollowsRecyclerViewAdapter.ViewHolder>() {
open class AccountListFragment : FeedFragment<Account, AccountListFragment.FollowsRecyclerViewAdapter.ViewHolder>() {
lateinit var profilePicRequest: RequestBuilder<Drawable>
override fun onCreateView(
@ -45,7 +45,7 @@ class FollowsFragment : FeedFragment<Account, FollowsFragment.FollowsRecyclerVie
//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
Glide.with(this), adapter as AccountListFragment.FollowsRecyclerViewAdapter, sizeProvider, 4
)
list.addOnScrollListener(preloader)
@ -54,9 +54,7 @@ class FollowsFragment : FeedFragment<Account, FollowsFragment.FollowsRecyclerVie
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 = makeContent()
content.observe(viewLifecycleOwner,
Observer { c ->
@ -66,35 +64,54 @@ class FollowsFragment : FeedFragment<Account, FollowsFragment.FollowsRecyclerVie
})
}
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)
}
}
internal open fun makeContent(): LiveData<PagedList<Account>> {
val id = arguments?.getSerializable(ACCOUNT_ID_TAG) as String
val following = arguments?.getSerializable(FOLLOWING_TAG) as Boolean
val (makeInitialCall, makeAfterCall)
= makeCalls(following, id)
val config: PagedList.Config = PagedList.Config.Builder().setPageSize(10).build()
factory = FeedDataSourceFactory(::makeInitialCall, ::makeAfterCall)
val dataSource = FeedDataSource(makeInitialCall, makeAfterCall)
factory = FeedDataSourceFactory(dataSource)
return LivePagedListBuilder(factory, config).build()
}
inner class FollowsRecyclerViewAdapter : FeedsRecyclerViewAdapter<Account,FollowsRecyclerViewAdapter.ViewHolder>() {
private fun makeCalls(following: Boolean, id: String):
Pair<(Int) -> Call<List<Account>>, (Int, String) -> Call<List<Account>>> {
val makeInitialCall: (Int) -> Call<List<Account>> =
if (following) { requestedLoadSize ->
pixelfedAPI.followers(
id, "Bearer $accessToken",
limit = requestedLoadSize
)
} else { requestedLoadSize ->
pixelfedAPI.following(
id, "Bearer $accessToken",
limit = requestedLoadSize
)
}
val makeAfterCall: (Int, String) -> Call<List<Account>> =
if (following) { requestedLoadSize, key ->
pixelfedAPI.followers(
id, "Bearer $accessToken",
since_id = key, limit = requestedLoadSize
)
} else { requestedLoadSize, key ->
pixelfedAPI.following(
id, "Bearer $accessToken",
since_id = key, limit = requestedLoadSize
)
}
return Pair(makeInitialCall, makeAfterCall)
}
inner class FollowsRecyclerViewAdapter : FeedsRecyclerViewAdapter<Account,FollowsRecyclerViewAdapter.ViewHolder>(),
ListPreloader.PreloadModelProvider<Account> {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.fragment_follows, parent, false)
.inflate(R.layout.account_list_entry, parent, false)
context = view.context
return ViewHolder(view)
}
@ -109,8 +126,8 @@ class FollowsFragment : FeedFragment<Account, FollowsFragment.FollowsRecyclerVie
}
inner class ViewHolder(val mView : View) : RecyclerView.ViewHolder(mView) {
val avatar : ImageView = mView.follows_avatar
val username : TextView = mView.follows_username
val avatar : ImageView = mView.account_entry_avatar
val username : TextView = mView.account_entry_username
}
override fun getPreloadItems(position : Int) : MutableList<Account> {

View File

@ -25,6 +25,7 @@ import com.h.pixeldroid.BuildConfig
import com.h.pixeldroid.R
import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.objects.FeedContent
import com.h.pixeldroid.objects.Status
import kotlinx.android.synthetic.main.fragment_feed.view.*
import retrofit2.Call
import retrofit2.Callback
@ -33,7 +34,7 @@ import retrofit2.Response
open class FeedFragment<T: FeedContent, VH: RecyclerView.ViewHolder?>: Fragment() {
lateinit var content: LiveData<PagedList<T>>
lateinit var factory: FeedDataSourceFactory
lateinit var factory: FeedDataSourceFactory<FeedDataSource>
protected var accessToken: String? = null
protected lateinit var pixelfedAPI: PixelfedAPI
@ -42,7 +43,7 @@ open class FeedFragment<T: FeedContent, VH: RecyclerView.ViewHolder?>: Fragment(
protected lateinit var list : RecyclerView
protected lateinit var adapter : FeedsRecyclerViewAdapter<T, VH>
protected lateinit var swipeRefreshLayout: SwipeRefreshLayout
private lateinit var loadingIndicator: ProgressBar
internal lateinit var loadingIndicator: ProgressBar
override fun onCreateView(
inflater: LayoutInflater,
@ -75,11 +76,14 @@ open class FeedFragment<T: FeedContent, VH: RecyclerView.ViewHolder?>: Fragment(
}
inner class FeedDataSource(private val makeInitialCall: (Int) -> Call<List<T>>,
private val makeAfterCall: (Int, String) -> Call<List<T>>
open inner class FeedDataSource(private val makeInitialCall: ((Int) -> Call<List<T>>)?,
private val makeAfterCall: ((Int, String) -> Call<List<T>>)?
): ItemKeyedDataSource<String, T>() {
open fun newSource(): FeedDataSource {
return FeedDataSource(makeInitialCall, makeAfterCall)
}
//We use the id as the key
override fun getKey(item: T): String {
return item.id
@ -89,13 +93,13 @@ open class FeedFragment<T: FeedContent, VH: RecyclerView.ViewHolder?>: Fragment(
params: LoadInitialParams<String>,
callback: LoadInitialCallback<T>
) {
enqueueCall(makeInitialCall(params.requestedLoadSize), callback)
enqueueCall(makeInitialCall!!(params.requestedLoadSize), callback)
}
//This is called to when we get to the bottom of the loaded content, so we want statuses
//older than the given key (params.key)
override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<T>) {
enqueueCall(makeAfterCall(params.requestedLoadSize, params.key), callback)
enqueueCall(makeAfterCall!!(params.requestedLoadSize, params.key), callback)
}
override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<T>) {
@ -124,16 +128,15 @@ open class FeedFragment<T: FeedContent, VH: RecyclerView.ViewHolder?>: Fragment(
})
}
}
inner class FeedDataSourceFactory(
private val makeInitialCall: (Int) -> Call<List<T>>,
private val makeAfterCall: (Int, String) -> Call<List<T>>
open inner class FeedDataSourceFactory<DS: FeedDataSource>(
private val dataSource: DS
): DataSource.Factory<String, T>() {
lateinit var liveData: MutableLiveData<FeedDataSource>
lateinit var liveData: MutableLiveData<DS>
override fun create(): DataSource<String, T> {
val dataSource = FeedDataSource(::makeInitialCall.get(), ::makeAfterCall.get())
val dataSource = dataSource.newSource()
liveData = MutableLiveData()
liveData.postValue(dataSource)
liveData.postValue(dataSource as DS)
return dataSource
}
@ -151,7 +154,7 @@ abstract class FeedsRecyclerViewAdapter<T: FeedContent, VH : RecyclerView.ViewHo
return oldItem == newItem
}
}
), PreloadModelProvider<T> {
){
protected lateinit var context: Context
}

View File

@ -59,7 +59,7 @@ class NotificationsFragment : FeedFragment<Notification, NotificationsFragment.N
//Make Glide be aware of the recyclerview and pre-load images
val sizeProvider: ListPreloader.PreloadSizeProvider<Notification> = ViewPreloadSizeProvider()
val preloader: RecyclerViewPreloader<Notification> = RecyclerViewPreloader(
Glide.with(this), adapter, sizeProvider, 4
Glide.with(this), adapter as NotificationsFragment.NotificationsRecyclerViewAdapter, sizeProvider, 4
)
list.addOnScrollListener(preloader)
@ -89,14 +89,16 @@ class NotificationsFragment : FeedFragment<Notification, NotificationsFragment.N
}
val config: PagedList.Config = PagedList.Config.Builder().setPageSize(10).build()
factory = FeedDataSourceFactory(::makeInitialCall, ::makeAfterCall)
val dataSource = FeedDataSource(::makeInitialCall, ::makeAfterCall)
factory = FeedDataSourceFactory(dataSource)
return LivePagedListBuilder(factory, config).build()
}
/**
* [RecyclerView.Adapter] that can display a [Notification]
*/
inner class NotificationsRecyclerViewAdapter: FeedsRecyclerViewAdapter<Notification, NotificationsRecyclerViewAdapter.ViewHolder>() {
inner class NotificationsRecyclerViewAdapter: FeedsRecyclerViewAdapter<Notification, NotificationsRecyclerViewAdapter.ViewHolder>(),
ListPreloader.PreloadModelProvider<Notification> {
private val mOnClickListener: View.OnClickListener

View File

@ -24,7 +24,7 @@ import com.h.pixeldroid.objects.Status
import retrofit2.Call
class HomeFragment : FeedFragment<Status, PostViewHolder>() {
open class PostsFeedFragment : FeedFragment<Status, PostViewHolder>() {
lateinit var picRequest: RequestBuilder<Drawable>
@ -39,14 +39,14 @@ class HomeFragment : FeedFragment<Status, PostViewHolder>() {
.asDrawable().fitCenter()
.placeholder(ColorDrawable(Color.GRAY))
adapter = HomeRecyclerViewAdapter(this)
adapter = PostsFeedRecyclerViewAdapter(this)
list.adapter = adapter
//Make Glide be aware of the recyclerview and pre-load images
val sizeProvider: ListPreloader.PreloadSizeProvider<Status> = ViewPreloadSizeProvider()
val preloader: RecyclerViewPreloader<Status> = RecyclerViewPreloader(
Glide.with(this), adapter, sizeProvider, 4
Glide.with(this), adapter as PostsFeedFragment.PostsFeedRecyclerViewAdapter, sizeProvider, 4
)
list.addOnScrollListener(preloader)
@ -64,7 +64,7 @@ class HomeFragment : FeedFragment<Status, PostViewHolder>() {
})
}
private fun makeContent(): LiveData<PagedList<Status>> {
internal open fun makeContent(): LiveData<PagedList<Status>> {
fun makeInitialCall(requestedLoadSize: Int): Call<List<Status>> {
return pixelfedAPI
.timelineHome("Bearer $accessToken", limit="$requestedLoadSize")
@ -75,15 +75,17 @@ class HomeFragment : FeedFragment<Status, PostViewHolder>() {
limit="$requestedLoadSize")
}
val config: PagedList.Config = PagedList.Config.Builder().setPageSize(10).build()
factory = FeedDataSourceFactory(::makeInitialCall, ::makeAfterCall)
val dataSource = FeedDataSource(::makeInitialCall, ::makeAfterCall)
factory = FeedDataSourceFactory(dataSource)
return LivePagedListBuilder(factory, config).build()
}
/**
* [RecyclerView.Adapter] that can display a list of Statuses
*/
inner class HomeRecyclerViewAdapter(private val homeFragment: HomeFragment)
: FeedsRecyclerViewAdapter<Status, PostViewHolder>() {
inner class PostsFeedRecyclerViewAdapter(private val postsFeedFragment: PostsFeedFragment)
: FeedsRecyclerViewAdapter<Status, PostViewHolder>(),
ListPreloader.PreloadModelProvider<Status> {
private val api = pixelfedAPI
private val credential = "Bearer $accessToken"
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PostViewHolder {
@ -104,7 +106,7 @@ class HomeFragment : FeedFragment<Status, PostViewHolder>() {
holder.postPic.maxHeight = metrics.heightPixels
//Setup the post layout
post.setupPost(holder.postView, picRequest, homeFragment)
post.setupPost(holder.postView, picRequest, postsFeedFragment)
//Set the special HTML text
post.setDescription(holder.postView, api, credential)

View File

@ -0,0 +1,100 @@
package com.h.pixeldroid.fragments.feeds.search
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.lifecycle.LiveData
import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList
import com.h.pixeldroid.fragments.feeds.AccountListFragment
import com.h.pixeldroid.fragments.feeds.FeedFragment
import com.h.pixeldroid.objects.Account
import com.h.pixeldroid.objects.Results
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class SearchAccountFragment: AccountListFragment(){
private lateinit var query: String
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
query = arguments?.getSerializable("searchFeed") as String
return view
}
inner class SearchAccountListDataSource: FeedDataSource(null, null){
override fun newSource(): FeedDataSource {
return SearchAccountListDataSource()
}
private fun makeInitialCall(requestedLoadSize: Int): Call<Results> {
return pixelfedAPI
.search("Bearer $accessToken",
limit="$requestedLoadSize", q=query,
type = Results.SearchType.accounts)
}
private fun makeAfterCall(requestedLoadSize: Int, key: String): Call<Results> {
return pixelfedAPI
.search("Bearer $accessToken", max_id=key,
limit="$requestedLoadSize", q = query,
type = Results.SearchType.accounts)
}
override fun loadInitial(
params: LoadInitialParams<String>,
callback: LoadInitialCallback<Account>
) {
enqueueCall(makeInitialCall(params.requestedLoadSize), callback)
}
//This is called to when we get to the bottom of the loaded content, so we want statuses
//older than the given key (params.key)
override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<Account>) {
enqueueCall(makeAfterCall(params.requestedLoadSize, params.key), callback)
}
private fun enqueueCall(call: Call<Results>, callback: LoadCallback<Account>){
call.enqueue(object : Callback<Results> {
override fun onResponse(call: Call<Results>, response: Response<Results>) {
if (response.code() == 200) {
val notifications = response.body()!!.accounts as ArrayList<Account>
callback.onResult(notifications as List<Account>)
} else{
Toast.makeText(context,"Something went wrong while loading", Toast.LENGTH_SHORT).show()
}
swipeRefreshLayout.isRefreshing = false
loadingIndicator.visibility = View.GONE
}
override fun onFailure(call: Call<Results>, t: Throwable) {
Toast.makeText(context,"Could not get feed", Toast.LENGTH_SHORT).show()
Log.e("FeedFragment", t.toString())
}
})
}
}
override fun makeContent(): LiveData<PagedList<Account>> {
val config: PagedList.Config = PagedList.Config.Builder().setPageSize(10).build()
factory =
FeedFragment<Account, FollowsRecyclerViewAdapter.ViewHolder>()
.FeedDataSourceFactory(
SearchAccountListDataSource()
)
return LivePagedListBuilder(factory, config).build()
}
}

View File

@ -0,0 +1,159 @@
package com.h.pixeldroid.fragments.feeds.search
import android.annotation.SuppressLint
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList
import androidx.recyclerview.widget.RecyclerView
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.util.ViewPreloadSizeProvider
import com.h.pixeldroid.R
import com.h.pixeldroid.fragments.feeds.AccountListFragment
import com.h.pixeldroid.fragments.feeds.FeedFragment
import com.h.pixeldroid.fragments.feeds.FeedsRecyclerViewAdapter
import com.h.pixeldroid.fragments.feeds.NotificationsFragment
import com.h.pixeldroid.objects.Account
import com.h.pixeldroid.objects.Notification
import com.h.pixeldroid.objects.Results
import com.h.pixeldroid.objects.Tag
import kotlinx.android.synthetic.main.account_list_entry.view.*
import kotlinx.android.synthetic.main.fragment_tags.view.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class SearchHashtagFragment: FeedFragment<Tag, SearchHashtagFragment.TagsRecyclerViewAdapter.ViewHolder>(){
private lateinit var query: String
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
query = arguments?.getSerializable("searchFeed") as String
adapter = TagsRecyclerViewAdapter()
list.adapter = adapter
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
content = makeContent()
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
})
}
inner class SearchTagsListDataSource: FeedDataSource(null, null){
override fun newSource(): FeedDataSource {
return SearchTagsListDataSource()
}
private fun makeInitialCall(requestedLoadSize: Int): Call<Results> {
return pixelfedAPI
.search("Bearer $accessToken",
limit="$requestedLoadSize", q=query,
type = Results.SearchType.hashtags)
}
private fun makeAfterCall(requestedLoadSize: Int, key: String): Call<Results> {
return pixelfedAPI
.search("Bearer $accessToken", offset=key.toInt(),
limit="$requestedLoadSize", q = query,
type = Results.SearchType.hashtags)
}
override fun getKey(item: Tag): String {
val value = content.value
val count = value?.loadedCount ?: 0
return count.toString()
}
override fun loadInitial(
params: LoadInitialParams<String>,
callback: LoadInitialCallback<Tag>
) {
enqueueCall(makeInitialCall(params.requestedLoadSize), callback)
}
//This is called to when we get to the bottom of the loaded content, so we want statuses
//older than the given key (params.key)
override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<Tag>) {
enqueueCall(makeAfterCall(params.requestedLoadSize, params.key), callback)
}
private fun enqueueCall(call: Call<Results>, callback: LoadCallback<Tag>){
call.enqueue(object : Callback<Results> {
override fun onResponse(call: Call<Results>, response: Response<Results>) {
if (response.code() == 200) {
val notifications = response.body()!!.hashtags as ArrayList<Tag>
callback.onResult(notifications as List<Tag>)
} else{
Toast.makeText(context,"Something went wrong while loading", Toast.LENGTH_SHORT).show()
}
swipeRefreshLayout.isRefreshing = false
loadingIndicator.visibility = View.GONE
}
override fun onFailure(call: Call<Results>, t: Throwable) {
Toast.makeText(context,"Could not get feed", Toast.LENGTH_SHORT).show()
Log.e("FeedFragment", t.toString())
}
})
}
}
private fun makeContent(): LiveData<PagedList<Tag>> {
val config: PagedList.Config = PagedList.Config.Builder().setPageSize(10).build()
factory =
FeedFragment<Tag, TagsRecyclerViewAdapter.ViewHolder>()
.FeedDataSourceFactory(
SearchTagsListDataSource()
)
return LivePagedListBuilder(factory, config).build()
}
inner class TagsRecyclerViewAdapter : FeedsRecyclerViewAdapter<Tag, TagsRecyclerViewAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.fragment_tags, parent, false)
context = view.context
return ViewHolder(view)
}
override fun onBindViewHolder(holder : ViewHolder, position : Int) {
val tag = getItem(position) ?: return
@SuppressLint("SetTextI18n")
holder.name.text = "#" + tag.name
holder.mView.setOnClickListener { Log.e("Tag: ", tag.name) }
}
inner class ViewHolder(val mView : View) : RecyclerView.ViewHolder(mView) {
val name : TextView = mView.tag_name
}
}
}

View File

@ -0,0 +1,99 @@
package com.h.pixeldroid.fragments.feeds.search
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.lifecycle.LiveData
import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList
import com.h.pixeldroid.fragments.feeds.FeedFragment
import com.h.pixeldroid.fragments.feeds.PostViewHolder
import com.h.pixeldroid.fragments.feeds.PostsFeedFragment
import com.h.pixeldroid.objects.Results
import com.h.pixeldroid.objects.Status
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class SearchPostsFragment: PostsFeedFragment(){
private lateinit var query: String
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
query = arguments?.getSerializable("searchFeed") as String
return view
}
inner class SearchFeedDataSource(
) : FeedDataSource(null, null){
override fun newSource(): FeedDataSource {
return SearchFeedDataSource()
}
private fun makeInitialCall(requestedLoadSize: Int): Call<Results> {
return pixelfedAPI
.search("Bearer $accessToken",
limit="$requestedLoadSize", q=query,
type = Results.SearchType.statuses)
}
private fun makeAfterCall(requestedLoadSize: Int, key: String): Call<Results> {
return pixelfedAPI
.search("Bearer $accessToken", max_id=key,
limit="$requestedLoadSize", q = query,
type = Results.SearchType.statuses)
}
override fun loadInitial(
params: LoadInitialParams<String>,
callback: LoadInitialCallback<Status>
) {
enqueueCall(makeInitialCall(params.requestedLoadSize), callback)
}
//This is called to when we get to the bottom of the loaded content, so we want statuses
//older than the given key (params.key)
override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<Status>) {
enqueueCall(makeAfterCall(params.requestedLoadSize, params.key), callback)
}
private fun enqueueCall(call: Call<Results>, callback: LoadCallback<Status>){
call.enqueue(object : Callback<Results> {
override fun onResponse(call: Call<Results>, response: Response<Results>) {
if (response.code() == 200) {
val notifications = response.body()!!.statuses as ArrayList<Status>
callback.onResult(notifications as List<Status>)
} else{
Log.e("FeedFragment", "got response code ${response.code()}")
}
swipeRefreshLayout.isRefreshing = false
loadingIndicator.visibility = View.GONE
}
override fun onFailure(call: Call<Results>, t: Throwable) {
Log.e("FeedFragment", t.toString())
}
})
}
}
override fun makeContent(): LiveData<PagedList<Status>> {
val config: PagedList.Config = PagedList.Config.Builder().setPageSize(10).build()
factory = FeedFragment<Status, PostViewHolder>()
.FeedDataSourceFactory(SearchFeedDataSource())
return LivePagedListBuilder(factory, config).build()
}
}

View File

@ -31,7 +31,7 @@ data class Account(
val acct: String,
val url: String, //HTTPS URL
//Display attributes
val display_name: String,
val display_name: String?,
val note: String, //HTML
val avatar: String, //URL
val avatar_static: String, //URL
@ -57,7 +57,7 @@ data class Account(
const val FOLLOWING_TAG = "FollowingTag"
/**
* @brief Opens an activity of the profile withn the given id
* @brief Opens an activity of the profile with the given id
*/
fun getAccountFromId(id: String, api : PixelfedAPI, context: Context, credential: String) {
Log.e("ACCOUNT_ID", id)

View File

@ -0,0 +1,14 @@
package com.h.pixeldroid.objects
import java.io.Serializable
data class Results(
val accounts : List<Account>,
val statuses : List<Status>,
val hashtags: List<Tag>
) : Serializable {
enum class SearchType: Serializable{
accounts, hashtags, statuses
}
}

View File

@ -100,7 +100,7 @@ data class Status(
fun getUsername() : CharSequence {
var name = account.display_name
if (name.isEmpty()) {
if (name.isNullOrEmpty()) {
name = account.username
}
return name

View File

@ -7,6 +7,10 @@ data class Tag(
val name: String,
val url: String,
//Optional attributes
val history: List<History>? = emptyList()
) : Serializable
val history: List<History>? = emptyList()) : Serializable, FeedContent() {
//needed to be a FeedContent, this inheritance is a bit fickle. Do not use.
override val id: String
get() = "tag"
}

View File

@ -6,7 +6,7 @@
xmlns:tools="http://schemas.android.com/tools">
<ImageView
android:id="@+id/follows_avatar"
android:id="@+id/account_entry_avatar"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="16dp"
@ -17,13 +17,13 @@
tools:src="@drawable/ic_default_user" />
<TextView
android:id="@+id/follows_username"
android:id="@+id/account_entry_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_constraintStart_toEndOf="@+id/account_entry_avatar"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,31 @@
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="@+id/search_tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabBackground="@color/colorPrimary"
app:tabMode="fixed"/>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/search_view_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -4,7 +4,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".fragments.feeds.HomeFragment">
tools:context=".fragments.feeds.PostsFeedFragment">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"

View File

@ -0,0 +1,50 @@
<?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:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/search"
android:layout_width="250dp"
android:layout_height="wrap_content"
android:gravity="center"
android:hint="Search"
app:errorEnabled="true"
app:layout_constraintEnd_toStartOf="@+id/searchButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" >
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/searchEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:imeOptions="actionDone"
android:inputType="textUri" />
</com.google.android.material.textfield.TextInputLayout>
<ProgressBar
android:id="@+id/searchProgressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/search" />
<Button
android:id="@+id/searchButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Search"
app:layout_constraintBottom_toBottomOf="@+id/search"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/search" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,17 @@
<?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">
<TextView
android:id="@+id/tag_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="28dp"
android:text="TextView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:label="@string/app_name"
android:hint="Search" >
</searchable>