2021-04-22 18:48:16 +02:00
|
|
|
/* Copyright 2021 Tusky Contributors
|
|
|
|
*
|
|
|
|
* 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
|
|
|
|
|
2021-04-28 04:54:29 +02:00
|
|
|
import android.content.Context.CONNECTIVITY_SERVICE
|
|
|
|
import android.content.SharedPreferences
|
|
|
|
import android.net.ConnectivityManager
|
2021-04-22 18:48:16 +02:00
|
|
|
import android.os.Bundle
|
|
|
|
import android.util.Log
|
|
|
|
import android.view.LayoutInflater
|
|
|
|
import android.view.View
|
|
|
|
import android.view.ViewGroup
|
|
|
|
import android.view.accessibility.AccessibilityManager
|
|
|
|
import androidx.core.content.ContextCompat
|
|
|
|
import androidx.core.util.Pair
|
|
|
|
import androidx.lifecycle.Lifecycle
|
|
|
|
import androidx.preference.PreferenceManager
|
2021-04-28 04:54:29 +02:00
|
|
|
import androidx.recyclerview.widget.*
|
2021-04-22 18:48:16 +02:00
|
|
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
|
|
|
|
import at.connyduck.sparkbutton.helpers.Utils
|
|
|
|
import com.keylesspalace.tusky.AccountListActivity
|
|
|
|
import com.keylesspalace.tusky.AccountListActivity.Companion.newIntent
|
|
|
|
import com.keylesspalace.tusky.BaseActivity
|
|
|
|
import com.keylesspalace.tusky.R
|
|
|
|
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder
|
|
|
|
import com.keylesspalace.tusky.adapter.TimelineAdapter
|
2021-04-28 04:54:29 +02:00
|
|
|
import com.keylesspalace.tusky.appstore.*
|
|
|
|
import com.keylesspalace.tusky.components.compose.CAN_USE_QUOTE_ID
|
2021-04-22 18:48:16 +02:00
|
|
|
import com.keylesspalace.tusky.databinding.FragmentTimelineBinding
|
|
|
|
import com.keylesspalace.tusky.db.AccountManager
|
|
|
|
import com.keylesspalace.tusky.di.Injectable
|
|
|
|
import com.keylesspalace.tusky.entity.Filter
|
|
|
|
import com.keylesspalace.tusky.entity.Poll
|
|
|
|
import com.keylesspalace.tusky.entity.Status
|
|
|
|
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
|
|
|
import com.keylesspalace.tusky.interfaces.RefreshableFragment
|
|
|
|
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
|
|
|
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
|
|
|
import com.keylesspalace.tusky.repository.Placeholder
|
|
|
|
import com.keylesspalace.tusky.repository.TimelineRepository
|
|
|
|
import com.keylesspalace.tusky.repository.TimelineRequestMode
|
|
|
|
import com.keylesspalace.tusky.settings.PrefKeys
|
2021-04-28 04:54:29 +02:00
|
|
|
import com.keylesspalace.tusky.util.*
|
2021-04-22 18:48:16 +02:00
|
|
|
import com.keylesspalace.tusky.util.Either.Left
|
|
|
|
import com.keylesspalace.tusky.util.Either.Right
|
|
|
|
import com.keylesspalace.tusky.view.EndlessOnScrollListener
|
|
|
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
|
|
|
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
|
|
|
|
import com.uber.autodispose.autoDispose
|
|
|
|
import io.reactivex.Observable
|
|
|
|
import io.reactivex.Single
|
|
|
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
2021-04-28 04:54:29 +02:00
|
|
|
import net.accelf.yuito.TimelineStreamingListener
|
|
|
|
import okhttp3.OkHttpClient
|
|
|
|
import okhttp3.Request
|
|
|
|
import okhttp3.WebSocket
|
2021-04-22 18:48:16 +02:00
|
|
|
import retrofit2.Response
|
|
|
|
import java.io.IOException
|
2021-04-28 04:54:29 +02:00
|
|
|
import java.util.*
|
2021-04-22 18:48:16 +02:00
|
|
|
import java.util.concurrent.TimeUnit
|
|
|
|
import javax.inject.Inject
|
2021-04-28 04:54:29 +02:00
|
|
|
import kotlin.collections.ArrayList
|
2021-04-22 18:48:16 +02:00
|
|
|
|
|
|
|
class TimelineFragment : SFragment(), OnRefreshListener, StatusActionListener, Injectable, ReselectableFragment, RefreshableFragment {
|
|
|
|
|
|
|
|
@Inject
|
|
|
|
lateinit var eventHub: EventHub
|
|
|
|
|
|
|
|
@Inject
|
|
|
|
lateinit var timelineRepo: TimelineRepository
|
|
|
|
|
|
|
|
@Inject
|
|
|
|
lateinit var accountManager: AccountManager
|
|
|
|
|
|
|
|
private val binding by viewBinding(FragmentTimelineBinding::bind)
|
|
|
|
|
|
|
|
private var kind: Kind? = null
|
|
|
|
private var id: String? = null
|
|
|
|
private var tags: List<String> = emptyList()
|
|
|
|
|
|
|
|
private lateinit var adapter: TimelineAdapter
|
|
|
|
|
|
|
|
private var isSwipeToRefreshEnabled = true
|
|
|
|
private var isNeedRefresh = false
|
|
|
|
|
2021-04-28 04:54:29 +02:00
|
|
|
var isStreamingEnabled = false
|
|
|
|
set(value) {
|
|
|
|
field = value
|
|
|
|
when (value) {
|
|
|
|
true -> startStreaming()
|
|
|
|
false -> stopStreaming()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private var webSocket: WebSocket? = null
|
|
|
|
|
2021-04-22 18:48:16 +02:00
|
|
|
private var eventRegistered = false
|
|
|
|
|
|
|
|
/**
|
|
|
|
* For some timeline kinds we must use LINK headers and not just status ids.
|
|
|
|
*/
|
|
|
|
private var nextId: String? = null
|
|
|
|
private var layoutManager: LinearLayoutManager? = null
|
|
|
|
private var scrollListener: EndlessOnScrollListener? = null
|
|
|
|
private var filterRemoveReplies = false
|
|
|
|
private var filterRemoveReblogs = false
|
|
|
|
private var hideFab = false
|
|
|
|
private var bottomLoading = false
|
|
|
|
private var didLoadEverythingBottom = false
|
|
|
|
private var alwaysShowSensitiveMedia = false
|
|
|
|
private var alwaysOpenSpoiler = false
|
|
|
|
private var initialUpdateFailed = false
|
|
|
|
|
2021-04-28 04:54:29 +02:00
|
|
|
private var reduceTimelineLoading = false
|
|
|
|
private var checkMobileNetwork = true
|
|
|
|
|
2021-04-22 18:48:16 +02:00
|
|
|
private val statuses = PairedList<Either<Placeholder, Status>, StatusViewData> { input ->
|
|
|
|
val status = input.asRightOrNull()
|
|
|
|
if (status != null) {
|
|
|
|
ViewDataUtils.statusToViewData(
|
|
|
|
status,
|
|
|
|
alwaysShowSensitiveMedia,
|
|
|
|
alwaysOpenSpoiler
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
val (id1) = input.asLeft()
|
|
|
|
StatusViewData.Placeholder(id1, false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
|
|
super.onCreate(savedInstanceState)
|
|
|
|
val arguments = requireArguments()
|
|
|
|
kind = Kind.valueOf(arguments.getString(KIND_ARG)!!)
|
|
|
|
if (kind == Kind.USER || kind == Kind.USER_PINNED || kind == Kind.USER_WITH_REPLIES || kind == Kind.LIST) {
|
|
|
|
id = arguments.getString(ID_ARG)!!
|
|
|
|
}
|
|
|
|
if (kind == Kind.TAG) {
|
|
|
|
tags = arguments.getStringArrayList(HASHTAGS_ARG)!!
|
|
|
|
}
|
|
|
|
|
|
|
|
isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true)
|
2021-04-28 04:54:29 +02:00
|
|
|
isStreamingEnabled = arguments.getBoolean(ARG_ENABLE_STREAMING, false)
|
2021-04-22 18:48:16 +02:00
|
|
|
|
|
|
|
val preferences = PreferenceManager.getDefaultSharedPreferences(activity)
|
|
|
|
val statusDisplayOptions = StatusDisplayOptions(
|
|
|
|
animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false),
|
|
|
|
mediaPreviewEnabled = accountManager.activeAccount!!.mediaPreviewEnabled,
|
|
|
|
useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false),
|
|
|
|
showBotOverlay = preferences.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, true),
|
|
|
|
useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true),
|
|
|
|
cardViewMode = if (preferences.getBoolean(PrefKeys.SHOW_CARDS_IN_TIMELINES, false)) CardViewMode.INDENTED else CardViewMode.NONE,
|
|
|
|
confirmReblogs = preferences.getBoolean(PrefKeys.CONFIRM_REBLOGS, true),
|
|
|
|
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
|
2021-04-28 04:54:29 +02:00
|
|
|
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false),
|
|
|
|
quoteEnabled = CAN_USE_QUOTE_ID.contains(accountManager.activeAccount?.domain),
|
2021-04-22 18:48:16 +02:00
|
|
|
)
|
|
|
|
adapter = TimelineAdapter(dataSource, statusDisplayOptions, this)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
|
|
|
return inflater.inflate(R.layout.fragment_timeline, container, false)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
|
|
setupSwipeRefreshLayout()
|
|
|
|
setupRecyclerView()
|
|
|
|
updateAdapter()
|
|
|
|
setupTimelinePreferences()
|
|
|
|
if (statuses.isEmpty()) {
|
|
|
|
binding.progressBar.show()
|
|
|
|
bottomLoading = true
|
|
|
|
sendInitialRequest()
|
|
|
|
} else {
|
|
|
|
binding.progressBar.hide()
|
|
|
|
if (isNeedRefresh) {
|
|
|
|
onRefresh()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-28 04:54:29 +02:00
|
|
|
override fun onStart() {
|
|
|
|
super.onStart()
|
|
|
|
|
|
|
|
if (isStreamingEnabled) {
|
|
|
|
startStreaming()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onStop() {
|
|
|
|
super.onStop()
|
|
|
|
|
|
|
|
stopStreaming()
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun startStreaming() {
|
|
|
|
accountManager.activeAccount?.let { activeAccount ->
|
|
|
|
val params = when (kind) {
|
|
|
|
Kind.HOME -> {
|
|
|
|
"user"
|
|
|
|
}
|
|
|
|
Kind.PUBLIC_FEDERATED -> {
|
|
|
|
"public"
|
|
|
|
}
|
|
|
|
Kind.PUBLIC_LOCAL -> {
|
|
|
|
"public:local"
|
|
|
|
}
|
|
|
|
Kind.LIST -> {
|
|
|
|
"list&list=$id"
|
|
|
|
}
|
|
|
|
else -> {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
val endpoint = ("wss://${activeAccount.domain}/api/v1/streaming/?access_token=${activeAccount.accessToken}&stream=${params}")
|
|
|
|
if (webSocket != null) {
|
|
|
|
stopStreaming()
|
|
|
|
}
|
|
|
|
val request: Request = Request.Builder().url(endpoint).build()
|
|
|
|
val client: OkHttpClient = OkHttpClient.Builder().build()
|
|
|
|
webSocket = client.newWebSocket(request, TimelineStreamingListener(eventHub, kind!!, id))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun stopStreaming() {
|
|
|
|
webSocket?.close(1000, null)
|
|
|
|
webSocket = null
|
|
|
|
}
|
|
|
|
|
2021-04-22 18:48:16 +02:00
|
|
|
private fun sendInitialRequest() {
|
|
|
|
if (kind == Kind.HOME) {
|
|
|
|
tryCache()
|
|
|
|
} else {
|
|
|
|
sendFetchTimelineRequest(null, null, null, FetchEnd.BOTTOM, -1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun tryCache() {
|
|
|
|
// Request timeline from disk to make it quick, then replace it with timeline from
|
|
|
|
// the server to update it
|
|
|
|
timelineRepo.getStatuses(null, null, null, LOAD_AT_ONCE, TimelineRequestMode.DISK)
|
|
|
|
.observeOn(AndroidSchedulers.mainThread())
|
|
|
|
.autoDispose(from(this))
|
|
|
|
.subscribe { statuses: List<Either<Placeholder, Status>> ->
|
|
|
|
val mutableStatusResponse = statuses.toMutableList()
|
|
|
|
filterStatuses(mutableStatusResponse)
|
|
|
|
if (statuses.size > 1) {
|
|
|
|
clearPlaceholdersForResponse(mutableStatusResponse)
|
|
|
|
this.statuses.clear()
|
|
|
|
this.statuses.addAll(statuses)
|
|
|
|
updateAdapter()
|
|
|
|
binding.progressBar.hide()
|
|
|
|
// Request statuses including current top to refresh all of them
|
|
|
|
}
|
|
|
|
updateCurrent()
|
|
|
|
loadAbove()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun updateCurrent() {
|
|
|
|
if (statuses.isEmpty()) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
val topId = statuses.first { status -> status.isRight() }!!.asRight().id
|
|
|
|
timelineRepo.getStatuses(topId, null, null, LOAD_AT_ONCE,
|
|
|
|
TimelineRequestMode.NETWORK)
|
|
|
|
.observeOn(AndroidSchedulers.mainThread())
|
|
|
|
.autoDispose(from(this))
|
|
|
|
.subscribe(
|
|
|
|
{ statuses: List<Either<Placeholder, Status>> ->
|
|
|
|
|
|
|
|
initialUpdateFailed = false
|
|
|
|
// When cached timeline is too old, we would replace it with nothing
|
|
|
|
if (statuses.isNotEmpty()) {
|
|
|
|
val mutableStatuses = statuses.toMutableList()
|
|
|
|
filterStatuses(mutableStatuses)
|
|
|
|
if (!this.statuses.isEmpty()) {
|
|
|
|
// clear old cached statuses
|
|
|
|
val iterator = this.statuses.iterator()
|
|
|
|
while (iterator.hasNext()) {
|
|
|
|
val item = iterator.next()
|
|
|
|
if (item.isRight()) {
|
|
|
|
val (id1) = item.asRight()
|
|
|
|
if (id1.length < topId.length || id1 < topId) {
|
|
|
|
iterator.remove()
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
val (id1) = item.asLeft()
|
|
|
|
if (id1.length < topId.length || id1 < topId) {
|
|
|
|
iterator.remove()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.statuses.addAll(mutableStatuses)
|
|
|
|
updateAdapter()
|
|
|
|
}
|
|
|
|
bottomLoading = false
|
|
|
|
},
|
|
|
|
{ t: Throwable? ->
|
|
|
|
Log.d(TAG, "Failed updating timeline", t)
|
|
|
|
initialUpdateFailed = true
|
|
|
|
// Indicate that we are not loading anymore
|
|
|
|
binding.progressBar.hide()
|
|
|
|
binding.swipeRefreshLayout.isRefreshing = false
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun setupTimelinePreferences() {
|
|
|
|
alwaysShowSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia
|
|
|
|
alwaysOpenSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler
|
2021-04-28 04:54:29 +02:00
|
|
|
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
2021-04-22 18:48:16 +02:00
|
|
|
if (kind == Kind.HOME) {
|
|
|
|
filterRemoveReplies = !preferences.getBoolean("tabFilterHomeReplies", true)
|
|
|
|
filterRemoveReblogs = !preferences.getBoolean("tabFilterHomeBoosts", true)
|
|
|
|
}
|
|
|
|
reloadFilters(false)
|
2021-04-28 04:54:29 +02:00
|
|
|
updateLimitedBandwidthStatus(preferences)
|
2021-04-22 18:48:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun filterIsRelevant(filter: Filter): Boolean {
|
|
|
|
return filterContextMatchesKind(kind, filter.context)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun refreshAfterApplyingFilters() {
|
|
|
|
fullyRefresh()
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun setupSwipeRefreshLayout() {
|
|
|
|
binding.swipeRefreshLayout.isEnabled = isSwipeToRefreshEnabled
|
|
|
|
binding.swipeRefreshLayout.setOnRefreshListener(this)
|
|
|
|
binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun setupRecyclerView() {
|
|
|
|
binding.recyclerView.setAccessibilityDelegateCompat(
|
|
|
|
ListStatusAccessibilityDelegate(binding.recyclerView, this)
|
|
|
|
{ pos -> statuses.getPairedItemOrNull(pos) }
|
|
|
|
)
|
|
|
|
binding.recyclerView.setHasFixedSize(true)
|
|
|
|
layoutManager = LinearLayoutManager(context)
|
|
|
|
binding.recyclerView.layoutManager = layoutManager
|
|
|
|
val divider = DividerItemDecoration(context, RecyclerView.VERTICAL)
|
|
|
|
binding.recyclerView.addItemDecoration(divider)
|
|
|
|
|
|
|
|
// CWs are expanded without animation, buttons animate itself, we don't need it basically
|
|
|
|
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
|
|
|
binding.recyclerView.adapter = adapter
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun deleteStatusById(id: String) {
|
|
|
|
for (i in statuses.indices) {
|
|
|
|
val either = statuses[i]
|
|
|
|
if (either.isRight() && id == either.asRight().id) {
|
|
|
|
statuses.remove(either)
|
|
|
|
updateAdapter()
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (statuses.isEmpty()) {
|
|
|
|
showEmptyView()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun showEmptyView() {
|
|
|
|
binding.statusView.show()
|
|
|
|
binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
|
|
|
super.onActivityCreated(savedInstanceState)
|
|
|
|
|
|
|
|
/* This is delayed until onActivityCreated solely because MainActivity.composeButton isn't
|
|
|
|
* guaranteed to be set until then. */
|
|
|
|
scrollListener = if (actionButtonPresent()) {
|
|
|
|
/* Use a modified scroll listener that both loads more statuses as it goes, and hides
|
|
|
|
* the follow button on down-scroll. */
|
|
|
|
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
|
|
|
hideFab = preferences.getBoolean("fabHide", false)
|
|
|
|
object : EndlessOnScrollListener(layoutManager) {
|
|
|
|
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
|
|
|
|
super.onScrolled(view, dx, dy)
|
|
|
|
val composeButton = (activity as ActionButtonActivity).actionButton
|
|
|
|
if (composeButton != null) {
|
|
|
|
if (hideFab) {
|
|
|
|
if (dy > 0 && composeButton.isShown) {
|
|
|
|
composeButton.hide() // hides the button if we're scrolling down
|
|
|
|
} else if (dy < 0 && !composeButton.isShown) {
|
|
|
|
composeButton.show() // shows it if we are scrolling up
|
|
|
|
}
|
|
|
|
} else if (!composeButton.isShown) {
|
|
|
|
composeButton.show()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) {
|
|
|
|
this@TimelineFragment.onLoadMore()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Just use the basic scroll listener to load more statuses.
|
|
|
|
object : EndlessOnScrollListener(layoutManager) {
|
|
|
|
override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) {
|
|
|
|
this@TimelineFragment.onLoadMore()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}.also {
|
|
|
|
binding.recyclerView.addOnScrollListener(it)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!eventRegistered) {
|
|
|
|
eventHub.events
|
|
|
|
.observeOn(AndroidSchedulers.mainThread())
|
2021-05-02 17:17:30 +02:00
|
|
|
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
|
2021-04-22 18:48:16 +02:00
|
|
|
.subscribe { event: Event? ->
|
|
|
|
when (event) {
|
|
|
|
is FavoriteEvent -> handleFavEvent(event)
|
|
|
|
is ReblogEvent -> handleReblogEvent(event)
|
|
|
|
is BookmarkEvent -> handleBookmarkEvent(event)
|
|
|
|
is MuteConversationEvent -> fullyRefresh()
|
|
|
|
is UnfollowEvent -> {
|
|
|
|
if (kind == Kind.HOME) {
|
|
|
|
val id = event.accountId
|
|
|
|
removeAllByAccountId(id)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
is BlockEvent -> {
|
|
|
|
if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) {
|
|
|
|
val id = event.accountId
|
|
|
|
removeAllByAccountId(id)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
is MuteEvent -> {
|
|
|
|
if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) {
|
|
|
|
val id = event.accountId
|
|
|
|
removeAllByAccountId(id)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
is DomainMuteEvent -> {
|
|
|
|
if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) {
|
|
|
|
val instance = event.instance
|
|
|
|
removeAllByInstance(instance)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
is StatusDeletedEvent -> {
|
|
|
|
if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) {
|
|
|
|
val id = event.statusId
|
|
|
|
deleteStatusById(id)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
is StatusComposedEvent -> {
|
|
|
|
val status = event.status
|
|
|
|
handleStatusComposeEvent(status)
|
|
|
|
}
|
|
|
|
is PreferenceChangedEvent -> {
|
|
|
|
onPreferenceChanged(event.preferenceKey)
|
|
|
|
}
|
2021-04-28 04:54:29 +02:00
|
|
|
is StreamUpdateEvent -> {
|
|
|
|
if (isStreamingEnabled) {
|
|
|
|
handleStreamUpdateEvent(event)
|
|
|
|
}
|
|
|
|
}
|
2021-04-22 18:48:16 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
eventRegistered = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onRefresh() {
|
|
|
|
binding.swipeRefreshLayout.isEnabled = isSwipeToRefreshEnabled
|
|
|
|
binding.statusView.hide()
|
|
|
|
isNeedRefresh = false
|
|
|
|
if (initialUpdateFailed) {
|
|
|
|
updateCurrent()
|
|
|
|
}
|
|
|
|
loadAbove()
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun loadAbove() {
|
|
|
|
var firstOrNull: String? = null
|
|
|
|
var secondOrNull: String? = null
|
|
|
|
for (i in statuses.indices) {
|
|
|
|
val status = statuses[i]
|
|
|
|
if (status.isRight()) {
|
|
|
|
firstOrNull = status.asRight().id
|
|
|
|
if (i + 1 < statuses.size && statuses[i + 1].isRight()) {
|
|
|
|
secondOrNull = statuses[i + 1].asRight().id
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (firstOrNull != null) {
|
|
|
|
sendFetchTimelineRequest(null, firstOrNull, secondOrNull, FetchEnd.TOP, -1)
|
|
|
|
} else {
|
|
|
|
sendFetchTimelineRequest(null, null, null, FetchEnd.BOTTOM, -1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onReply(position: Int) {
|
2021-04-28 04:54:29 +02:00
|
|
|
when (kind) {
|
|
|
|
Kind.HOME,
|
|
|
|
Kind.PUBLIC_LOCAL,
|
|
|
|
Kind.PUBLIC_FEDERATED,
|
|
|
|
Kind.TAG,
|
|
|
|
Kind.FAVOURITES,
|
|
|
|
Kind.LIST -> {
|
|
|
|
eventHub.dispatch(QuickReplyEvent(statuses[position].asRight().actionableStatus))
|
|
|
|
}
|
|
|
|
Kind.BOOKMARKS,
|
|
|
|
Kind.USER,
|
|
|
|
Kind.USER_PINNED,
|
|
|
|
Kind.USER_WITH_REPLIES -> {
|
|
|
|
super.reply(statuses[position].asRight())
|
|
|
|
}
|
|
|
|
}
|
2021-04-22 18:48:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onReblog(reblog: Boolean, position: Int) {
|
|
|
|
val status = statuses[position].asRight()
|
|
|
|
timelineCases.reblog(status, reblog)
|
|
|
|
.observeOn(AndroidSchedulers.mainThread())
|
|
|
|
.autoDispose(from(this))
|
|
|
|
.subscribe(
|
|
|
|
{ newStatus: Status -> setRebloggedForStatus(position, newStatus, reblog) }
|
|
|
|
) { t: Throwable? -> Log.d(TAG, "Failed to reblog status " + status.id, t) }
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun setRebloggedForStatus(position: Int, status: Status, reblog: Boolean) {
|
|
|
|
status.reblogged = reblog
|
|
|
|
if (status.reblog != null) {
|
|
|
|
status.reblog.reblogged = reblog
|
|
|
|
}
|
|
|
|
val actual = findStatusAndPosition(position, status) ?: return
|
|
|
|
val newViewData: StatusViewData = StatusViewData.Builder(actual.first)
|
|
|
|
.setReblogged(reblog)
|
|
|
|
.createStatusViewData()
|
|
|
|
statuses.setPairedItem(actual.second!!, newViewData)
|
|
|
|
updateAdapter()
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onFavourite(favourite: Boolean, position: Int) {
|
|
|
|
val status = statuses[position].asRight()
|
|
|
|
timelineCases.favourite(status, favourite)
|
|
|
|
.observeOn(AndroidSchedulers.mainThread())
|
|
|
|
.autoDispose(from(this))
|
|
|
|
.subscribe(
|
|
|
|
{ newStatus: Status -> setFavouriteForStatus(position, newStatus, favourite) },
|
|
|
|
{ t: Throwable? -> Log.d(TAG, "Failed to favourite status " + status.id, t) }
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun setFavouriteForStatus(position: Int, status: Status, favourite: Boolean) {
|
|
|
|
status.favourited = favourite
|
|
|
|
if (status.reblog != null) {
|
|
|
|
status.reblog.favourited = favourite
|
|
|
|
}
|
|
|
|
val actual = findStatusAndPosition(position, status) ?: return
|
|
|
|
val newViewData: StatusViewData = StatusViewData.Builder(actual.first)
|
|
|
|
.setFavourited(favourite)
|
|
|
|
.createStatusViewData()
|
|
|
|
statuses.setPairedItem(actual.second!!, newViewData)
|
|
|
|
updateAdapter()
|
|
|
|
}
|
|
|
|
|
2021-04-28 04:54:29 +02:00
|
|
|
override fun onQuote(position: Int) {
|
|
|
|
super.quote(statuses[position].asRight())
|
|
|
|
}
|
|
|
|
|
2021-04-22 18:48:16 +02:00
|
|
|
override fun onBookmark(bookmark: Boolean, position: Int) {
|
|
|
|
val status = statuses[position].asRight()
|
|
|
|
timelineCases.bookmark(status, bookmark)
|
|
|
|
.observeOn(AndroidSchedulers.mainThread())
|
|
|
|
.autoDispose(from(this))
|
|
|
|
.subscribe(
|
|
|
|
{ newStatus: Status -> setBookmarkForStatus(position, newStatus, bookmark) },
|
|
|
|
{ t: Throwable? -> Log.d(TAG, "Failed to favourite status " + status.id, t) }
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun setBookmarkForStatus(position: Int, status: Status, bookmark: Boolean) {
|
|
|
|
status.bookmarked = bookmark
|
|
|
|
if (status.reblog != null) {
|
|
|
|
status.reblog.bookmarked = bookmark
|
|
|
|
}
|
|
|
|
val actual = findStatusAndPosition(position, status) ?: return
|
|
|
|
val newViewData: StatusViewData = StatusViewData.Builder(actual.first)
|
|
|
|
.setBookmarked(bookmark)
|
|
|
|
.createStatusViewData()
|
|
|
|
statuses.setPairedItem(actual.second!!, newViewData)
|
|
|
|
updateAdapter()
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onVoteInPoll(position: Int, choices: List<Int>) {
|
|
|
|
val status = statuses[position].asRight()
|
|
|
|
val votedPoll = status.actionableStatus.poll!!.votedCopy(choices)
|
|
|
|
setVoteForPoll(position, status, votedPoll)
|
|
|
|
timelineCases.voteInPoll(status, choices)
|
|
|
|
.observeOn(AndroidSchedulers.mainThread())
|
|
|
|
.autoDispose(from(this))
|
|
|
|
.subscribe(
|
|
|
|
{ newPoll: Poll -> setVoteForPoll(position, status, newPoll) },
|
|
|
|
{ t: Throwable? -> Log.d(TAG, "Failed to vote in poll: " + status.id, t) }
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun setVoteForPoll(position: Int, status: Status, newPoll: Poll) {
|
|
|
|
val actual = findStatusAndPosition(position, status) ?: return
|
|
|
|
val newViewData: StatusViewData = StatusViewData.Builder(actual.first)
|
|
|
|
.setPoll(newPoll)
|
|
|
|
.createStatusViewData()
|
|
|
|
statuses.setPairedItem(actual.second!!, newViewData)
|
|
|
|
updateAdapter()
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onMore(view: View, position: Int) {
|
|
|
|
super.more(statuses[position].asRight(), view, position)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onOpenReblog(position: Int) {
|
|
|
|
super.openReblog(statuses[position].asRight())
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onExpandedChange(expanded: Boolean, position: Int) {
|
|
|
|
val newViewData: StatusViewData = StatusViewData.Builder(
|
|
|
|
statuses.getPairedItem(position) as StatusViewData.Concrete)
|
|
|
|
.setIsExpanded(expanded).createStatusViewData()
|
|
|
|
statuses.setPairedItem(position, newViewData)
|
|
|
|
updateAdapter()
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
|
|
|
|
val newViewData: StatusViewData = StatusViewData.Builder(
|
|
|
|
statuses.getPairedItem(position) as StatusViewData.Concrete)
|
|
|
|
.setIsShowingSensitiveContent(isShowing).createStatusViewData()
|
|
|
|
statuses.setPairedItem(position, newViewData)
|
|
|
|
updateAdapter()
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onShowReblogs(position: Int) {
|
|
|
|
val statusId = statuses[position].asRight().id
|
|
|
|
val intent = newIntent(requireContext(), AccountListActivity.Type.REBLOGGED, statusId)
|
|
|
|
(activity as BaseActivity).startActivityWithSlideInAnimation(intent)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onShowFavs(position: Int) {
|
|
|
|
val statusId = statuses[position].asRight().id
|
|
|
|
val intent = newIntent(requireContext(), AccountListActivity.Type.FAVOURITED, statusId)
|
|
|
|
(activity as BaseActivity).startActivityWithSlideInAnimation(intent)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onLoadMore(position: Int) {
|
|
|
|
//check bounds before accessing list,
|
|
|
|
if (statuses.size >= position && position > 0) {
|
|
|
|
val fromStatus = statuses[position - 1].asRightOrNull()
|
|
|
|
val toStatus = statuses[position + 1].asRightOrNull()
|
|
|
|
val maxMinusOne = if (statuses.size > position + 1 && statuses[position + 2].isRight()) statuses[position + 1].asRight().id else null
|
|
|
|
if (fromStatus == null || toStatus == null) {
|
|
|
|
Log.e(TAG, "Failed to load more at $position, wrong placeholder position")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
sendFetchTimelineRequest(fromStatus.id, toStatus.id, maxMinusOne,
|
|
|
|
FetchEnd.MIDDLE, position)
|
|
|
|
val (id1) = statuses[position].asLeft()
|
|
|
|
val newViewData: StatusViewData = StatusViewData.Placeholder(id1, true)
|
|
|
|
statuses.setPairedItem(position, newViewData)
|
|
|
|
updateAdapter()
|
|
|
|
} else {
|
|
|
|
Log.e(TAG, "error loading more")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) {
|
|
|
|
if (position < 0 || position >= statuses.size) {
|
|
|
|
Log.e(TAG, String.format("Tried to access out of bounds status position: %d of %d", position, statuses.size - 1))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
val status = statuses.getPairedItem(position)
|
|
|
|
if (status !is StatusViewData.Concrete) {
|
|
|
|
// Statuses PairedList contains a base type of StatusViewData.Concrete and also doesn't
|
|
|
|
// check for null values when adding values to it although this doesn't seem to be an issue.
|
|
|
|
Log.e(TAG, String.format(
|
|
|
|
"Expected StatusViewData.Concrete, got %s instead at position: %d of %d",
|
|
|
|
status?.javaClass?.simpleName ?: "<null>",
|
|
|
|
position,
|
|
|
|
statuses.size - 1
|
|
|
|
))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
val updatedStatus: StatusViewData = StatusViewData.Builder(status)
|
|
|
|
.setCollapsed(isCollapsed)
|
|
|
|
.createStatusViewData()
|
|
|
|
statuses.setPairedItem(position, updatedStatus)
|
|
|
|
updateAdapter()
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
|
|
|
|
val status = statuses.getOrNull(position)?.asRightOrNull() ?: return
|
|
|
|
super.viewMedia(attachmentIndex, status, view)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onViewThread(position: Int) {
|
|
|
|
super.viewThread(statuses[position].asRight())
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onViewTag(tag: String) {
|
|
|
|
if (kind == Kind.TAG && tags.size == 1 && tags.contains(tag)) {
|
|
|
|
// If already viewing a tag page, then ignore any request to view that tag again.
|
|
|
|
return
|
|
|
|
}
|
|
|
|
super.viewTag(tag)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onViewAccount(id: String) {
|
|
|
|
if ((kind == Kind.USER || kind == Kind.USER_WITH_REPLIES) && this.id == id) {
|
|
|
|
/* If already viewing an account page, then any requests to view that account page
|
|
|
|
* should be ignored. */
|
|
|
|
return
|
|
|
|
}
|
|
|
|
super.viewAccount(id)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun onPreferenceChanged(key: String) {
|
|
|
|
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
|
|
|
when (key) {
|
|
|
|
PrefKeys.FAB_HIDE -> {
|
|
|
|
hideFab = sharedPreferences.getBoolean(PrefKeys.FAB_HIDE, false)
|
|
|
|
}
|
|
|
|
PrefKeys.MEDIA_PREVIEW_ENABLED -> {
|
|
|
|
val enabled = accountManager.activeAccount!!.mediaPreviewEnabled
|
|
|
|
val oldMediaPreviewEnabled = adapter.mediaPreviewEnabled
|
|
|
|
if (enabled != oldMediaPreviewEnabled) {
|
|
|
|
adapter.mediaPreviewEnabled = enabled
|
|
|
|
fullyRefresh()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
PrefKeys.TAB_FILTER_HOME_REPLIES -> {
|
|
|
|
val filter = sharedPreferences.getBoolean(PrefKeys.TAB_FILTER_HOME_REPLIES, true)
|
|
|
|
val oldRemoveReplies = filterRemoveReplies
|
|
|
|
filterRemoveReplies = kind == Kind.HOME && !filter
|
|
|
|
if (adapter.itemCount > 1 && oldRemoveReplies != filterRemoveReplies) {
|
|
|
|
fullyRefresh()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
PrefKeys.TAB_FILTER_HOME_BOOSTS -> {
|
|
|
|
val filter = sharedPreferences.getBoolean(PrefKeys.TAB_FILTER_HOME_BOOSTS, true)
|
|
|
|
val oldRemoveReblogs = filterRemoveReblogs
|
|
|
|
filterRemoveReblogs = kind == Kind.HOME && !filter
|
|
|
|
if (adapter.itemCount > 1 && oldRemoveReblogs != filterRemoveReblogs) {
|
|
|
|
fullyRefresh()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Filter.HOME, Filter.NOTIFICATIONS, Filter.THREAD, Filter.PUBLIC, Filter.ACCOUNT -> {
|
|
|
|
if (filterContextMatchesKind(kind, listOf(key))) {
|
|
|
|
reloadFilters(true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
PrefKeys.ALWAYS_SHOW_SENSITIVE_MEDIA -> {
|
|
|
|
//it is ok if only newly loaded statuses are affected, no need to fully refresh
|
|
|
|
alwaysShowSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia
|
|
|
|
}
|
2021-04-28 04:54:29 +02:00
|
|
|
PrefKeys.LIMITED_BANDWIDTH_ACTIVE,
|
|
|
|
PrefKeys.LIMITED_BANDWIDTH_TIMELINE_LOADING,
|
|
|
|
PrefKeys.LIMITED_BANDWIDTH_ONLY_MOBILE_NETWORK -> {
|
|
|
|
updateLimitedBandwidthStatus(sharedPreferences)
|
|
|
|
}
|
2021-04-22 18:48:16 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-28 04:54:29 +02:00
|
|
|
private fun updateLimitedBandwidthStatus(sharedPreferences: SharedPreferences) {
|
|
|
|
reduceTimelineLoading = sharedPreferences.getBoolean(PrefKeys.LIMITED_BANDWIDTH_ACTIVE, false)
|
|
|
|
&& sharedPreferences.getBoolean(PrefKeys.LIMITED_BANDWIDTH_TIMELINE_LOADING, true)
|
|
|
|
checkMobileNetwork = sharedPreferences.getBoolean(PrefKeys.LIMITED_BANDWIDTH_ONLY_MOBILE_NETWORK, true)
|
|
|
|
}
|
|
|
|
|
2021-04-22 18:48:16 +02:00
|
|
|
public override fun removeItem(position: Int) {
|
|
|
|
statuses.removeAt(position)
|
|
|
|
updateAdapter()
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun removeAllByAccountId(accountId: String) {
|
|
|
|
// using iterator to safely remove items while iterating
|
|
|
|
val iterator = statuses.iterator()
|
|
|
|
while (iterator.hasNext()) {
|
|
|
|
val status = iterator.next().asRightOrNull()
|
|
|
|
if (status != null &&
|
|
|
|
(status.account.id == accountId || status.actionableStatus.account.id == accountId)) {
|
|
|
|
iterator.remove()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
updateAdapter()
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun removeAllByInstance(instance: String) {
|
|
|
|
// using iterator to safely remove items while iterating
|
|
|
|
val iterator = statuses.iterator()
|
|
|
|
while (iterator.hasNext()) {
|
|
|
|
val status = iterator.next().asRightOrNull()
|
|
|
|
if (status != null && LinkHelper.getDomain(status.account.url) == instance) {
|
|
|
|
iterator.remove()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
updateAdapter()
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun onLoadMore() {
|
|
|
|
if (didLoadEverythingBottom || bottomLoading) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if (statuses.isEmpty()) {
|
|
|
|
sendInitialRequest()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
bottomLoading = true
|
|
|
|
val last = statuses[statuses.size - 1]
|
|
|
|
val placeholder: Placeholder
|
|
|
|
if (last!!.isRight()) {
|
|
|
|
val placeholderId = last.asRight().id.dec()
|
|
|
|
placeholder = Placeholder(placeholderId)
|
|
|
|
statuses.add(Left(placeholder))
|
|
|
|
} else {
|
|
|
|
placeholder = last.asLeft()
|
|
|
|
}
|
|
|
|
statuses.setPairedItem(statuses.size - 1,
|
|
|
|
StatusViewData.Placeholder(placeholder.id, true))
|
|
|
|
updateAdapter()
|
|
|
|
|
|
|
|
val bottomId: String? = if (kind == Kind.FAVOURITES || kind == Kind.BOOKMARKS) {
|
|
|
|
nextId
|
|
|
|
} else {
|
|
|
|
statuses.lastOrNull { it.isRight() }?.asRight()?.id
|
|
|
|
}
|
|
|
|
|
|
|
|
sendFetchTimelineRequest(bottomId, null, null, FetchEnd.BOTTOM, -1)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun fullyRefresh() {
|
|
|
|
statuses.clear()
|
|
|
|
updateAdapter()
|
|
|
|
bottomLoading = true
|
|
|
|
sendFetchTimelineRequest(null, null, null, FetchEnd.BOTTOM, -1)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun actionButtonPresent(): Boolean {
|
|
|
|
return kind != Kind.TAG && kind != Kind.FAVOURITES && kind != Kind.BOOKMARKS &&
|
|
|
|
activity is ActionButtonActivity
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun getFetchCallByTimelineType(fromId: String?, uptoId: String?): Single<Response<List<Status>>> {
|
|
|
|
val api = mastodonApi
|
|
|
|
return when (kind) {
|
|
|
|
Kind.HOME -> api.homeTimeline(fromId, uptoId, LOAD_AT_ONCE)
|
|
|
|
Kind.PUBLIC_FEDERATED -> api.publicTimeline(null, fromId, uptoId, LOAD_AT_ONCE)
|
|
|
|
Kind.PUBLIC_LOCAL -> api.publicTimeline(true, fromId, uptoId, LOAD_AT_ONCE)
|
|
|
|
Kind.TAG -> {
|
|
|
|
val firstHashtag = tags[0]
|
|
|
|
val additionalHashtags = tags.subList(1, tags.size)
|
|
|
|
api.hashtagTimeline(firstHashtag, additionalHashtags, null, fromId, uptoId, LOAD_AT_ONCE)
|
|
|
|
}
|
|
|
|
Kind.USER -> api.accountStatuses(id!!, fromId, uptoId, LOAD_AT_ONCE, true, null, null)
|
|
|
|
Kind.USER_PINNED -> api.accountStatuses(id!!, fromId, uptoId, LOAD_AT_ONCE, null, null, true)
|
|
|
|
Kind.USER_WITH_REPLIES -> api.accountStatuses(id!!, fromId, uptoId, LOAD_AT_ONCE, null, null, null)
|
|
|
|
Kind.FAVOURITES -> api.favourites(fromId, uptoId, LOAD_AT_ONCE)
|
|
|
|
Kind.BOOKMARKS -> api.bookmarks(fromId, uptoId, LOAD_AT_ONCE)
|
|
|
|
Kind.LIST -> api.listTimeline(id!!, fromId, uptoId, LOAD_AT_ONCE)
|
|
|
|
else -> api.homeTimeline(fromId, uptoId, LOAD_AT_ONCE)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun sendFetchTimelineRequest(maxId: String?, sinceId: String?,
|
|
|
|
sinceIdMinusOne: String?,
|
|
|
|
fetchEnd: FetchEnd, pos: Int) {
|
|
|
|
if (isAdded && (fetchEnd == FetchEnd.TOP || fetchEnd == FetchEnd.BOTTOM && maxId == null && binding.progressBar.visibility != View.VISIBLE) && !isSwipeToRefreshEnabled) {
|
|
|
|
binding.topProgressBar.show()
|
|
|
|
}
|
|
|
|
if (kind == Kind.HOME) {
|
|
|
|
// allow getting old statuses/fallbacks for network only for for bottom loading
|
|
|
|
val mode = if (fetchEnd == FetchEnd.BOTTOM) {
|
|
|
|
TimelineRequestMode.ANY
|
|
|
|
} else {
|
|
|
|
TimelineRequestMode.NETWORK
|
|
|
|
}
|
|
|
|
timelineRepo.getStatuses(maxId, sinceId, sinceIdMinusOne, LOAD_AT_ONCE, mode)
|
|
|
|
.observeOn(AndroidSchedulers.mainThread())
|
|
|
|
.autoDispose(from(this))
|
|
|
|
.subscribe(
|
|
|
|
{ result: List<Either<Placeholder, Status>> -> onFetchTimelineSuccess(result.toMutableList(), fetchEnd, pos) },
|
|
|
|
{ t: Throwable -> onFetchTimelineFailure(t, fetchEnd, pos) }
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
getFetchCallByTimelineType(maxId, sinceId)
|
|
|
|
.observeOn(AndroidSchedulers.mainThread())
|
|
|
|
.autoDispose(from(this))
|
|
|
|
.subscribe(
|
|
|
|
{ response: Response<List<Status>> ->
|
|
|
|
if (response.isSuccessful) {
|
|
|
|
val newNextId = extractNextId(response)
|
|
|
|
if (newNextId != null) {
|
|
|
|
// when we reach the bottom of the list, we won't have a new link. If
|
|
|
|
// we blindly write `null` here we will start loading from the top
|
|
|
|
// again.
|
|
|
|
nextId = newNextId
|
|
|
|
}
|
|
|
|
onFetchTimelineSuccess(liftStatusList(response.body()!!).toMutableList(), fetchEnd, pos)
|
|
|
|
} else {
|
|
|
|
onFetchTimelineFailure(Exception(response.message()), fetchEnd, pos)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
) { t: Throwable -> onFetchTimelineFailure(t, fetchEnd, pos) }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun extractNextId(response: Response<*>): String? {
|
|
|
|
val linkHeader = response.headers()["Link"] ?: return null
|
|
|
|
val links = HttpHeaderLink.parse(linkHeader)
|
|
|
|
val nextHeader = HttpHeaderLink.findByRelationType(links, "next") ?: return null
|
|
|
|
val nextLink = nextHeader.uri ?: return null
|
|
|
|
return nextLink.getQueryParameter("max_id")
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun onFetchTimelineSuccess(statuses: MutableList<Either<Placeholder, Status>>,
|
|
|
|
fetchEnd: FetchEnd, pos: Int) {
|
|
|
|
|
|
|
|
// We filled the hole (or reached the end) if the server returned less statuses than we
|
|
|
|
// we asked for.
|
|
|
|
val fullFetch = statuses.size >= LOAD_AT_ONCE
|
|
|
|
filterStatuses(statuses)
|
|
|
|
when (fetchEnd) {
|
|
|
|
FetchEnd.TOP -> {
|
|
|
|
updateStatuses(statuses, fullFetch)
|
|
|
|
}
|
|
|
|
FetchEnd.MIDDLE -> {
|
|
|
|
replacePlaceholderWithStatuses(statuses, fullFetch, pos)
|
|
|
|
}
|
|
|
|
FetchEnd.BOTTOM -> {
|
|
|
|
if (!this.statuses.isEmpty()
|
|
|
|
&& !this.statuses[this.statuses.size - 1].isRight()) {
|
|
|
|
this.statuses.removeAt(this.statuses.size - 1)
|
|
|
|
updateAdapter()
|
|
|
|
}
|
|
|
|
if (statuses.isNotEmpty() && !statuses[statuses.size - 1].isRight()) {
|
|
|
|
// Removing placeholder if it's the last one from the cache
|
|
|
|
statuses.removeAt(statuses.size - 1)
|
|
|
|
}
|
|
|
|
val oldSize = this.statuses.size
|
|
|
|
if (this.statuses.size > 1) {
|
|
|
|
addItems(statuses)
|
|
|
|
} else {
|
|
|
|
updateStatuses(statuses, fullFetch)
|
|
|
|
}
|
|
|
|
if (this.statuses.size == oldSize) {
|
|
|
|
// This may be a brittle check but seems like it works
|
|
|
|
// Can we check it using headers somehow? Do all server support them?
|
|
|
|
didLoadEverythingBottom = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (isAdded) {
|
|
|
|
binding.topProgressBar.hide()
|
|
|
|
updateBottomLoadingState(fetchEnd)
|
|
|
|
binding.progressBar.hide()
|
|
|
|
binding.swipeRefreshLayout.isRefreshing = false
|
|
|
|
binding.swipeRefreshLayout.isEnabled = true
|
|
|
|
if (this.statuses.size == 0) {
|
|
|
|
showEmptyView()
|
|
|
|
} else {
|
|
|
|
binding.statusView.hide()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun onFetchTimelineFailure(throwable: Throwable, fetchEnd: FetchEnd, position: Int) {
|
|
|
|
if (isAdded) {
|
|
|
|
binding.swipeRefreshLayout.isRefreshing = false
|
|
|
|
binding.topProgressBar.hide()
|
|
|
|
if (fetchEnd == FetchEnd.MIDDLE && !statuses[position].isRight()) {
|
|
|
|
var placeholder = statuses[position].asLeftOrNull()
|
|
|
|
val newViewData: StatusViewData
|
|
|
|
if (placeholder == null) {
|
|
|
|
val (id1) = statuses[position - 1].asRight()
|
|
|
|
val newId = id1.dec()
|
|
|
|
placeholder = Placeholder(newId)
|
|
|
|
}
|
|
|
|
newViewData = StatusViewData.Placeholder(placeholder.id, false)
|
|
|
|
statuses.setPairedItem(position, newViewData)
|
|
|
|
updateAdapter()
|
|
|
|
} else if (statuses.isEmpty()) {
|
|
|
|
binding.swipeRefreshLayout.isEnabled = false
|
|
|
|
binding.statusView.visibility = View.VISIBLE
|
|
|
|
if (throwable is IOException) {
|
|
|
|
binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network) {
|
|
|
|
binding.progressBar.visibility = View.VISIBLE
|
|
|
|
onRefresh()
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic) {
|
|
|
|
binding.progressBar.visibility = View.VISIBLE
|
|
|
|
onRefresh()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Log.e(TAG, "Fetch Failure: " + throwable.message)
|
|
|
|
updateBottomLoadingState(fetchEnd)
|
|
|
|
binding.progressBar.hide()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun updateBottomLoadingState(fetchEnd: FetchEnd) {
|
|
|
|
if (fetchEnd == FetchEnd.BOTTOM) {
|
|
|
|
bottomLoading = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun filterStatuses(statuses: MutableList<Either<Placeholder, Status>>) {
|
|
|
|
val it = statuses.iterator()
|
|
|
|
while (it.hasNext()) {
|
|
|
|
val status = it.next().asRightOrNull()
|
|
|
|
if (status != null
|
|
|
|
&& (status.inReplyToId != null && filterRemoveReplies
|
|
|
|
|| status.reblog != null && filterRemoveReblogs
|
|
|
|
|| shouldFilterStatus(status.actionableStatus))) {
|
|
|
|
it.remove()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun updateStatuses(newStatuses: MutableList<Either<Placeholder, Status>>, fullFetch: Boolean) {
|
|
|
|
if (newStatuses.isEmpty()) {
|
|
|
|
updateAdapter()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if (statuses.isEmpty()) {
|
|
|
|
statuses.addAll(newStatuses)
|
|
|
|
} else {
|
|
|
|
val lastOfNew = newStatuses[newStatuses.size - 1]
|
|
|
|
val index = statuses.indexOf(lastOfNew)
|
|
|
|
if (index >= 0) {
|
|
|
|
statuses.subList(0, index).clear()
|
|
|
|
}
|
|
|
|
val newIndex = newStatuses.indexOf(statuses[0])
|
|
|
|
if (newIndex == -1) {
|
|
|
|
if (index == -1 && fullFetch) {
|
|
|
|
val placeholderId = newStatuses.last { status -> status.isRight() }.asRight().id.inc()
|
|
|
|
newStatuses.add(Left(Placeholder(placeholderId)))
|
|
|
|
}
|
|
|
|
statuses.addAll(0, newStatuses)
|
|
|
|
} else {
|
|
|
|
statuses.addAll(0, newStatuses.subList(0, newIndex))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Remove all consecutive placeholders
|
|
|
|
removeConsecutivePlaceholders()
|
|
|
|
updateAdapter()
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun removeConsecutivePlaceholders() {
|
|
|
|
for (i in 0 until statuses.size - 1) {
|
|
|
|
if (statuses[i].isLeft() && statuses[i + 1].isLeft()) {
|
|
|
|
statuses.removeAt(i)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun addItems(newStatuses: List<Either<Placeholder, Status>?>) {
|
|
|
|
if (newStatuses.isEmpty()) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
val last = statuses.last { status ->
|
|
|
|
status.isRight()
|
|
|
|
}
|
|
|
|
|
|
|
|
// I was about to replace findStatus with indexOf but it is incorrect to compare value
|
|
|
|
// types by ID anyway and we should change equals() for Status, I think, so this makes sense
|
|
|
|
if (last != null && !newStatuses.contains(last)) {
|
|
|
|
statuses.addAll(newStatuses)
|
|
|
|
removeConsecutivePlaceholders()
|
|
|
|
updateAdapter()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-28 04:54:29 +02:00
|
|
|
private fun addStatus(item: Either<Placeholder, Status>) {
|
|
|
|
if (item.isRight()) {
|
|
|
|
val status = item.asRight()
|
|
|
|
if (!(status.inReplyToId != null && filterRemoveReplies
|
|
|
|
|| status.reblog != null && filterRemoveReblogs
|
|
|
|
|| shouldFilterStatus(status))) {
|
|
|
|
if (findStatusOrReblogPositionById(status.id) < 0) {
|
|
|
|
statuses.add(0, item)
|
|
|
|
updateAdapter()
|
|
|
|
if (kind == Kind.HOME) {
|
|
|
|
timelineRepo.addSingleStatusToDb(status)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
statuses.add(0, item)
|
|
|
|
updateAdapter()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-22 18:48:16 +02:00
|
|
|
/**
|
|
|
|
* For certain requests we don't want to see placeholders, they will be removed some other way
|
|
|
|
*/
|
|
|
|
private fun clearPlaceholdersForResponse(statuses: MutableList<Either<Placeholder, Status>>) {
|
2021-04-28 04:54:29 +02:00
|
|
|
statuses.removeAll { status -> status.isLeft() }
|
2021-04-22 18:48:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun replacePlaceholderWithStatuses(newStatuses: MutableList<Either<Placeholder, Status>>,
|
|
|
|
fullFetch: Boolean, pos: Int) {
|
|
|
|
val placeholder = statuses[pos]
|
|
|
|
if (placeholder.isLeft()) {
|
|
|
|
statuses.removeAt(pos)
|
|
|
|
}
|
|
|
|
if (newStatuses.isEmpty()) {
|
|
|
|
updateAdapter()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if (fullFetch) {
|
|
|
|
newStatuses.add(placeholder)
|
|
|
|
}
|
|
|
|
statuses.addAll(pos, newStatuses)
|
|
|
|
removeConsecutivePlaceholders()
|
|
|
|
updateAdapter()
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun findStatusOrReblogPositionById(statusId: String): Int {
|
|
|
|
return statuses.indexOfFirst { either ->
|
|
|
|
val status = either.asRightOrNull()
|
|
|
|
status != null &&
|
|
|
|
(statusId == status.id ||
|
|
|
|
(status.reblog != null && statusId == status.reblog.id))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private val statusLifter: Function1<Status, Either<Placeholder, Status>> = { value -> Right(value) }
|
|
|
|
|
|
|
|
private fun findStatusAndPosition(position: Int, status: Status): Pair<StatusViewData.Concrete, Int>? {
|
|
|
|
val statusToUpdate: StatusViewData.Concrete
|
|
|
|
val positionToUpdate: Int
|
|
|
|
val someOldViewData = statuses.getPairedItem(position)
|
|
|
|
|
|
|
|
// Unlikely, but data could change between the request and response
|
|
|
|
if (someOldViewData is StatusViewData.Placeholder ||
|
|
|
|
(someOldViewData as StatusViewData.Concrete).id != status.id) {
|
|
|
|
// try to find the status we need to update
|
|
|
|
val foundPos = statuses.indexOf(Right(status))
|
|
|
|
if (foundPos < 0) return null // okay, it's hopeless, give up
|
|
|
|
statusToUpdate = statuses.getPairedItem(foundPos) as StatusViewData.Concrete
|
|
|
|
positionToUpdate = position
|
|
|
|
} else {
|
|
|
|
statusToUpdate = someOldViewData
|
|
|
|
positionToUpdate = position
|
|
|
|
}
|
|
|
|
return Pair(statusToUpdate, positionToUpdate)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun handleReblogEvent(reblogEvent: ReblogEvent) {
|
|
|
|
val pos = findStatusOrReblogPositionById(reblogEvent.statusId)
|
|
|
|
if (pos < 0) return
|
|
|
|
val status = statuses[pos].asRight()
|
|
|
|
setRebloggedForStatus(pos, status, reblogEvent.reblog)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun handleFavEvent(favEvent: FavoriteEvent) {
|
|
|
|
val pos = findStatusOrReblogPositionById(favEvent.statusId)
|
|
|
|
if (pos < 0) return
|
|
|
|
val status = statuses[pos].asRight()
|
|
|
|
setFavouriteForStatus(pos, status, favEvent.favourite)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun handleBookmarkEvent(bookmarkEvent: BookmarkEvent) {
|
|
|
|
val pos = findStatusOrReblogPositionById(bookmarkEvent.statusId)
|
|
|
|
if (pos < 0) return
|
|
|
|
val status = statuses[pos].asRight()
|
|
|
|
setBookmarkForStatus(pos, status, bookmarkEvent.bookmark)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun handleStatusComposeEvent(status: Status) {
|
2021-04-28 04:54:29 +02:00
|
|
|
if (isStreamingEnabled) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var reload = when (kind) {
|
|
|
|
Kind.HOME, Kind.PUBLIC_FEDERATED, Kind.PUBLIC_LOCAL -> true
|
|
|
|
Kind.USER, Kind.USER_WITH_REPLIES -> status.account.id == id
|
|
|
|
Kind.TAG, Kind.FAVOURITES, Kind.LIST, Kind.BOOKMARKS, Kind.USER_PINNED -> false
|
|
|
|
else -> false
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!reload) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (reduceTimelineLoading) {
|
|
|
|
reload = false
|
|
|
|
if (checkMobileNetwork
|
|
|
|
&& activity?.let {
|
|
|
|
(activity?.getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager).isActiveNetworkMetered
|
|
|
|
} == false) {
|
|
|
|
reload = true
|
2021-04-22 18:48:16 +02:00
|
|
|
}
|
2021-04-28 04:54:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (reload) {
|
|
|
|
onRefresh()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun handleStreamUpdateEvent(event: StreamUpdateEvent) {
|
|
|
|
if (event.targetKind != kind || event.targetIdentifier != null && event.targetIdentifier != id) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
val status = event.status
|
|
|
|
if (event.first && statuses[0].isRight()) {
|
|
|
|
val placeholder = Placeholder(statuses[0].asRight().id + 1)
|
|
|
|
updateStatuses(mutableListOf(Right(status), Left(placeholder)), false)
|
|
|
|
} else {
|
|
|
|
addStatus(Right(status))
|
2021-04-22 18:48:16 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun liftStatusList(list: List<Status>): List<Either<Placeholder, Status>> {
|
|
|
|
return list.map(statusLifter)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun updateAdapter() {
|
|
|
|
differ.submitList(statuses.pairedCopy)
|
|
|
|
}
|
|
|
|
|
|
|
|
private val listUpdateCallback: ListUpdateCallback = object : ListUpdateCallback {
|
|
|
|
override fun onInserted(position: Int, count: Int) {
|
|
|
|
if (isAdded) {
|
|
|
|
adapter.notifyItemRangeInserted(position, count)
|
|
|
|
val context = context
|
|
|
|
// scroll up when new items at the top are loaded while being in the first position
|
|
|
|
// https://github.com/tuskyapp/Tusky/pull/1905#issuecomment-677819724
|
2021-04-28 04:54:29 +02:00
|
|
|
if (position == 0 && context != null && layoutManager?.findFirstVisibleItemPosition() == 0 && adapter.itemCount != count) {
|
|
|
|
when {
|
|
|
|
count == 1 -> {
|
|
|
|
layoutManager?.scrollToPosition(0)
|
|
|
|
binding.recyclerView.stopScroll()
|
|
|
|
scrollListener?.reset()
|
|
|
|
}
|
|
|
|
isSwipeToRefreshEnabled -> {
|
|
|
|
binding.recyclerView.scrollBy(0, Utils.dpToPx(context, -30))
|
|
|
|
}
|
|
|
|
else -> binding.recyclerView.scrollToPosition(0)
|
|
|
|
}
|
2021-04-22 18:48:16 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onRemoved(position: Int, count: Int) {
|
|
|
|
adapter.notifyItemRangeRemoved(position, count)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
|
|
|
adapter.notifyItemMoved(fromPosition, toPosition)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
|
|
|
adapter.notifyItemRangeChanged(position, count, payload)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
private val differ = AsyncListDiffer(listUpdateCallback,
|
|
|
|
AsyncDifferConfig.Builder(diffCallback).build())
|
|
|
|
|
|
|
|
private val dataSource: TimelineAdapter.AdapterDataSource<StatusViewData> = object : TimelineAdapter.AdapterDataSource<StatusViewData> {
|
|
|
|
override fun getItemCount(): Int {
|
|
|
|
return differ.currentList.size
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun getItemAt(pos: Int): StatusViewData {
|
|
|
|
return differ.currentList[pos]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private var talkBackWasEnabled = false
|
|
|
|
|
|
|
|
override fun onResume() {
|
|
|
|
super.onResume()
|
|
|
|
val a11yManager = ContextCompat.getSystemService(requireContext(), AccessibilityManager::class.java)
|
|
|
|
|
|
|
|
val wasEnabled = talkBackWasEnabled
|
|
|
|
talkBackWasEnabled = a11yManager?.isEnabled == true
|
|
|
|
Log.d(TAG, "talkback was enabled: $wasEnabled, now $talkBackWasEnabled")
|
|
|
|
if (talkBackWasEnabled && !wasEnabled) {
|
|
|
|
adapter.notifyDataSetChanged()
|
|
|
|
}
|
|
|
|
startUpdateTimestamp()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Start to update adapter every minute to refresh timestamp
|
|
|
|
* If setting absoluteTimeView is false
|
|
|
|
* Auto dispose observable on pause
|
|
|
|
*/
|
|
|
|
private fun startUpdateTimestamp() {
|
|
|
|
val preferences = PreferenceManager.getDefaultSharedPreferences(activity)
|
|
|
|
val useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false)
|
|
|
|
if (!useAbsoluteTime) {
|
|
|
|
Observable.interval(1, TimeUnit.MINUTES)
|
|
|
|
.observeOn(AndroidSchedulers.mainThread())
|
|
|
|
.autoDispose(from(this, Lifecycle.Event.ON_PAUSE))
|
|
|
|
.subscribe { updateAdapter() }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onReselect() {
|
|
|
|
if (isAdded) {
|
|
|
|
layoutManager!!.scrollToPosition(0)
|
|
|
|
binding.recyclerView.stopScroll()
|
|
|
|
scrollListener!!.reset()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-28 04:54:29 +02:00
|
|
|
override fun onReset() {
|
|
|
|
fullyRefresh()
|
|
|
|
}
|
|
|
|
|
2021-04-22 18:48:16 +02:00
|
|
|
override fun refreshContent() {
|
|
|
|
if (isAdded) {
|
|
|
|
onRefresh()
|
|
|
|
} else {
|
|
|
|
isNeedRefresh = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
enum class Kind {
|
|
|
|
HOME, PUBLIC_LOCAL, PUBLIC_FEDERATED, TAG, USER, USER_PINNED, USER_WITH_REPLIES, FAVOURITES, LIST, BOOKMARKS
|
|
|
|
}
|
|
|
|
|
|
|
|
private enum class FetchEnd {
|
|
|
|
TOP, BOTTOM, MIDDLE
|
|
|
|
}
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
private const val TAG = "TimelineF" // logging tag
|
|
|
|
private const val KIND_ARG = "kind"
|
|
|
|
private const val ID_ARG = "id"
|
|
|
|
private const val HASHTAGS_ARG = "hashtags"
|
|
|
|
private const val ARG_ENABLE_SWIPE_TO_REFRESH = "enableSwipeToRefresh"
|
2021-04-28 04:54:29 +02:00
|
|
|
private const val ARG_ENABLE_STREAMING = "enableStreaming"
|
2021-04-22 18:48:16 +02:00
|
|
|
private const val LOAD_AT_ONCE = 30
|
|
|
|
|
2021-04-28 04:54:29 +02:00
|
|
|
fun newInstance(kind: Kind, hashtagOrId: String? = null, enableSwipeToRefresh: Boolean = true, enableStreaming: Boolean = false): TimelineFragment {
|
2021-04-22 18:48:16 +02:00
|
|
|
val fragment = TimelineFragment()
|
|
|
|
val arguments = Bundle(3)
|
|
|
|
arguments.putString(KIND_ARG, kind.name)
|
|
|
|
arguments.putString(ID_ARG, hashtagOrId)
|
|
|
|
arguments.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, enableSwipeToRefresh)
|
2021-04-28 04:54:29 +02:00
|
|
|
arguments.putBoolean(ARG_ENABLE_STREAMING, enableStreaming)
|
2021-04-22 18:48:16 +02:00
|
|
|
fragment.arguments = arguments
|
|
|
|
return fragment
|
|
|
|
}
|
|
|
|
|
|
|
|
@JvmStatic
|
|
|
|
fun newHashtagInstance(hashtags: List<String>): TimelineFragment {
|
|
|
|
val fragment = TimelineFragment()
|
|
|
|
val arguments = Bundle(3)
|
|
|
|
arguments.putString(KIND_ARG, Kind.TAG.name)
|
|
|
|
arguments.putStringArrayList(HASHTAGS_ARG, ArrayList(hashtags))
|
|
|
|
arguments.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true)
|
|
|
|
fragment.arguments = arguments
|
|
|
|
return fragment
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun filterContextMatchesKind(kind: Kind?, filterContext: List<String>): Boolean {
|
|
|
|
// home, notifications, public, thread
|
|
|
|
return when (kind) {
|
|
|
|
Kind.HOME, Kind.LIST -> filterContext.contains(Filter.HOME)
|
|
|
|
Kind.PUBLIC_FEDERATED, Kind.PUBLIC_LOCAL, Kind.TAG -> filterContext.contains(Filter.PUBLIC)
|
|
|
|
Kind.FAVOURITES -> filterContext.contains(Filter.PUBLIC) || filterContext.contains(Filter.NOTIFICATIONS)
|
|
|
|
Kind.USER, Kind.USER_WITH_REPLIES, Kind.USER_PINNED -> filterContext.contains(Filter.ACCOUNT)
|
|
|
|
else -> false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private val diffCallback: DiffUtil.ItemCallback<StatusViewData> = object : DiffUtil.ItemCallback<StatusViewData>() {
|
|
|
|
override fun areItemsTheSame(oldItem: StatusViewData, newItem: StatusViewData): Boolean {
|
|
|
|
return oldItem.viewDataId == newItem.viewDataId
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun areContentsTheSame(oldItem: StatusViewData, newItem: StatusViewData): Boolean {
|
|
|
|
return false // Items are different always. It allows to refresh timestamp on every view holder update
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun getChangePayload(oldItem: StatusViewData, newItem: StatusViewData): Any? {
|
|
|
|
return if (oldItem.deepEquals(newItem)) {
|
|
|
|
// If items are equal - update timestamp only
|
|
|
|
listOf(StatusBaseViewHolder.Key.KEY_CREATED)
|
|
|
|
} else // If items are different - update the whole view holder
|
|
|
|
null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-04-28 04:54:29 +02:00
|
|
|
}
|