Yuito-app-android/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt

419 lines
17 KiB
Kotlin
Raw Normal View History

/* Copyright 2022 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.components.viewthread
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
Improve the actual and perceived speed of thread loading (#3118) * Improve the actual and perceived speed of thread loading To improve the actual speed, note that if the user has opened a thread from their home timeline then the initial status is cached in the database. Other statuses in the same thread may be cached as well. So try and load the initial status from the database, falling back to the network if it's not present (e.g., the user has opened a thread from the local or federated timelines, or a search). Introduce a new loading state to deal with this case. In typical cases this allows the UI to display the initial status immediately with no need to show a progress indicator. To improve the perceived speed, delay showing the initial loading circular progress indicators by 500ms. If loading the initial status completes within that time no spinner is shown and the user will perceive the action as close-to-immediate (https://www.nngroup.com/articles/response-times-3-important-limits/). Additionally, introduce an extra indeterminate progress indicator. The new indicator is linear, anchored to the bottom of the screen, and shows progress loading ancestor/descendant statuses. Like the other indicator is also delayed 500ms from when ancestor/descendant status information is fetched, and if the fetch completes in that time it will not be shown. * Mark `getStatus` as suspend so it doesn't run on the main thread * Save an allocation, use an isDetailed parameter to TimelineStatusWithAccount.toViewData Rename Status.toViewData's "detailed" parameter to "isDetailed" for consistency with other uses. * Ensure suspend functions run to completion when testing * Delay-load the status from the network even if it's cached This speeds up the UI while ensuring it will eventually contain accurate data from the remote. * Load the network status before updating the list Avoids excess animations if the network copy has changes * Fix UI flicker when loading reblogged statuses * Lint * Fixup tests
2023-01-09 21:31:31 +01:00
import androidx.annotation.CheckResult
import androidx.fragment.app.commit
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import com.google.android.material.snackbar.Snackbar
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.components.instanceinfo.InstanceInfoRepository.Companion.CAN_USE_QUOTE_ID
import com.keylesspalace.tusky.components.viewthread.edits.ViewEditsFragment
import com.keylesspalace.tusky.databinding.FragmentViewThreadBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.fragment.SFragment
import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.CardViewMode
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.openLink
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.viewdata.AttachmentViewData.Companion.list
import com.keylesspalace.tusky.viewdata.StatusViewData
Improve the actual and perceived speed of thread loading (#3118) * Improve the actual and perceived speed of thread loading To improve the actual speed, note that if the user has opened a thread from their home timeline then the initial status is cached in the database. Other statuses in the same thread may be cached as well. So try and load the initial status from the database, falling back to the network if it's not present (e.g., the user has opened a thread from the local or federated timelines, or a search). Introduce a new loading state to deal with this case. In typical cases this allows the UI to display the initial status immediately with no need to show a progress indicator. To improve the perceived speed, delay showing the initial loading circular progress indicators by 500ms. If loading the initial status completes within that time no spinner is shown and the user will perceive the action as close-to-immediate (https://www.nngroup.com/articles/response-times-3-important-limits/). Additionally, introduce an extra indeterminate progress indicator. The new indicator is linear, anchored to the bottom of the screen, and shows progress loading ancestor/descendant statuses. Like the other indicator is also delayed 500ms from when ancestor/descendant status information is fetched, and if the fetch completes in that time it will not be shown. * Mark `getStatus` as suspend so it doesn't run on the main thread * Save an allocation, use an isDetailed parameter to TimelineStatusWithAccount.toViewData Rename Status.toViewData's "detailed" parameter to "isDetailed" for consistency with other uses. * Ensure suspend functions run to completion when testing * Delay-load the status from the network even if it's cached This speeds up the UI while ensuring it will eventually contain accurate data from the remote. * Load the network status before updating the list Avoids excess animations if the network copy has changes * Fix UI flicker when loading reblogged statuses * Lint * Fixup tests
2023-01-09 21:31:31 +01:00
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.IOException
import javax.inject.Inject
class ViewThreadFragment : SFragment(), OnRefreshListener, StatusActionListener, Injectable {
@Inject
lateinit var viewModelFactory: ViewModelFactory
private val viewModel: ViewThreadViewModel by viewModels { viewModelFactory }
private val binding by viewBinding(FragmentViewThreadBinding::bind)
private lateinit var adapter: ThreadAdapter
private lateinit var thisThreadsStatusId: String
private var alwaysShowSensitiveMedia = false
private var alwaysOpenSpoiler = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
thisThreadsStatusId = requireArguments().getString(ID_EXTRA)!!
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val statusDisplayOptions = StatusDisplayOptions(
animateAvatars = preferences.getBoolean("animateGifAvatars", false),
mediaPreviewEnabled = accountManager.activeAccount!!.mediaPreviewEnabled,
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false),
showBotOverlay = preferences.getBoolean("showBotOverlay", true),
useBlurhash = preferences.getBoolean("useBlurhash", true),
cardViewMode = if (preferences.getBoolean("showCardsInTimelines", false)) {
CardViewMode.INDENTED
} else {
CardViewMode.NONE
},
confirmReblogs = preferences.getBoolean("confirmReblogs", true),
confirmFavourites = preferences.getBoolean("confirmFavourites", false),
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false),
quoteEnabled = accountManager.activeAccount!!.domain in CAN_USE_QUOTE_ID
)
adapter = ThreadAdapter(statusDisplayOptions, this)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_view_thread, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.toolbar.setNavigationOnClickListener {
activity?.onBackPressedDispatcher?.onBackPressed()
}
binding.toolbar.inflateMenu(R.menu.view_thread_toolbar)
binding.toolbar.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.action_reveal -> {
viewModel.toggleRevealButton()
true
}
R.id.action_open_in_web -> {
context?.openLink(requireArguments().getString(URL_EXTRA)!!)
true
}
else -> false
}
}
binding.swipeRefreshLayout.setOnRefreshListener(this)
binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = LinearLayoutManager(context)
binding.recyclerView.setAccessibilityDelegateCompat(
ListStatusAccessibilityDelegate(
binding.recyclerView,
this
) { index -> adapter.currentList.getOrNull(index) }
)
val divider = DividerItemDecoration(context, LinearLayout.VERTICAL)
binding.recyclerView.addItemDecoration(divider)
binding.recyclerView.addItemDecoration(ConversationLineItemDecoration(requireContext()))
alwaysShowSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia
alwaysOpenSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler
binding.recyclerView.adapter = adapter
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
Improve the actual and perceived speed of thread loading (#3118) * Improve the actual and perceived speed of thread loading To improve the actual speed, note that if the user has opened a thread from their home timeline then the initial status is cached in the database. Other statuses in the same thread may be cached as well. So try and load the initial status from the database, falling back to the network if it's not present (e.g., the user has opened a thread from the local or federated timelines, or a search). Introduce a new loading state to deal with this case. In typical cases this allows the UI to display the initial status immediately with no need to show a progress indicator. To improve the perceived speed, delay showing the initial loading circular progress indicators by 500ms. If loading the initial status completes within that time no spinner is shown and the user will perceive the action as close-to-immediate (https://www.nngroup.com/articles/response-times-3-important-limits/). Additionally, introduce an extra indeterminate progress indicator. The new indicator is linear, anchored to the bottom of the screen, and shows progress loading ancestor/descendant statuses. Like the other indicator is also delayed 500ms from when ancestor/descendant status information is fetched, and if the fetch completes in that time it will not be shown. * Mark `getStatus` as suspend so it doesn't run on the main thread * Save an allocation, use an isDetailed parameter to TimelineStatusWithAccount.toViewData Rename Status.toViewData's "detailed" parameter to "isDetailed" for consistency with other uses. * Ensure suspend functions run to completion when testing * Delay-load the status from the network even if it's cached This speeds up the UI while ensuring it will eventually contain accurate data from the remote. * Load the network status before updating the list Avoids excess animations if the network copy has changes * Fix UI flicker when loading reblogged statuses * Lint * Fixup tests
2023-01-09 21:31:31 +01:00
var initialProgressBar = getProgressBarJob(binding.initialProgressBar, 500)
var threadProgressBar = getProgressBarJob(binding.threadProgressBar, 500)
viewLifecycleOwner.lifecycleScope.launch {
viewModel.uiState.collect { uiState ->
when (uiState) {
is ThreadUiState.Loading -> {
updateRevealButton(RevealButtonState.NO_BUTTON)
Improve the actual and perceived speed of thread loading (#3118) * Improve the actual and perceived speed of thread loading To improve the actual speed, note that if the user has opened a thread from their home timeline then the initial status is cached in the database. Other statuses in the same thread may be cached as well. So try and load the initial status from the database, falling back to the network if it's not present (e.g., the user has opened a thread from the local or federated timelines, or a search). Introduce a new loading state to deal with this case. In typical cases this allows the UI to display the initial status immediately with no need to show a progress indicator. To improve the perceived speed, delay showing the initial loading circular progress indicators by 500ms. If loading the initial status completes within that time no spinner is shown and the user will perceive the action as close-to-immediate (https://www.nngroup.com/articles/response-times-3-important-limits/). Additionally, introduce an extra indeterminate progress indicator. The new indicator is linear, anchored to the bottom of the screen, and shows progress loading ancestor/descendant statuses. Like the other indicator is also delayed 500ms from when ancestor/descendant status information is fetched, and if the fetch completes in that time it will not be shown. * Mark `getStatus` as suspend so it doesn't run on the main thread * Save an allocation, use an isDetailed parameter to TimelineStatusWithAccount.toViewData Rename Status.toViewData's "detailed" parameter to "isDetailed" for consistency with other uses. * Ensure suspend functions run to completion when testing * Delay-load the status from the network even if it's cached This speeds up the UI while ensuring it will eventually contain accurate data from the remote. * Load the network status before updating the list Avoids excess animations if the network copy has changes * Fix UI flicker when loading reblogged statuses * Lint * Fixup tests
2023-01-09 21:31:31 +01:00
binding.recyclerView.hide()
binding.statusView.hide()
Improve the actual and perceived speed of thread loading (#3118) * Improve the actual and perceived speed of thread loading To improve the actual speed, note that if the user has opened a thread from their home timeline then the initial status is cached in the database. Other statuses in the same thread may be cached as well. So try and load the initial status from the database, falling back to the network if it's not present (e.g., the user has opened a thread from the local or federated timelines, or a search). Introduce a new loading state to deal with this case. In typical cases this allows the UI to display the initial status immediately with no need to show a progress indicator. To improve the perceived speed, delay showing the initial loading circular progress indicators by 500ms. If loading the initial status completes within that time no spinner is shown and the user will perceive the action as close-to-immediate (https://www.nngroup.com/articles/response-times-3-important-limits/). Additionally, introduce an extra indeterminate progress indicator. The new indicator is linear, anchored to the bottom of the screen, and shows progress loading ancestor/descendant statuses. Like the other indicator is also delayed 500ms from when ancestor/descendant status information is fetched, and if the fetch completes in that time it will not be shown. * Mark `getStatus` as suspend so it doesn't run on the main thread * Save an allocation, use an isDetailed parameter to TimelineStatusWithAccount.toViewData Rename Status.toViewData's "detailed" parameter to "isDetailed" for consistency with other uses. * Ensure suspend functions run to completion when testing * Delay-load the status from the network even if it's cached This speeds up the UI while ensuring it will eventually contain accurate data from the remote. * Load the network status before updating the list Avoids excess animations if the network copy has changes * Fix UI flicker when loading reblogged statuses * Lint * Fixup tests
2023-01-09 21:31:31 +01:00
initialProgressBar = getProgressBarJob(binding.initialProgressBar, 500)
initialProgressBar.start()
}
is ThreadUiState.LoadingThread -> {
if (uiState.statusViewDatum == null) {
// no detailed statuses available, e.g. because author is blocked
activity?.finish()
return@collect
}
initialProgressBar.cancel()
threadProgressBar = getProgressBarJob(binding.threadProgressBar, 500)
threadProgressBar.start()
adapter.submitList(listOf(uiState.statusViewDatum))
updateRevealButton(uiState.revealButton)
binding.swipeRefreshLayout.isRefreshing = false
binding.recyclerView.show()
binding.statusView.hide()
}
is ThreadUiState.Error -> {
Log.w(TAG, "failed to load status", uiState.throwable)
Improve the actual and perceived speed of thread loading (#3118) * Improve the actual and perceived speed of thread loading To improve the actual speed, note that if the user has opened a thread from their home timeline then the initial status is cached in the database. Other statuses in the same thread may be cached as well. So try and load the initial status from the database, falling back to the network if it's not present (e.g., the user has opened a thread from the local or federated timelines, or a search). Introduce a new loading state to deal with this case. In typical cases this allows the UI to display the initial status immediately with no need to show a progress indicator. To improve the perceived speed, delay showing the initial loading circular progress indicators by 500ms. If loading the initial status completes within that time no spinner is shown and the user will perceive the action as close-to-immediate (https://www.nngroup.com/articles/response-times-3-important-limits/). Additionally, introduce an extra indeterminate progress indicator. The new indicator is linear, anchored to the bottom of the screen, and shows progress loading ancestor/descendant statuses. Like the other indicator is also delayed 500ms from when ancestor/descendant status information is fetched, and if the fetch completes in that time it will not be shown. * Mark `getStatus` as suspend so it doesn't run on the main thread * Save an allocation, use an isDetailed parameter to TimelineStatusWithAccount.toViewData Rename Status.toViewData's "detailed" parameter to "isDetailed" for consistency with other uses. * Ensure suspend functions run to completion when testing * Delay-load the status from the network even if it's cached This speeds up the UI while ensuring it will eventually contain accurate data from the remote. * Load the network status before updating the list Avoids excess animations if the network copy has changes * Fix UI flicker when loading reblogged statuses * Lint * Fixup tests
2023-01-09 21:31:31 +01:00
initialProgressBar.cancel()
threadProgressBar.cancel()
updateRevealButton(RevealButtonState.NO_BUTTON)
binding.swipeRefreshLayout.isRefreshing = false
binding.recyclerView.hide()
binding.statusView.show()
if (uiState.throwable is IOException) {
binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network) {
viewModel.retry(thisThreadsStatusId)
}
} else {
binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic) {
viewModel.retry(thisThreadsStatusId)
}
}
}
is ThreadUiState.Success -> {
if (uiState.statusViewData.none { viewData -> viewData.isDetailed }) {
// no detailed statuses available, e.g. because author is blocked
activity?.finish()
return@collect
}
Improve the actual and perceived speed of thread loading (#3118) * Improve the actual and perceived speed of thread loading To improve the actual speed, note that if the user has opened a thread from their home timeline then the initial status is cached in the database. Other statuses in the same thread may be cached as well. So try and load the initial status from the database, falling back to the network if it's not present (e.g., the user has opened a thread from the local or federated timelines, or a search). Introduce a new loading state to deal with this case. In typical cases this allows the UI to display the initial status immediately with no need to show a progress indicator. To improve the perceived speed, delay showing the initial loading circular progress indicators by 500ms. If loading the initial status completes within that time no spinner is shown and the user will perceive the action as close-to-immediate (https://www.nngroup.com/articles/response-times-3-important-limits/). Additionally, introduce an extra indeterminate progress indicator. The new indicator is linear, anchored to the bottom of the screen, and shows progress loading ancestor/descendant statuses. Like the other indicator is also delayed 500ms from when ancestor/descendant status information is fetched, and if the fetch completes in that time it will not be shown. * Mark `getStatus` as suspend so it doesn't run on the main thread * Save an allocation, use an isDetailed parameter to TimelineStatusWithAccount.toViewData Rename Status.toViewData's "detailed" parameter to "isDetailed" for consistency with other uses. * Ensure suspend functions run to completion when testing * Delay-load the status from the network even if it's cached This speeds up the UI while ensuring it will eventually contain accurate data from the remote. * Load the network status before updating the list Avoids excess animations if the network copy has changes * Fix UI flicker when loading reblogged statuses * Lint * Fixup tests
2023-01-09 21:31:31 +01:00
threadProgressBar.cancel()
Improve the actual and perceived speed of thread loading (#3118) * Improve the actual and perceived speed of thread loading To improve the actual speed, note that if the user has opened a thread from their home timeline then the initial status is cached in the database. Other statuses in the same thread may be cached as well. So try and load the initial status from the database, falling back to the network if it's not present (e.g., the user has opened a thread from the local or federated timelines, or a search). Introduce a new loading state to deal with this case. In typical cases this allows the UI to display the initial status immediately with no need to show a progress indicator. To improve the perceived speed, delay showing the initial loading circular progress indicators by 500ms. If loading the initial status completes within that time no spinner is shown and the user will perceive the action as close-to-immediate (https://www.nngroup.com/articles/response-times-3-important-limits/). Additionally, introduce an extra indeterminate progress indicator. The new indicator is linear, anchored to the bottom of the screen, and shows progress loading ancestor/descendant statuses. Like the other indicator is also delayed 500ms from when ancestor/descendant status information is fetched, and if the fetch completes in that time it will not be shown. * Mark `getStatus` as suspend so it doesn't run on the main thread * Save an allocation, use an isDetailed parameter to TimelineStatusWithAccount.toViewData Rename Status.toViewData's "detailed" parameter to "isDetailed" for consistency with other uses. * Ensure suspend functions run to completion when testing * Delay-load the status from the network even if it's cached This speeds up the UI while ensuring it will eventually contain accurate data from the remote. * Load the network status before updating the list Avoids excess animations if the network copy has changes * Fix UI flicker when loading reblogged statuses * Lint * Fixup tests
2023-01-09 21:31:31 +01:00
adapter.submitList(uiState.statusViewData) {
if (viewModel.isInitialLoad) {
viewModel.isInitialLoad = false
// Ensure the top of the status is visible
(binding.recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(uiState.detailedStatusPosition, 0)
}
}
updateRevealButton(uiState.revealButton)
Improve the actual and perceived speed of thread loading (#3118) * Improve the actual and perceived speed of thread loading To improve the actual speed, note that if the user has opened a thread from their home timeline then the initial status is cached in the database. Other statuses in the same thread may be cached as well. So try and load the initial status from the database, falling back to the network if it's not present (e.g., the user has opened a thread from the local or federated timelines, or a search). Introduce a new loading state to deal with this case. In typical cases this allows the UI to display the initial status immediately with no need to show a progress indicator. To improve the perceived speed, delay showing the initial loading circular progress indicators by 500ms. If loading the initial status completes within that time no spinner is shown and the user will perceive the action as close-to-immediate (https://www.nngroup.com/articles/response-times-3-important-limits/). Additionally, introduce an extra indeterminate progress indicator. The new indicator is linear, anchored to the bottom of the screen, and shows progress loading ancestor/descendant statuses. Like the other indicator is also delayed 500ms from when ancestor/descendant status information is fetched, and if the fetch completes in that time it will not be shown. * Mark `getStatus` as suspend so it doesn't run on the main thread * Save an allocation, use an isDetailed parameter to TimelineStatusWithAccount.toViewData Rename Status.toViewData's "detailed" parameter to "isDetailed" for consistency with other uses. * Ensure suspend functions run to completion when testing * Delay-load the status from the network even if it's cached This speeds up the UI while ensuring it will eventually contain accurate data from the remote. * Load the network status before updating the list Avoids excess animations if the network copy has changes * Fix UI flicker when loading reblogged statuses * Lint * Fixup tests
2023-01-09 21:31:31 +01:00
binding.swipeRefreshLayout.isRefreshing = false
binding.recyclerView.show()
binding.statusView.hide()
Improve the actual and perceived speed of thread loading (#3118) * Improve the actual and perceived speed of thread loading To improve the actual speed, note that if the user has opened a thread from their home timeline then the initial status is cached in the database. Other statuses in the same thread may be cached as well. So try and load the initial status from the database, falling back to the network if it's not present (e.g., the user has opened a thread from the local or federated timelines, or a search). Introduce a new loading state to deal with this case. In typical cases this allows the UI to display the initial status immediately with no need to show a progress indicator. To improve the perceived speed, delay showing the initial loading circular progress indicators by 500ms. If loading the initial status completes within that time no spinner is shown and the user will perceive the action as close-to-immediate (https://www.nngroup.com/articles/response-times-3-important-limits/). Additionally, introduce an extra indeterminate progress indicator. The new indicator is linear, anchored to the bottom of the screen, and shows progress loading ancestor/descendant statuses. Like the other indicator is also delayed 500ms from when ancestor/descendant status information is fetched, and if the fetch completes in that time it will not be shown. * Mark `getStatus` as suspend so it doesn't run on the main thread * Save an allocation, use an isDetailed parameter to TimelineStatusWithAccount.toViewData Rename Status.toViewData's "detailed" parameter to "isDetailed" for consistency with other uses. * Ensure suspend functions run to completion when testing * Delay-load the status from the network even if it's cached This speeds up the UI while ensuring it will eventually contain accurate data from the remote. * Load the network status before updating the list Avoids excess animations if the network copy has changes * Fix UI flicker when loading reblogged statuses * Lint * Fixup tests
2023-01-09 21:31:31 +01:00
}
is ThreadUiState.Refreshing -> {
threadProgressBar.cancel()
}
}
}
}
lifecycleScope.launch {
viewModel.errors.collect { throwable ->
Log.w(TAG, "failed to load status context", throwable)
Snackbar.make(binding.root, R.string.error_generic, Snackbar.LENGTH_SHORT)
.setAction(R.string.action_retry) {
viewModel.retry(thisThreadsStatusId)
}
.show()
}
}
viewModel.loadThread(thisThreadsStatusId)
}
Improve the actual and perceived speed of thread loading (#3118) * Improve the actual and perceived speed of thread loading To improve the actual speed, note that if the user has opened a thread from their home timeline then the initial status is cached in the database. Other statuses in the same thread may be cached as well. So try and load the initial status from the database, falling back to the network if it's not present (e.g., the user has opened a thread from the local or federated timelines, or a search). Introduce a new loading state to deal with this case. In typical cases this allows the UI to display the initial status immediately with no need to show a progress indicator. To improve the perceived speed, delay showing the initial loading circular progress indicators by 500ms. If loading the initial status completes within that time no spinner is shown and the user will perceive the action as close-to-immediate (https://www.nngroup.com/articles/response-times-3-important-limits/). Additionally, introduce an extra indeterminate progress indicator. The new indicator is linear, anchored to the bottom of the screen, and shows progress loading ancestor/descendant statuses. Like the other indicator is also delayed 500ms from when ancestor/descendant status information is fetched, and if the fetch completes in that time it will not be shown. * Mark `getStatus` as suspend so it doesn't run on the main thread * Save an allocation, use an isDetailed parameter to TimelineStatusWithAccount.toViewData Rename Status.toViewData's "detailed" parameter to "isDetailed" for consistency with other uses. * Ensure suspend functions run to completion when testing * Delay-load the status from the network even if it's cached This speeds up the UI while ensuring it will eventually contain accurate data from the remote. * Load the network status before updating the list Avoids excess animations if the network copy has changes * Fix UI flicker when loading reblogged statuses * Lint * Fixup tests
2023-01-09 21:31:31 +01:00
/**
* Create a job to implement a delayed-visible progress bar.
*
* Delaying the visibility of the progress bar can improve user perception of UI speed because
* fewer UI elements are appearing and disappearing.
*
* When started the job will wait `delayMs` then show `view`. If the job is cancelled at
* any time `view` is hidden.
*/
@CheckResult()
private fun getProgressBarJob(view: View, delayMs: Long) = viewLifecycleOwner.lifecycleScope.launch(
start = CoroutineStart.LAZY
) {
try {
delay(delayMs)
view.show()
awaitCancellation()
} finally {
view.hide()
}
}
private fun updateRevealButton(state: RevealButtonState) {
val menuItem = binding.toolbar.menu.findItem(R.id.action_reveal)
menuItem.isVisible = state != RevealButtonState.NO_BUTTON
menuItem.setIcon(if (state == RevealButtonState.REVEAL) R.drawable.ic_eye_24dp else R.drawable.ic_hide_media_24dp)
}
override fun onRefresh() {
viewModel.refresh(thisThreadsStatusId)
}
override fun onReply(position: Int) {
super.reply(adapter.currentList[position].status)
}
override fun onReblog(reblog: Boolean, position: Int) {
val status = adapter.currentList[position]
viewModel.reblog(reblog, status)
}
override fun onFavourite(favourite: Boolean, position: Int) {
val status = adapter.currentList[position]
viewModel.favorite(favourite, status)
}
override fun onQuote(position: Int) {
super.quote(adapter.currentList[position].status)
}
override fun onBookmark(bookmark: Boolean, position: Int) {
val status = adapter.currentList[position]
viewModel.bookmark(bookmark, status)
}
override fun onMore(view: View, position: Int) {
super.more(adapter.currentList[position].status, view, position)
}
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
val status = adapter.currentList[position].status
super.viewMedia(attachmentIndex, list(status), view)
}
override fun onViewThread(position: Int) {
val status = adapter.currentList[position]
if (thisThreadsStatusId == status.id) {
// If already viewing this thread, don't reopen it.
return
}
super.viewThread(status.actionableId, status.actionable.url)
}
override fun onViewUrl(url: String, text: String) {
val status: StatusViewData.Concrete? = viewModel.detailedStatus()
if (status != null && status.status.url == url) {
// already viewing the status with this url
// probably just a preview federated and the user is clicking again to view more -> open the browser
// this can happen with some friendica statuses
requireContext().openLink(url)
return
}
super.onViewUrl(url, text)
}
override fun onOpenReblog(position: Int) {
// there are no reblogs in threads
}
override fun onExpandedChange(expanded: Boolean, position: Int) {
viewModel.changeExpanded(expanded, adapter.currentList[position])
}
override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
viewModel.changeContentShowing(isShowing, adapter.currentList[position])
}
override fun onLoadMore(position: Int) {
// only used in timelines
}
override fun onShowReblogs(position: Int) {
val statusId = adapter.currentList[position].id
val intent = newIntent(requireContext(), AccountListActivity.Type.REBLOGGED, statusId)
(requireActivity() as BaseActivity).startActivityWithSlideInAnimation(intent)
}
override fun onShowFavs(position: Int) {
val statusId = adapter.currentList[position].id
val intent = newIntent(requireContext(), AccountListActivity.Type.FAVOURITED, statusId)
(requireActivity() as BaseActivity).startActivityWithSlideInAnimation(intent)
}
override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) {
viewModel.changeContentCollapsed(isCollapsed, adapter.currentList[position])
}
override fun onViewTag(tag: String) {
super.viewTag(tag)
}
override fun onViewAccount(id: String) {
super.viewAccount(id)
}
public override fun removeItem(position: Int) {
val status = adapter.currentList[position]
if (status.isDetailed) {
// the main status we are viewing is being removed, finish the activity
activity?.finish()
return
}
viewModel.removeStatus(status)
}
override fun onVoteInPoll(position: Int, choices: List<Int>) {
val status = adapter.currentList[position]
viewModel.voteInPoll(choices, status)
}
override fun onShowEdits(position: Int) {
val status = adapter.currentList[position]
val viewEditsFragment = ViewEditsFragment.newInstance(status.actionableId)
parentFragmentManager.commit {
setCustomAnimations(R.anim.slide_from_right, R.anim.slide_to_left, R.anim.slide_from_left, R.anim.slide_to_right)
replace(R.id.fragment_container, viewEditsFragment, "ViewEditsFragment_$id")
addToBackStack(null)
}
}
companion object {
private const val TAG = "ViewThreadFragment"
private const val ID_EXTRA = "id"
private const val URL_EXTRA = "url"
fun newInstance(id: String, url: String): ViewThreadFragment {
val arguments = Bundle(2)
val fragment = ViewThreadFragment()
arguments.putString(ID_EXTRA, id)
arguments.putString(URL_EXTRA, url)
fragment.arguments = arguments
return fragment
}
}
}