Refactor "trending hashtags" code (#3595)
- Fix codeformatting - Add new refreshing state - Disable LogConditional lint rule - Update lint-baseline
This commit is contained in:
parent
071e00774e
commit
f23c0cc634
13 changed files with 335 additions and 480 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -1,71 +0,0 @@
|
||||||
/* Copyright 2023 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.adapter
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.keylesspalace.tusky.R
|
|
||||||
import com.keylesspalace.tusky.databinding.ItemTrendingCellBinding
|
|
||||||
import com.keylesspalace.tusky.entity.TrendingTagHistory
|
|
||||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
|
||||||
import com.keylesspalace.tusky.util.formatNumber
|
|
||||||
import com.keylesspalace.tusky.viewdata.TrendingViewData
|
|
||||||
|
|
||||||
class TrendingTagViewHolder(
|
|
||||||
private val binding: ItemTrendingCellBinding
|
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
|
||||||
|
|
||||||
fun setup(
|
|
||||||
tagViewData: TrendingViewData.Tag,
|
|
||||||
maxTrendingValue: Long,
|
|
||||||
trendingListener: LinkListener
|
|
||||||
) {
|
|
||||||
val reversedHistory = tagViewData.tag.history.reversed()
|
|
||||||
setGraph(reversedHistory, maxTrendingValue)
|
|
||||||
setTag(tagViewData.tag.name)
|
|
||||||
|
|
||||||
val totalUsage = tagViewData.tag.history.sumOf { it.uses.toLongOrNull() ?: 0 }
|
|
||||||
binding.totalUsage.text = formatNumber(totalUsage)
|
|
||||||
|
|
||||||
val totalAccounts = tagViewData.tag.history.sumOf { it.accounts.toLongOrNull() ?: 0 }
|
|
||||||
binding.totalAccounts.text = formatNumber(totalAccounts)
|
|
||||||
|
|
||||||
binding.currentUsage.text = reversedHistory.last().uses
|
|
||||||
binding.currentAccounts.text = reversedHistory.last().accounts
|
|
||||||
|
|
||||||
itemView.setOnClickListener {
|
|
||||||
trendingListener.onViewTag(tagViewData.tag.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
setAccessibility(totalAccounts, tagViewData.tag.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setGraph(history: List<TrendingTagHistory>, maxTrendingValue: Long) {
|
|
||||||
binding.graph.maxTrendingValue = maxTrendingValue
|
|
||||||
binding.graph.primaryLineData = history
|
|
||||||
.mapNotNull { it.uses.toLongOrNull() }
|
|
||||||
binding.graph.secondaryLineData = history
|
|
||||||
.mapNotNull { it.accounts.toLongOrNull() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setTag(tag: String) {
|
|
||||||
binding.tag.text = binding.root.context.getString(R.string.title_tag, tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setAccessibility(totalAccounts: Long, tag: String) {
|
|
||||||
itemView.contentDescription =
|
|
||||||
itemView.context.getString(R.string.accessibility_talking_about_tag, totalAccounts, tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -19,23 +19,19 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import com.keylesspalace.tusky.BottomSheetActivity
|
import com.keylesspalace.tusky.BaseActivity
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
|
||||||
import com.keylesspalace.tusky.databinding.ActivityTrendingBinding
|
import com.keylesspalace.tusky.databinding.ActivityTrendingBinding
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
import dagger.android.DispatchingAndroidInjector
|
import dagger.android.DispatchingAndroidInjector
|
||||||
import dagger.android.HasAndroidInjector
|
import dagger.android.HasAndroidInjector
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class TrendingActivity : BottomSheetActivity(), HasAndroidInjector {
|
class TrendingActivity : BaseActivity(), HasAndroidInjector {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
|
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var eventHub: EventHub
|
|
||||||
|
|
||||||
private val binding: ActivityTrendingBinding by viewBinding(ActivityTrendingBinding::inflate)
|
private val binding: ActivityTrendingBinding by viewBinding(ActivityTrendingBinding::inflate)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|
@ -44,10 +40,8 @@ class TrendingActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
|
|
||||||
setSupportActionBar(binding.includedToolbar.toolbar)
|
setSupportActionBar(binding.includedToolbar.toolbar)
|
||||||
|
|
||||||
val title = getString(R.string.title_public_trending_hashtags)
|
|
||||||
|
|
||||||
supportActionBar?.run {
|
supportActionBar?.run {
|
||||||
setTitle(title)
|
setTitle(R.string.title_public_trending_hashtags)
|
||||||
setDisplayHomeAsUpEnabled(true)
|
setDisplayHomeAsUpEnabled(true)
|
||||||
setDisplayShowHomeEnabled(true)
|
setDisplayShowHomeEnabled(true)
|
||||||
}
|
}
|
||||||
|
|
@ -63,10 +57,6 @@ class TrendingActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
override fun androidInjector() = dispatchingAndroidInjector
|
override fun androidInjector() = dispatchingAndroidInjector
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "TrendingActivity"
|
fun getIntent(context: Context) = Intent(context, TrendingActivity::class.java)
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getIntent(context: Context) =
|
|
||||||
Intent(context, TrendingActivity::class.java)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,15 +20,12 @@ import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.keylesspalace.tusky.adapter.TrendingDateViewHolder
|
|
||||||
import com.keylesspalace.tusky.adapter.TrendingTagViewHolder
|
|
||||||
import com.keylesspalace.tusky.databinding.ItemTrendingCellBinding
|
import com.keylesspalace.tusky.databinding.ItemTrendingCellBinding
|
||||||
import com.keylesspalace.tusky.databinding.ItemTrendingDateBinding
|
import com.keylesspalace.tusky.databinding.ItemTrendingDateBinding
|
||||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
|
||||||
import com.keylesspalace.tusky.viewdata.TrendingViewData
|
import com.keylesspalace.tusky.viewdata.TrendingViewData
|
||||||
|
|
||||||
class TrendingAdapter(
|
class TrendingAdapter(
|
||||||
private val trendingListener: LinkListener
|
private val onViewTag: (String) -> Unit
|
||||||
) : ListAdapter<TrendingViewData, RecyclerView.ViewHolder>(TrendingDifferCallback) {
|
) : ListAdapter<TrendingViewData, RecyclerView.ViewHolder>(TrendingDifferCallback) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
@ -42,7 +39,6 @@ class TrendingAdapter(
|
||||||
ItemTrendingCellBinding.inflate(LayoutInflater.from(viewGroup.context))
|
ItemTrendingCellBinding.inflate(LayoutInflater.from(viewGroup.context))
|
||||||
TrendingTagViewHolder(binding)
|
TrendingTagViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
val binding =
|
val binding =
|
||||||
ItemTrendingDateBinding.inflate(LayoutInflater.from(viewGroup.context))
|
ItemTrendingDateBinding.inflate(LayoutInflater.from(viewGroup.context))
|
||||||
|
|
@ -52,38 +48,15 @@ class TrendingAdapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
|
||||||
bindViewHolder(viewHolder, position, null)
|
when (val viewData = getItem(position)) {
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(
|
|
||||||
viewHolder: RecyclerView.ViewHolder,
|
|
||||||
position: Int,
|
|
||||||
payloads: List<*>
|
|
||||||
) {
|
|
||||||
bindViewHolder(viewHolder, position, payloads)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun bindViewHolder(
|
|
||||||
viewHolder: RecyclerView.ViewHolder,
|
|
||||||
position: Int,
|
|
||||||
payloads: List<*>?
|
|
||||||
) {
|
|
||||||
when (val header = getItem(position)) {
|
|
||||||
is TrendingViewData.Tag -> {
|
is TrendingViewData.Tag -> {
|
||||||
val maxTrendingValue = currentList
|
|
||||||
.flatMap { trendingViewData ->
|
|
||||||
trendingViewData.asTagOrNull()?.tag?.history.orEmpty()
|
|
||||||
}
|
|
||||||
.mapNotNull { it.uses.toLongOrNull() }
|
|
||||||
.maxOrNull() ?: 1
|
|
||||||
|
|
||||||
val holder = viewHolder as TrendingTagViewHolder
|
val holder = viewHolder as TrendingTagViewHolder
|
||||||
holder.setup(header, maxTrendingValue, trendingListener)
|
holder.setup(viewData, onViewTag)
|
||||||
}
|
}
|
||||||
|
|
||||||
is TrendingViewData.Header -> {
|
is TrendingViewData.Header -> {
|
||||||
val holder = viewHolder as TrendingDateViewHolder
|
val holder = viewHolder as TrendingDateViewHolder
|
||||||
holder.setup(header.start, header.end)
|
holder.setup(viewData.start, viewData.end)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -112,14 +85,7 @@ class TrendingAdapter(
|
||||||
oldItem: TrendingViewData,
|
oldItem: TrendingViewData,
|
||||||
newItem: TrendingViewData
|
newItem: TrendingViewData
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return false
|
return oldItem == newItem
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChangePayload(
|
|
||||||
oldItem: TrendingViewData,
|
|
||||||
newItem: TrendingViewData
|
|
||||||
): Any? {
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
package com.keylesspalace.tusky.adapter
|
package com.keylesspalace.tusky.components.trending
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
|
|
@ -15,17 +15,14 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.components.trending
|
package com.keylesspalace.tusky.components.trending
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.accessibility.AccessibilityManager
|
import android.view.accessibility.AccessibilityManager
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup
|
import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup
|
||||||
|
|
@ -33,18 +30,14 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
|
||||||
import at.connyduck.sparkbutton.helpers.Utils
|
import at.connyduck.sparkbutton.helpers.Utils
|
||||||
import com.keylesspalace.tusky.BottomSheetActivity
|
import com.keylesspalace.tusky.BaseActivity
|
||||||
import com.keylesspalace.tusky.PostLookupFallbackBehavior
|
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.StatusListActivity
|
import com.keylesspalace.tusky.StatusListActivity
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
|
||||||
import com.keylesspalace.tusky.components.trending.viewmodel.TrendingViewModel
|
import com.keylesspalace.tusky.components.trending.viewmodel.TrendingViewModel
|
||||||
import com.keylesspalace.tusky.databinding.FragmentTrendingBinding
|
import com.keylesspalace.tusky.databinding.FragmentTrendingBinding
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
||||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
|
||||||
import com.keylesspalace.tusky.interfaces.RefreshableFragment
|
import com.keylesspalace.tusky.interfaces.RefreshableFragment
|
||||||
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
|
|
@ -56,48 +49,20 @@ import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class TrendingFragment :
|
class TrendingFragment :
|
||||||
Fragment(),
|
Fragment(R.layout.fragment_trending),
|
||||||
OnRefreshListener,
|
OnRefreshListener,
|
||||||
LinkListener,
|
|
||||||
Injectable,
|
Injectable,
|
||||||
ReselectableFragment,
|
ReselectableFragment,
|
||||||
RefreshableFragment {
|
RefreshableFragment {
|
||||||
|
|
||||||
private lateinit var bottomSheetActivity: BottomSheetActivity
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var viewModelFactory: ViewModelFactory
|
lateinit var viewModelFactory: ViewModelFactory
|
||||||
|
|
||||||
@Inject
|
private val viewModel: TrendingViewModel by viewModels { viewModelFactory }
|
||||||
lateinit var accountManager: AccountManager
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var eventHub: EventHub
|
|
||||||
|
|
||||||
private val viewModel: TrendingViewModel by lazy {
|
|
||||||
ViewModelProvider(this, viewModelFactory)[TrendingViewModel::class.java]
|
|
||||||
}
|
|
||||||
|
|
||||||
private val binding by viewBinding(FragmentTrendingBinding::bind)
|
private val binding by viewBinding(FragmentTrendingBinding::bind)
|
||||||
|
|
||||||
private lateinit var adapter: TrendingAdapter
|
private val adapter = TrendingAdapter(::onViewTag)
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
|
||||||
super.onAttach(context)
|
|
||||||
bottomSheetActivity = if (context is BottomSheetActivity) {
|
|
||||||
context
|
|
||||||
} else {
|
|
||||||
throw IllegalStateException("Fragment must be attached to a BottomSheetActivity!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
adapter = TrendingAdapter(
|
|
||||||
this
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
super.onConfigurationChanged(newConfig)
|
super.onConfigurationChanged(newConfig)
|
||||||
|
|
@ -106,14 +71,6 @@ class TrendingFragment :
|
||||||
setupLayoutManager(columnCount)
|
setupLayoutManager(columnCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View? {
|
|
||||||
return inflater.inflate(R.layout.fragment_trending, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
setupSwipeRefreshLayout()
|
setupSwipeRefreshLayout()
|
||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
|
|
@ -175,25 +132,19 @@ class TrendingFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRefresh() {
|
override fun onRefresh() {
|
||||||
viewModel.invalidate()
|
viewModel.invalidate(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewUrl(url: String) {
|
fun onViewTag(tag: String) {
|
||||||
bottomSheetActivity.viewUrl(url, PostLookupFallbackBehavior.OPEN_IN_BROWSER)
|
(requireActivity() as BaseActivity).startActivityWithSlideInAnimation(StatusListActivity.newHashtagIntent(requireContext(), tag))
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewTag(tag: String) {
|
|
||||||
bottomSheetActivity.startActivityWithSlideInAnimation(StatusListActivity.newHashtagIntent(requireContext(), tag))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewAccount(id: String) {
|
|
||||||
bottomSheetActivity.viewAccount(id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processViewState(uiState: TrendingViewModel.TrendingUiState) {
|
private fun processViewState(uiState: TrendingViewModel.TrendingUiState) {
|
||||||
|
Log.d(TAG, uiState.loadingState.name)
|
||||||
when (uiState.loadingState) {
|
when (uiState.loadingState) {
|
||||||
TrendingViewModel.LoadingState.INITIAL -> clearLoadingState()
|
TrendingViewModel.LoadingState.INITIAL -> clearLoadingState()
|
||||||
TrendingViewModel.LoadingState.LOADING -> applyLoadingState()
|
TrendingViewModel.LoadingState.LOADING -> applyLoadingState()
|
||||||
|
TrendingViewModel.LoadingState.REFRESHING -> applyRefreshingState()
|
||||||
TrendingViewModel.LoadingState.LOADED -> applyLoadedState(uiState.trendingViewData)
|
TrendingViewModel.LoadingState.LOADED -> applyLoadedState(uiState.trendingViewData)
|
||||||
TrendingViewModel.LoadingState.ERROR_NETWORK -> networkError()
|
TrendingViewModel.LoadingState.ERROR_NETWORK -> networkError()
|
||||||
TrendingViewModel.LoadingState.ERROR_OTHER -> otherError()
|
TrendingViewModel.LoadingState.ERROR_OTHER -> otherError()
|
||||||
|
|
@ -203,8 +154,9 @@ class TrendingFragment :
|
||||||
private fun applyLoadedState(viewData: List<TrendingViewData>) {
|
private fun applyLoadedState(viewData: List<TrendingViewData>) {
|
||||||
clearLoadingState()
|
clearLoadingState()
|
||||||
|
|
||||||
|
adapter.submitList(viewData)
|
||||||
|
|
||||||
if (viewData.isEmpty()) {
|
if (viewData.isEmpty()) {
|
||||||
adapter.submitList(emptyList())
|
|
||||||
binding.recyclerView.hide()
|
binding.recyclerView.hide()
|
||||||
binding.messageView.show()
|
binding.messageView.show()
|
||||||
binding.messageView.setup(
|
binding.messageView.setup(
|
||||||
|
|
@ -213,16 +165,16 @@ class TrendingFragment :
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
val viewDataWithDates = listOf(viewData.first().asHeaderOrNull()) + viewData
|
|
||||||
|
|
||||||
adapter.submitList(viewDataWithDates)
|
|
||||||
|
|
||||||
binding.recyclerView.show()
|
binding.recyclerView.show()
|
||||||
binding.messageView.hide()
|
binding.messageView.hide()
|
||||||
}
|
}
|
||||||
binding.progressBar.hide()
|
binding.progressBar.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun applyRefreshingState() {
|
||||||
|
binding.swipeRefreshLayout.isRefreshing = true
|
||||||
|
}
|
||||||
|
|
||||||
private fun applyLoadingState() {
|
private fun applyLoadingState() {
|
||||||
binding.recyclerView.hide()
|
binding.recyclerView.hide()
|
||||||
binding.messageView.hide()
|
binding.messageView.hide()
|
||||||
|
|
@ -297,8 +249,6 @@ class TrendingFragment :
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "TrendingFragment"
|
private const val TAG = "TrendingFragment"
|
||||||
|
|
||||||
fun newInstance(): TrendingFragment {
|
fun newInstance() = TrendingFragment()
|
||||||
return TrendingFragment()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
/* Copyright 2023 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.trending
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.databinding.ItemTrendingCellBinding
|
||||||
|
import com.keylesspalace.tusky.util.formatNumber
|
||||||
|
import com.keylesspalace.tusky.viewdata.TrendingViewData
|
||||||
|
|
||||||
|
class TrendingTagViewHolder(
|
||||||
|
private val binding: ItemTrendingCellBinding
|
||||||
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
fun setup(
|
||||||
|
tagViewData: TrendingViewData.Tag,
|
||||||
|
onViewTag: (String) -> Unit
|
||||||
|
) {
|
||||||
|
binding.tag.text = binding.root.context.getString(R.string.title_tag, tagViewData.name)
|
||||||
|
|
||||||
|
binding.graph.maxTrendingValue = tagViewData.maxTrendingValue
|
||||||
|
binding.graph.primaryLineData = tagViewData.usage
|
||||||
|
binding.graph.secondaryLineData = tagViewData.accounts
|
||||||
|
|
||||||
|
binding.totalUsage.text = formatNumber(tagViewData.usage.sum(), 1000)
|
||||||
|
|
||||||
|
val totalAccounts = tagViewData.accounts.sum()
|
||||||
|
binding.totalAccounts.text = formatNumber(totalAccounts, 1000)
|
||||||
|
|
||||||
|
binding.currentUsage.text = tagViewData.usage.last().toString()
|
||||||
|
binding.currentAccounts.text = tagViewData.usage.last().toString()
|
||||||
|
|
||||||
|
itemView.setOnClickListener {
|
||||||
|
onViewTag(tagViewData.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
itemView.contentDescription =
|
||||||
|
itemView.context.getString(
|
||||||
|
R.string.accessibility_talking_about_tag,
|
||||||
|
totalAccounts,
|
||||||
|
tagViewData.name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,11 +15,15 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.components.trending.viewmodel
|
package com.keylesspalace.tusky.components.trending.viewmodel
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||||
import com.keylesspalace.tusky.entity.Filter
|
import com.keylesspalace.tusky.entity.Filter
|
||||||
|
import com.keylesspalace.tusky.entity.end
|
||||||
|
import com.keylesspalace.tusky.entity.start
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.toViewData
|
import com.keylesspalace.tusky.util.toViewData
|
||||||
import com.keylesspalace.tusky.viewdata.TrendingViewData
|
import com.keylesspalace.tusky.viewdata.TrendingViewData
|
||||||
|
|
@ -28,7 +32,7 @@ import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.filterIsInstance
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import okio.IOException
|
import java.io.IOException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class TrendingViewModel @Inject constructor(
|
class TrendingViewModel @Inject constructor(
|
||||||
|
|
@ -36,7 +40,7 @@ class TrendingViewModel @Inject constructor(
|
||||||
private val eventHub: EventHub
|
private val eventHub: EventHub
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
enum class LoadingState {
|
enum class LoadingState {
|
||||||
INITIAL, LOADING, LOADED, ERROR_NETWORK, ERROR_OTHER
|
INITIAL, LOADING, REFRESHING, LOADED, ERROR_NETWORK, ERROR_OTHER
|
||||||
}
|
}
|
||||||
|
|
||||||
data class TrendingUiState(
|
data class TrendingUiState(
|
||||||
|
|
@ -67,37 +71,43 @@ class TrendingViewModel @Inject constructor(
|
||||||
*
|
*
|
||||||
* A tag is excluded if it is filtered by the user on their home timeline.
|
* A tag is excluded if it is filtered by the user on their home timeline.
|
||||||
*/
|
*/
|
||||||
fun invalidate() = viewModelScope.launch {
|
fun invalidate(refresh: Boolean = false) = viewModelScope.launch {
|
||||||
_uiState.value = TrendingUiState(emptyList(), LoadingState.LOADING)
|
if (refresh) {
|
||||||
|
_uiState.value = TrendingUiState(emptyList(), LoadingState.REFRESHING)
|
||||||
try {
|
} else {
|
||||||
val deferredFilters = async { mastodonApi.getFilters() }
|
_uiState.value = TrendingUiState(emptyList(), LoadingState.LOADING)
|
||||||
val response = mastodonApi.trendingTags()
|
|
||||||
if (!response.isSuccessful) {
|
|
||||||
_uiState.value = TrendingUiState(emptyList(), LoadingState.ERROR_NETWORK)
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
val homeFilters = deferredFilters.await().getOrNull()?.filter {
|
|
||||||
it.context.contains(Filter.Kind.HOME.kind)
|
|
||||||
}
|
|
||||||
|
|
||||||
val tags = response.body()!!
|
|
||||||
.filter {
|
|
||||||
homeFilters?.none { filter ->
|
|
||||||
filter.keywords.any { keyword -> keyword.keyword.equals(it.name, ignoreCase = true) }
|
|
||||||
} ?: false
|
|
||||||
}
|
|
||||||
.sortedBy { tag -> tag.history.sumOf { it.uses.toLongOrNull() ?: 0 } }
|
|
||||||
.map { it.toViewData() }
|
|
||||||
.asReversed()
|
|
||||||
|
|
||||||
_uiState.value = TrendingUiState(tags, LoadingState.LOADED)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
_uiState.value = TrendingUiState(emptyList(), LoadingState.ERROR_NETWORK)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
_uiState.value = TrendingUiState(emptyList(), LoadingState.ERROR_OTHER)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val deferredFilters = async { mastodonApi.getFilters() }
|
||||||
|
|
||||||
|
mastodonApi.trendingTags().fold(
|
||||||
|
{ tagResponse ->
|
||||||
|
val homeFilters = deferredFilters.await().getOrNull()?.filter { filter ->
|
||||||
|
filter.context.contains(Filter.Kind.HOME.kind)
|
||||||
|
}
|
||||||
|
val tags = tagResponse
|
||||||
|
.filter { tag ->
|
||||||
|
homeFilters?.none { filter ->
|
||||||
|
filter.keywords.any { keyword -> keyword.keyword.equals(tag.name, ignoreCase = true) }
|
||||||
|
} ?: false
|
||||||
|
}
|
||||||
|
.sortedByDescending { tag -> tag.history.sumOf { it.uses.toLongOrNull() ?: 0 } }
|
||||||
|
.toViewData()
|
||||||
|
|
||||||
|
val firstTag = tagResponse.first()
|
||||||
|
val header = TrendingViewData.Header(firstTag.start(), firstTag.end())
|
||||||
|
|
||||||
|
_uiState.value = TrendingUiState(listOf(header) + tags, LoadingState.LOADED)
|
||||||
|
},
|
||||||
|
{ error ->
|
||||||
|
Log.w(TAG, "failed loading trending tags", error)
|
||||||
|
if (error is IOException) {
|
||||||
|
_uiState.value = TrendingUiState(emptyList(), LoadingState.ERROR_NETWORK)
|
||||||
|
} else {
|
||||||
|
_uiState.value = TrendingUiState(emptyList(), LoadingState.ERROR_OTHER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
||||||
|
|
@ -21,15 +21,13 @@ import java.util.Date
|
||||||
* Mastodon API Documentation: https://docs.joinmastodon.org/methods/trends/#tags
|
* Mastodon API Documentation: https://docs.joinmastodon.org/methods/trends/#tags
|
||||||
*
|
*
|
||||||
* @param name The name of the hashtag (after the #). The "caturday" in "#caturday".
|
* @param name The name of the hashtag (after the #). The "caturday" in "#caturday".
|
||||||
* @param url The URL to your mastodon instance list for this hashtag.
|
* (@param url The URL to your mastodon instance list for this hashtag.)
|
||||||
* @param history A list of [TrendingTagHistory]. Each element contains metrics per day for this hashtag.
|
* @param history A list of [TrendingTagHistory]. Each element contains metrics per day for this hashtag.
|
||||||
* @param following This is not listed in the APIs at the time of writing, but an instance is delivering it.
|
* (@param following This is not listed in the APIs at the time of writing, but an instance is delivering it.)
|
||||||
*/
|
*/
|
||||||
data class TrendingTag(
|
data class TrendingTag(
|
||||||
val name: String,
|
val name: String,
|
||||||
val url: String,
|
val history: List<TrendingTagHistory>
|
||||||
val history: List<TrendingTagHistory>,
|
|
||||||
val following: Boolean
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -782,5 +782,5 @@ interface MastodonApi {
|
||||||
suspend fun unfollowTag(@Path("name") name: String): NetworkResult<HashTag>
|
suspend fun unfollowTag(@Path("name") name: String): NetworkResult<HashTag>
|
||||||
|
|
||||||
@GET("api/v1/trends/tags")
|
@GET("api/v1/trends/tags")
|
||||||
suspend fun trendingTags(): Response<List<TrendingTag>>
|
suspend fun trendingTags(): NetworkResult<List<TrendingTag>>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,6 @@ import com.keylesspalace.tusky.viewdata.NotificationViewData
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
import com.keylesspalace.tusky.viewdata.TrendingViewData
|
import com.keylesspalace.tusky.viewdata.TrendingViewData
|
||||||
|
|
||||||
@JvmName("statusToViewData")
|
|
||||||
fun Status.toViewData(
|
fun Status.toViewData(
|
||||||
isShowingContent: Boolean,
|
isShowingContent: Boolean,
|
||||||
isExpanded: Boolean,
|
isExpanded: Boolean,
|
||||||
|
|
@ -56,7 +55,6 @@ fun Status.toViewData(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmName("notificationToViewData")
|
|
||||||
fun Notification.toViewData(
|
fun Notification.toViewData(
|
||||||
isShowingContent: Boolean,
|
isShowingContent: Boolean,
|
||||||
isExpanded: Boolean,
|
isExpanded: Boolean,
|
||||||
|
|
@ -71,9 +69,20 @@ fun Notification.toViewData(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmName("tagToViewData")
|
fun List<TrendingTag>.toViewData(): List<TrendingViewData.Tag> {
|
||||||
fun TrendingTag.toViewData(): TrendingViewData.Tag {
|
val maxTrendingValue = flatMap { tag -> tag.history }
|
||||||
return TrendingViewData.Tag(
|
.mapNotNull { it.uses.toLongOrNull() }
|
||||||
tag = this
|
.maxOrNull() ?: 1
|
||||||
)
|
|
||||||
|
return map { tag ->
|
||||||
|
|
||||||
|
val reversedHistory = tag.history.asReversed()
|
||||||
|
|
||||||
|
TrendingViewData.Tag(
|
||||||
|
name = tag.name,
|
||||||
|
usage = reversedHistory.mapNotNull { it.uses.toLongOrNull() },
|
||||||
|
accounts = reversedHistory.mapNotNull { it.accounts.toLongOrNull() },
|
||||||
|
maxTrendingValue = maxTrendingValue
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,9 @@ import android.graphics.Path
|
||||||
import android.graphics.PathMeasure
|
import android.graphics.PathMeasure
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.annotation.Dimension
|
import androidx.annotation.Dimension
|
||||||
import androidx.appcompat.widget.AppCompatImageView
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.content.res.use
|
import androidx.core.content.res.use
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
@ -33,9 +32,8 @@ import kotlin.math.max
|
||||||
class GraphView @JvmOverloads constructor(
|
class GraphView @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
defStyleAttr: Int = 0,
|
defStyleAttr: Int = 0
|
||||||
defStyleRes: Int = 0
|
) : View(context, attrs, defStyleAttr) {
|
||||||
) : AppCompatImageView(context, attrs, defStyleAttr) {
|
|
||||||
@get:ColorInt
|
@get:ColorInt
|
||||||
@ColorInt
|
@ColorInt
|
||||||
var primaryLineColor = 0
|
var primaryLineColor = 0
|
||||||
|
|
@ -55,7 +53,7 @@ class GraphView @JvmOverloads constructor(
|
||||||
@ColorInt
|
@ColorInt
|
||||||
var metaColor = 0
|
var metaColor = 0
|
||||||
|
|
||||||
var proportionalTrending = false
|
private var proportionalTrending = false
|
||||||
|
|
||||||
private lateinit var primaryLinePaint: Paint
|
private lateinit var primaryLinePaint: Paint
|
||||||
private lateinit var secondaryLinePaint: Paint
|
private lateinit var secondaryLinePaint: Paint
|
||||||
|
|
@ -129,16 +127,14 @@ class GraphView @JvmOverloads constructor(
|
||||||
|
|
||||||
private fun initFromXML(attr: AttributeSet?) {
|
private fun initFromXML(attr: AttributeSet?) {
|
||||||
context.obtainStyledAttributes(attr, R.styleable.GraphView).use { a ->
|
context.obtainStyledAttributes(attr, R.styleable.GraphView).use { a ->
|
||||||
primaryLineColor = ContextCompat.getColor(
|
primaryLineColor = context.getColor(
|
||||||
context,
|
|
||||||
a.getResourceId(
|
a.getResourceId(
|
||||||
R.styleable.GraphView_primaryLineColor,
|
R.styleable.GraphView_primaryLineColor,
|
||||||
R.color.tusky_blue
|
R.color.tusky_blue
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
secondaryLineColor = ContextCompat.getColor(
|
secondaryLineColor = context.getColor(
|
||||||
context,
|
|
||||||
a.getResourceId(
|
a.getResourceId(
|
||||||
R.styleable.GraphView_secondaryLineColor,
|
R.styleable.GraphView_secondaryLineColor,
|
||||||
R.color.tusky_red
|
R.color.tusky_red
|
||||||
|
|
@ -150,16 +146,14 @@ class GraphView @JvmOverloads constructor(
|
||||||
R.dimen.graph_line_thickness
|
R.dimen.graph_line_thickness
|
||||||
).toFloat()
|
).toFloat()
|
||||||
|
|
||||||
graphColor = ContextCompat.getColor(
|
graphColor = context.getColor(
|
||||||
context,
|
|
||||||
a.getResourceId(
|
a.getResourceId(
|
||||||
R.styleable.GraphView_graphColor,
|
R.styleable.GraphView_graphColor,
|
||||||
R.color.colorBackground
|
R.color.colorBackground
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
metaColor = ContextCompat.getColor(
|
metaColor = context.getColor(
|
||||||
context,
|
|
||||||
a.getResourceId(
|
a.getResourceId(
|
||||||
R.styleable.GraphView_metaColor,
|
R.styleable.GraphView_metaColor,
|
||||||
R.color.dividerColor
|
R.color.dividerColor
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,6 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.viewdata
|
package com.keylesspalace.tusky.viewdata
|
||||||
|
|
||||||
import com.keylesspalace.tusky.entity.TrendingTag
|
|
||||||
import com.keylesspalace.tusky.entity.end
|
|
||||||
import com.keylesspalace.tusky.entity.start
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
sealed class TrendingViewData {
|
sealed class TrendingViewData {
|
||||||
|
|
@ -31,18 +28,13 @@ sealed class TrendingViewData {
|
||||||
get() = start.toString() + end.toString()
|
get() = start.toString() + end.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun asHeaderOrNull(): Header? {
|
|
||||||
val tag = (this as? Tag)?.tag
|
|
||||||
?: return null
|
|
||||||
return Header(tag.start(), tag.end())
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Tag(
|
data class Tag(
|
||||||
val tag: TrendingTag
|
val name: String,
|
||||||
|
val usage: List<Long>,
|
||||||
|
val accounts: List<Long>,
|
||||||
|
val maxTrendingValue: Long
|
||||||
) : TrendingViewData() {
|
) : TrendingViewData() {
|
||||||
override val id: String
|
override val id: String
|
||||||
get() = tag.name
|
get() = name
|
||||||
}
|
}
|
||||||
|
|
||||||
fun asTagOrNull() = this as? Tag
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue