/* 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 . */ 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 { override fun onResponse(call: Call, response: Response) { 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, 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) { 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" } }