Yuito-app-android/app/src/main/java/com/keylesspalace/tusky/fragment/SearchFragment.kt

276 lines
10 KiB
Kotlin

/* 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"
}
}