2022-08-15 11:00:18 +02:00
|
|
|
/* 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
|
2023-01-02 14:09:18 +01:00
|
|
|
import androidx.fragment.app.commit
|
2022-08-15 11:00:18 +02:00
|
|
|
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
|
2022-11-10 14:20:09 +01:00
|
|
|
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository.Companion.CAN_USE_QUOTE_ID
|
2023-01-02 14:09:18 +01:00
|
|
|
import com.keylesspalace.tusky.components.viewthread.edits.ViewEditsFragment
|
2022-08-15 11:00:18 +02:00
|
|
|
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
|
2022-08-15 11:00:18 +02:00
|
|
|
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),
|
2022-11-10 14:20:09 +01:00
|
|
|
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false),
|
|
|
|
quoteEnabled = accountManager.activeAccount!!.domain in CAN_USE_QUOTE_ID
|
2022-08-15 11:00:18 +02:00
|
|
|
)
|
|
|
|
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 {
|
2022-11-04 19:22:38 +01:00
|
|
|
activity?.onBackPressedDispatcher?.onBackPressed()
|
2022-08-15 11:00:18 +02:00
|
|
|
}
|
2023-01-02 14:09:18 +01:00
|
|
|
binding.toolbar.inflateMenu(R.menu.view_thread_toolbar)
|
2022-08-15 11:00:18 +02:00
|
|
|
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)
|
|
|
|
|
2022-08-15 11:00:18 +02:00
|
|
|
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
|
|
|
|
2022-08-15 11:00:18 +02: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()
|
2022-08-15 11:00:18 +02:00
|
|
|
}
|
|
|
|
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()
|
2022-08-15 11:00:18 +02:00
|
|
|
|
|
|
|
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 -> {
|
2023-01-12 19:41:07 +01:00
|
|
|
if (uiState.statusViewData.none { viewData -> viewData.isDetailed }) {
|
2022-12-05 14:44:52 +01:00
|
|
|
// 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()
|
2022-12-05 14:44:52 +01:00
|
|
|
|
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) {
|
2022-08-15 11:00:18 +02:00
|
|
|
if (viewModel.isInitialLoad) {
|
|
|
|
viewModel.isInitialLoad = false
|
2023-01-12 19:41:07 +01:00
|
|
|
|
|
|
|
// Ensure the top of the status is visible
|
|
|
|
(binding.recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(uiState.detailedStatusPosition, 0)
|
2022-08-15 11:00:18 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2022-08-15 11:00:18 +02:00
|
|
|
|
|
|
|
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()
|
2022-08-15 11:00:18 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-15 11:00:18 +02:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2022-11-10 14:20:09 +01:00
|
|
|
override fun onQuote(position: Int) {
|
|
|
|
super.quote(adapter.currentList[position].status)
|
|
|
|
}
|
|
|
|
|
2022-08-15 11:00:18 +02:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2022-11-10 14:20:09 +01:00
|
|
|
override fun onViewUrl(url: String, text: String) {
|
2022-08-15 11:00:18 +02:00
|
|
|
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
|
|
|
|
}
|
2022-11-10 14:20:09 +01:00
|
|
|
super.onViewUrl(url, text)
|
2022-08-15 11:00:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2023-01-02 14:09:18 +01:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-15 11:00:18 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|