Refactor "trending hashtags" code (#3595)

- Fix codeformatting
- Add new refreshing state
- Disable LogConditional lint rule
- Update lint-baseline
This commit is contained in:
Konrad Pozniak 2023-06-10 19:47:07 +02:00 committed by GitHub
commit f23c0cc634
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 335 additions and 480 deletions

File diff suppressed because it is too large Load diff

View file

@ -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)
}
}

View file

@ -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)
} }
} }

View file

@ -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
} }
} }
} }

View file

@ -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

View file

@ -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()
}
} }
} }

View file

@ -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
)
}
}

View file

@ -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 {

View file

@ -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
) )
/** /**

View file

@ -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>>
} }

View file

@ -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
)
}
} }

View file

@ -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

View file

@ -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
} }