Improve search results (#1327)
* Add entities and request for search APIv2 * Implement search adapter and fragment * Fix issue with snackbar * Implement search accounts fragment * Implement generic search fragment * Remove unneeded import * Implement "status" actions, fix issues * Remove SFragment dependency * Update app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt Co-Authored-By: Konrad Pozniak <connyduck@users.noreply.github.com> * Clean-up post review suggestions * Make TabLayout background colour match search bar * Corrected method call syntax * Added SwipeRefreshLayout to SearchFragment * Fixed refresh to update all three tabs
This commit is contained in:
parent
786a399bcd
commit
3b1288e99c
36 changed files with 1666 additions and 489 deletions
|
@ -1,275 +0,0 @@
|
|||
/* Copyright 2018 Conny Duck
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.fragment
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import com.keylesspalace.tusky.AccountActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.ViewTagActivity
|
||||
import com.keylesspalace.tusky.adapter.SearchResultsAdapter
|
||||
import com.keylesspalace.tusky.entity.SearchResults
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||
import com.keylesspalace.tusky.util.ViewDataUtils
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import com.uber.autodispose.autoDisposable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.android.synthetic.main.fragment_search.*
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
|
||||
class SearchFragment : SFragment(), StatusActionListener {
|
||||
|
||||
private lateinit var searchAdapter: SearchResultsAdapter
|
||||
|
||||
private var alwaysShowSensitiveMedia = false
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_search, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(view.context)
|
||||
val useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false)
|
||||
val showBotOverlay = preferences.getBoolean("showBotOverlay", true)
|
||||
val animateAvatar = preferences.getBoolean("animateGifAvatars", false)
|
||||
|
||||
val account = accountManager.activeAccount
|
||||
alwaysShowSensitiveMedia = account?.alwaysShowSensitiveMedia ?: false
|
||||
val mediaPreviewEnabled = account?.mediaPreviewEnabled ?: true
|
||||
|
||||
searchRecyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
|
||||
searchRecyclerView.layoutManager = LinearLayoutManager(view.context)
|
||||
searchAdapter = SearchResultsAdapter(
|
||||
this,
|
||||
this,
|
||||
mediaPreviewEnabled,
|
||||
alwaysShowSensitiveMedia,
|
||||
useAbsoluteTime,
|
||||
showBotOverlay,
|
||||
animateAvatar
|
||||
)
|
||||
searchRecyclerView.adapter = searchAdapter
|
||||
|
||||
}
|
||||
|
||||
fun search(query: String) {
|
||||
clearResults()
|
||||
val callback = object : Callback<SearchResults> {
|
||||
override fun onResponse(call: Call<SearchResults>, response: Response<SearchResults>) {
|
||||
if (response.isSuccessful) {
|
||||
val results = response.body()
|
||||
if (results != null && (results.accounts.isNotEmpty() || results.statuses.isNotEmpty() || results.hashtags.isNotEmpty())) {
|
||||
searchAdapter.updateSearchResults(results)
|
||||
hideFeedback()
|
||||
} else {
|
||||
displayNoResults()
|
||||
}
|
||||
} else {
|
||||
onSearchFailure()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<SearchResults>, t: Throwable) {
|
||||
onSearchFailure()
|
||||
}
|
||||
}
|
||||
mastodonApi.search(query, true)
|
||||
.enqueue(callback)
|
||||
}
|
||||
|
||||
private fun onSearchFailure() {
|
||||
displayNoResults()
|
||||
Log.e(TAG, "Search request failed.")
|
||||
}
|
||||
|
||||
private fun clearResults() {
|
||||
searchAdapter.updateSearchResults(null)
|
||||
searchProgressBar.visibility = View.VISIBLE
|
||||
searchNoResultsText.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun displayNoResults() {
|
||||
if (isAdded) {
|
||||
searchProgressBar.visibility = View.GONE
|
||||
searchNoResultsText.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideFeedback() {
|
||||
if (isAdded) {
|
||||
searchProgressBar.visibility = View.GONE
|
||||
searchNoResultsText.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeItem(position: Int) {
|
||||
searchAdapter.removeStatusAtPosition(position)
|
||||
}
|
||||
|
||||
override fun onReply(position: Int) {
|
||||
val status = searchAdapter.getStatusAtPosition(position)
|
||||
if (status != null) {
|
||||
super.reply(status)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReblog(reblog: Boolean, position: Int) {
|
||||
val status = searchAdapter.getStatusAtPosition(position)
|
||||
if (status != null) {
|
||||
timelineCases.reblog(status, reblog)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe({
|
||||
status.reblogged = reblog
|
||||
searchAdapter.updateStatusAtPosition(
|
||||
ViewDataUtils.statusToViewData(
|
||||
status,
|
||||
alwaysShowSensitiveMedia
|
||||
),
|
||||
position,
|
||||
false
|
||||
)
|
||||
}, { t -> Log.d(TAG, "Failed to reblog status " + status.id, t) })
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFavourite(favourite: Boolean, position: Int) {
|
||||
val status = searchAdapter.getStatusAtPosition(position)
|
||||
if (status != null) {
|
||||
timelineCases.favourite(status, favourite)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe({
|
||||
status.favourited = favourite
|
||||
searchAdapter.updateStatusAtPosition(
|
||||
ViewDataUtils.statusToViewData(
|
||||
status,
|
||||
alwaysShowSensitiveMedia
|
||||
),
|
||||
position,
|
||||
false
|
||||
)
|
||||
}, { t -> Log.d(TAG, "Failed to favourite status " + status.id, t) })
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMore(view: View, position: Int) {
|
||||
val status = searchAdapter.getStatusAtPosition(position)
|
||||
if (status != null) {
|
||||
more(status, view, position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
|
||||
val status = searchAdapter.getStatusAtPosition(position) ?: return
|
||||
viewMedia(attachmentIndex, status, view)
|
||||
}
|
||||
|
||||
override fun onViewThread(position: Int) {
|
||||
val status = searchAdapter.getStatusAtPosition(position)
|
||||
if (status != null) {
|
||||
viewThread(status)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOpenReblog(position: Int) {
|
||||
// there are no reblogs in search results
|
||||
}
|
||||
|
||||
override fun onExpandedChange(expanded: Boolean, position: Int) {
|
||||
val status = searchAdapter.getConcreteStatusAtPosition(position)
|
||||
if (status != null) {
|
||||
val newStatus = StatusViewData.Builder(status)
|
||||
.setIsExpanded(expanded).createStatusViewData()
|
||||
searchAdapter.updateStatusAtPosition(newStatus, position, false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
|
||||
val status = searchAdapter.getConcreteStatusAtPosition(position)
|
||||
if (status != null) {
|
||||
val newStatus = StatusViewData.Builder(status)
|
||||
.setIsShowingSensitiveContent(isShowing).createStatusViewData()
|
||||
searchAdapter.updateStatusAtPosition(newStatus, position, true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLoadMore(position: Int) {
|
||||
// not needed here, search is not paginated
|
||||
}
|
||||
|
||||
override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) {
|
||||
// TODO: No out-of-bounds check in getConcreteStatusAtPosition
|
||||
val status = searchAdapter.getConcreteStatusAtPosition(position)
|
||||
if (status == null) {
|
||||
Log.e(TAG, String.format("Tried to access status but got null at position: %d", position))
|
||||
return
|
||||
}
|
||||
|
||||
val updatedStatus = StatusViewData.Builder(status)
|
||||
.setCollapsed(isCollapsed)
|
||||
.createStatusViewData()
|
||||
searchAdapter.updateStatusAtPosition(updatedStatus, position, false)
|
||||
searchRecyclerView.post { searchAdapter.notifyItemChanged(position, updatedStatus) }
|
||||
}
|
||||
|
||||
override fun onViewAccount(id: String) {
|
||||
val intent = AccountActivity.getIntent(requireContext(), id)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onViewTag(tag: String) {
|
||||
val intent = Intent(context, ViewTagActivity::class.java)
|
||||
intent.putExtra("hashtag", tag)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onVoteInPoll(position: Int, choices: MutableList<Int>) {
|
||||
val status = searchAdapter.getStatusAtPosition(position)
|
||||
if (status != null) {
|
||||
timelineCases.voteInPoll(status, choices)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe({poll ->
|
||||
val viewData = ViewDataUtils.statusToViewData(
|
||||
status,
|
||||
alwaysShowSensitiveMedia
|
||||
)
|
||||
val newViewData = StatusViewData.Builder(viewData)
|
||||
.setPoll(poll)
|
||||
.createStatusViewData()
|
||||
searchAdapter.updateStatusAtPosition(newViewData, position, true)
|
||||
|
||||
}, { t -> Log.d(TAG, "Failed to vote in poll " + status.id, t) })
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "SearchFragment"
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue