Add "Trending posts" (statuses) feed (#4007)

Add "Trending posts" (statuses) feed.

This feed is a good source of interesting accounts to follow and,
personally, a sort of "Front page of the Fediverse".

Since #3908 and #3910 (which would provide a more thorough, albeit
complex, access to trending things) won't get merged, I'd like to
address this missing feed by simply adding another tab/feed.

~~If desired, I can move the second commit (fixing lint) to another
PR.~~

## Screenshots
### Tab
<img
src="https://github.com/tuskyapp/Tusky/assets/1063155/6a71a97e-673e-44c7-b67d-9b1df0bed4f5"
width=320 /> <img
src="https://github.com/tuskyapp/Tusky/assets/1063155/9bf60b23-d2f3-4dd8-8af6-e96647b02121"
width=320 />

### Activity
<img
src="https://github.com/tuskyapp/Tusky/assets/1063155/4e07dea3-d97f-42c6-8551-492a3116fcfa"
width=320 /> <img
src="https://github.com/tuskyapp/Tusky/assets/1063155/ad00a134-d622-43f4-8305-84cfa7fed706"
width=320 />
This commit is contained in:
Angelo Suzuki 2023-09-14 22:37:41 +02:00 committed by GitHub
commit fa80a0123a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 148 additions and 11 deletions

View file

@ -292,7 +292,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
setupDrawer(
savedInstanceState,
addSearchButton = hideTopToolbar,
addTrendingTagsButton = !accountManager.activeAccount!!.tabPreferences.hasTab(TRENDING_TAGS)
addTrendingTagsButton = !accountManager.activeAccount!!.tabPreferences.hasTab(TRENDING_TAGS),
addTrendingStatusesButton = !accountManager.activeAccount!!.tabPreferences.hasTab(TRENDING_STATUSES),
)
/* Fetch user info while we're doing other things. This has to be done after setting up the
@ -317,7 +318,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
is MainTabsChangedEvent -> {
refreshMainDrawerItems(
addSearchButton = hideTopToolbar,
addTrendingTagsButton = !event.newTabs.hasTab(TRENDING_TAGS)
addTrendingTagsButton = !event.newTabs.hasTab(TRENDING_TAGS),
addTrendingStatusesButton = !event.newTabs.hasTab(TRENDING_STATUSES),
)
setupTabs(false)
@ -482,7 +484,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
private fun setupDrawer(
savedInstanceState: Bundle?,
addSearchButton: Boolean,
addTrendingTagsButton: Boolean
addTrendingTagsButton: Boolean,
addTrendingStatusesButton: Boolean,
) {
val drawerOpenClickListener = View.OnClickListener { binding.mainDrawerLayout.open() }
@ -543,12 +546,20 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
})
binding.mainDrawer.apply {
refreshMainDrawerItems(addSearchButton, addTrendingTagsButton)
refreshMainDrawerItems(
addSearchButton = addSearchButton,
addTrendingTagsButton = addTrendingTagsButton,
addTrendingStatusesButton = addTrendingStatusesButton,
)
setSavedInstance(savedInstanceState)
}
}
private fun refreshMainDrawerItems(addSearchButton: Boolean, addTrendingTagsButton: Boolean) {
private fun refreshMainDrawerItems(
addSearchButton: Boolean,
addTrendingTagsButton: Boolean,
addTrendingStatusesButton: Boolean,
) {
binding.mainDrawer.apply {
itemAdapter.clear()
tintStatusBar = true
@ -677,6 +688,19 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
)
}
if (addTrendingStatusesButton) {
binding.mainDrawer.addItemsAtPosition(
6,
primaryDrawerItem {
nameRes = R.string.title_public_trending_statuses
iconicsIcon = GoogleMaterial.Icon.gmd_local_fire_department
onClick = {
startActivityWithSlideInAnimation(StatusListActivity.newTrendingIntent(context))
}
}
)
}
}
if (BuildConfig.DEBUG) {

View file

@ -76,6 +76,7 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
Kind.FAVOURITES -> getString(R.string.title_favourites)
Kind.BOOKMARKS -> getString(R.string.title_bookmarks)
Kind.TAG -> getString(R.string.title_tag).format(hashtag)
Kind.PUBLIC_TRENDING_STATUSES -> getString(R.string.title_public_trending_statuses)
else -> intent.getStringExtra(EXTRA_LIST_TITLE)
}
@ -383,5 +384,10 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
putExtra(EXTRA_KIND, Kind.TAG.name)
putExtra(EXTRA_HASHTAG, hashtag)
}
fun newTrendingIntent(context: Context) =
Intent(context, StatusListActivity::class.java).apply {
putExtra(EXTRA_KIND, Kind.PUBLIC_TRENDING_STATUSES.name)
}
}
}

View file

@ -34,6 +34,7 @@ const val LOCAL = "Local"
const val FEDERATED = "Federated"
const val DIRECT = "Direct"
const val TRENDING_TAGS = "TrendingTags"
const val TRENDING_STATUSES = "TrendingStatuses"
const val HASHTAG = "Hashtag"
const val LIST = "List"
const val BOOKMARKS = "Bookmarks"
@ -99,6 +100,12 @@ fun createTabDataFromId(id: String, arguments: List<String> = emptyList()): TabD
icon = R.drawable.ic_trending_up_24px,
fragment = { TrendingTagsFragment.newInstance() }
)
TRENDING_STATUSES -> TabData(
id = TRENDING_STATUSES,
text = R.string.title_public_trending_statuses,
icon = R.drawable.ic_hot_24dp,
fragment = { TimelineFragment.newInstance(TimelineViewModel.Kind.PUBLIC_TRENDING_STATUSES) }
)
HASHTAG -> TabData(
id = HASHTAG,
text = R.string.hashtags,

View file

@ -386,6 +386,10 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
if (!currentTabs.contains(trendingTagsTab)) {
addableTabs.add(bookmarksTab)
}
val trendingStatusesTab = createTabDataFromId(TRENDING_STATUSES)
if (!currentTabs.contains(trendingStatusesTab)) {
addableTabs.add(trendingStatusesTab)
}
addableTabs.add(createTabDataFromId(HASHTAG))
addableTabs.add(createTabDataFromId(LIST))

View file

@ -540,7 +540,8 @@ class TimelineFragment :
when (kind) {
TimelineViewModel.Kind.HOME,
TimelineViewModel.Kind.PUBLIC_FEDERATED,
TimelineViewModel.Kind.PUBLIC_LOCAL -> adapter.refresh()
TimelineViewModel.Kind.PUBLIC_LOCAL,
TimelineViewModel.Kind.PUBLIC_TRENDING_STATUSES -> adapter.refresh()
TimelineViewModel.Kind.USER,
TimelineViewModel.Kind.USER_WITH_REPLIES -> if (status.account.id == viewModel.id) {
adapter.refresh()

View file

@ -33,6 +33,14 @@ class NetworkTimelineRemoteMediator(
private val viewModel: NetworkTimelineViewModel
) : RemoteMediator<String, StatusViewData>() {
private val statusIds = mutableSetOf<String>()
init {
if (viewModel.kind == TimelineViewModel.Kind.PUBLIC_TRENDING_STATUSES) {
statusIds.addAll(viewModel.statusData.map { it.id })
}
}
override suspend fun load(
loadType: LoadType,
state: PagingState<String, StatusViewData>
@ -88,6 +96,10 @@ class NetworkTimelineRemoteMediator(
false
}
if (viewModel.kind == TimelineViewModel.Kind.PUBLIC_TRENDING_STATUSES) {
statusIds.addAll(data.map { it.id })
}
viewModel.statusData.addAll(0, data)
if (insertPlaceholder) {
@ -96,11 +108,22 @@ class NetworkTimelineRemoteMediator(
} else {
val linkHeader = statusResponse.headers()["Link"]
val links = HttpHeaderLink.parse(linkHeader)
val nextId = HttpHeaderLink.findByRelationType(links, "next")?.uri?.getQueryParameter("max_id")
val next = HttpHeaderLink.findByRelationType(links, "next")
viewModel.nextKey = nextId
var filteredData = data
if (viewModel.kind == TimelineViewModel.Kind.PUBLIC_TRENDING_STATUSES) {
// Trending statuses use offset for paging, not IDs. If a new status has been added to the remote
// feed after we performed the initial fetch, then the feed will have moved, but our offset won't.
// As a result, we'd get repeat statuses. This addresses that.
filteredData = data.filter { !statusIds.contains(it.id) }
statusIds.addAll(filteredData.map { it.id })
viewModel.statusData.addAll(data)
viewModel.nextKey = next?.uri?.getQueryParameter("offset")
} else {
viewModel.nextKey = next?.uri?.getQueryParameter("max_id")
}
viewModel.statusData.addAll(filteredData)
}
viewModel.currentSource?.invalidate()

View file

@ -308,6 +308,7 @@ class NetworkTimelineViewModel @Inject constructor(
Kind.FAVOURITES -> api.favourites(fromId, uptoId, limit)
Kind.BOOKMARKS -> api.bookmarks(fromId, uptoId, limit)
Kind.LIST -> api.listTimeline(id!!, fromId, uptoId, limit)
Kind.PUBLIC_TRENDING_STATUSES -> api.trendingStatuses(limit = limit, offset = fromId)
}
}

View file

@ -321,12 +321,12 @@ abstract class TimelineViewModel(
}
enum class Kind {
HOME, PUBLIC_LOCAL, PUBLIC_FEDERATED, TAG, USER, USER_PINNED, USER_WITH_REPLIES, FAVOURITES, LIST, BOOKMARKS;
HOME, PUBLIC_LOCAL, PUBLIC_FEDERATED, TAG, USER, USER_PINNED, USER_WITH_REPLIES, FAVOURITES, LIST, BOOKMARKS, PUBLIC_TRENDING_STATUSES;
fun toFilterKind(): Filter.Kind {
return when (valueOf(name)) {
HOME, LIST -> Filter.Kind.HOME
PUBLIC_FEDERATED, PUBLIC_LOCAL, TAG, FAVOURITES -> Filter.Kind.PUBLIC
PUBLIC_FEDERATED, PUBLIC_LOCAL, TAG, FAVOURITES, PUBLIC_TRENDING_STATUSES -> Filter.Kind.PUBLIC
USER, USER_WITH_REPLIES, USER_PINNED -> Filter.Kind.ACCOUNT
else -> Filter.Kind.PUBLIC
}

View file

@ -848,4 +848,10 @@ interface MastodonApi {
@GET("api/v1/trends/tags")
suspend fun trendingTags(): NetworkResult<List<TrendingTag>>
@GET("api/v1/trends/statuses")
suspend fun trendingStatuses(
@Query("limit") limit: Int? = null,
@Query("offset") offset: String? = null
): Response<List<Status>>
}