Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add trending tags #3149

Merged
merged 41 commits into from
Feb 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
b12a3ec
Add initial feature for viewing trending graphs. Currently only views…
DavidEdwards Jan 7, 2023
39e4ed6
Add clickable system through a LinkListener. Duplicates a little code…
DavidEdwards Jan 7, 2023
0642b03
Add accessibility description.
DavidEdwards Jan 7, 2023
9b6cba8
The background for the graph should match the background for black th…
DavidEdwards Jan 8, 2023
fa03b3d
Add error handling through a state flow system using existing code as…
DavidEdwards Jan 8, 2023
496c4e3
Graphing: Use a primary and a secondary line. Remove under line fill.…
DavidEdwards Jan 11, 2023
3deb9e3
Trending changes: New layout for trending: Cell. Use ViewBinding. Add…
DavidEdwards Jan 13, 2023
d3ec075
Trending changes: Remove old layout. Update cell textsizes and use pr…
DavidEdwards Jan 13, 2023
3def33c
Trending changes: Refresh the main drawer when the tabs are edited. T…
DavidEdwards Jan 14, 2023
c5e1b68
Trending changes: Add a trending activity to be able to view the tren…
DavidEdwards Jan 14, 2023
0c9f1d1
Trending changes: The title text should be changed to Trending Hashtags.
DavidEdwards Jan 14, 2023
4295273
Trending changes: Add meta color to draw axis etc. Draw the date boun…
DavidEdwards Jan 14, 2023
13266e2
Trending changes: Refresh FAB through the main activity and FabFragme…
DavidEdwards Jan 14, 2023
0513eb0
Trending changes: Make graph proportional to the highest usage value.…
DavidEdwards Jan 14, 2023
ede6645
Trending changes: KtLintFix
DavidEdwards Jan 14, 2023
33395e7
Trending changes: Remove accidental build gradle change. Remove trend…
DavidEdwards Jan 18, 2023
1635e8a
Trending changes: Use bottomsheet slide in animation helper for openi…
DavidEdwards Jan 19, 2023
fcd62f0
Use some platform standards for styling
nikclayton Jan 26, 2023
e313038
Correct lineWidth calculations
nikclayton Jan 26, 2023
565ce79
Does not need to inherit from FabFragment
nikclayton Jan 26, 2023
9e0e76c
Rename to TrendingAdapter
nikclayton Jan 26, 2023
4b7e55d
Clean up comments, use full class name as tag
nikclayton Jan 26, 2023
935a80d
Simplify TrendingViewModel
nikclayton Jan 26, 2023
66cf81a
Remove line dividers, use X-axis to separate content
nikclayton Jan 26, 2023
48d93e8
Adjust date format
nikclayton Jan 26, 2023
51007b5
Locale-aware format of numbers
nikclayton Jan 26, 2023
ed3b708
Prevent a crash if viewData is empty
nikclayton Jan 27, 2023
97295c7
Filter out tags the user has filtered from their home timeline
nikclayton Jan 27, 2023
859012c
Experiment with alternative layout
nikclayton Jan 27, 2023
745b8c1
Set chart height to 160dp to align to an 8dp grid
nikclayton Jan 27, 2023
20383a5
Draw ticks that are 5% the height of the x-axis
nikclayton Jan 31, 2023
0f09be1
Legend adjustments
nikclayton Jan 31, 2023
5667cbb
Bezier curves, shorter cell height
nikclayton Feb 1, 2023
b4c3476
More tweaks
nikclayton Feb 1, 2023
d3bc645
Hide FAB
nikclayton Feb 1, 2023
ee48f9b
Fix crash, it's not always hosted in an ActionButtonActivity
nikclayton Feb 2, 2023
1561437
Arrange totals vertically in landscape layout
nikclayton Feb 2, 2023
d2dfe8e
Always add the Trending drawer menu if it's not a tab
nikclayton Feb 2, 2023
21a7d68
Merge branch 'develop' into add-trending-tags
nikclayton Feb 12, 2023
dbf9df1
Revert unrelated whitespace changes
nikclayton Feb 12, 2023
a98a87b
One more whitespace revert
nikclayton Feb 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ android {
shrinkResources true
proguardFiles 'proguard-rules.pro'
}
debug {}
}

flavorDimensions += "color"
Expand Down
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@
<activity android:name=".ListsActivity" />
<activity android:name=".LicenseActivity" />
<activity android:name=".FiltersActivity" />
<activity android:name=".components.trending.TrendingActivity" />
<activity android:name=".components.followedtags.FollowedTagsActivity" />
<activity
android:name=".components.report.ReportActivity"
Expand Down
86 changes: 71 additions & 15 deletions app/src/main/java/com/keylesspalace/tusky/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,15 @@ import com.keylesspalace.tusky.components.notifications.showMigrationNoticeIfNec
import com.keylesspalace.tusky.components.preference.PreferencesActivity
import com.keylesspalace.tusky.components.scheduled.ScheduledStatusActivity
import com.keylesspalace.tusky.components.search.SearchActivity
import com.keylesspalace.tusky.components.trending.TrendingActivity
import com.keylesspalace.tusky.databinding.ActivityMainBinding
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.DraftsAlert
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Notification
import com.keylesspalace.tusky.interfaces.AccountSelectionListener
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
import com.keylesspalace.tusky.interfaces.FabFragment
import com.keylesspalace.tusky.interfaces.ReselectableFragment
import com.keylesspalace.tusky.pager.MainPagerAdapter
import com.keylesspalace.tusky.settings.PrefKeys
Expand Down Expand Up @@ -261,7 +263,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje

binding.viewPager.reduceSwipeSensitivity()

setupDrawer(savedInstanceState, addSearchButton = hideTopToolbar)
setupDrawer(
savedInstanceState,
addSearchButton = hideTopToolbar,
addTrendingButton = !accountManager.activeAccount!!.tabPreferences.hasTab(TRENDING)
)

/* Fetch user info while we're doing other things. This has to be done after setting up the
* drawer, though, because its callback touches the header in the drawer. */
Expand All @@ -277,7 +283,15 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
.subscribe { event: Event? ->
when (event) {
is ProfileEditedEvent -> onFetchUserInfoSuccess(event.newProfileData)
is MainTabsChangedEvent -> setupTabs(false)
is MainTabsChangedEvent -> {
refreshMainDrawerItems(
addSearchButton = hideTopToolbar,
addTrendingButton = !event.newTabs.hasTab(TRENDING),
)

setupTabs(false)
}

is AnnouncementReadEvent -> {
unreadAnnouncementsCount--
updateAnnouncementsBadge()
Expand Down Expand Up @@ -397,7 +411,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
finish()
}

private fun setupDrawer(savedInstanceState: Bundle?, addSearchButton: Boolean) {
private fun setupDrawer(
savedInstanceState: Bundle?,
addSearchButton: Boolean,
addTrendingButton: Boolean
) {

val drawerOpenClickListener = View.OnClickListener { binding.mainDrawerLayout.open() }

Expand Down Expand Up @@ -455,6 +473,14 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
})

binding.mainDrawer.apply {
refreshMainDrawerItems(addSearchButton, addTrendingButton)
setSavedInstance(savedInstanceState)
}
}

private fun refreshMainDrawerItems(addSearchButton: Boolean, addTrendingButton: Boolean) {
binding.mainDrawer.apply {
itemAdapter.clear()
tintStatusBar = true
addItems(
primaryDrawerItem {
Expand Down Expand Up @@ -521,7 +547,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
badgeStyle = BadgeStyle().apply {
textColor = ColorHolder.fromColor(MaterialColors.getColor(binding.mainDrawer, com.google.android.material.R.attr.colorOnPrimary))
color = ColorHolder.fromColor(MaterialColors.getColor(binding.mainDrawer, androidx.appcompat.R.attr.colorPrimary))
color = ColorHolder.fromColor(MaterialColors.getColor(binding.mainDrawer, com.google.android.material.R.attr.colorPrimary))
}
},
DividerDrawerItem(),
Expand Down Expand Up @@ -569,7 +595,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
)
}

setSavedInstance(savedInstanceState)
if (addTrendingButton) {
binding.mainDrawer.addItemsAtPosition(
5,
primaryDrawerItem {
nameRes = R.string.title_public_trending_hashtags
iconicsIcon = GoogleMaterial.Icon.gmd_trending_up
onClick = {
startActivityWithSlideInAnimation(TrendingActivity.getIntent(context))
}
}
)
}
}

if (BuildConfig.DEBUG) {
Expand Down Expand Up @@ -672,6 +709,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}

binding.mainToolbar.title = tabs[tab.position].title(this@MainActivity)

refreshComposeButtonState(adapter, tab.position)
}

override fun onTabUnselected(tab: TabLayout.Tab) {}
Expand All @@ -681,6 +720,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
if (fragment is ReselectableFragment) {
(fragment as ReselectableFragment).onReselect()
}

refreshComposeButtonState(adapter, tab.position)
}
}.also {
activeTabLayout.addOnTabSelectedListener(it)
Expand All @@ -695,6 +736,20 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
updateProfiles()
}

private fun refreshComposeButtonState(adapter: MainPagerAdapter, tabPosition: Int) {
DavidEdwards marked this conversation as resolved.
Show resolved Hide resolved
adapter.getFragment(tabPosition)?.also { fragment ->
if (fragment is FabFragment) {
if (fragment.isFabVisible()) {
binding.composeButton.show()
} else {
binding.composeButton.hide()
}
} else {
binding.composeButton.show()
}
}
}

private fun handleProfileClick(profile: IProfile, current: Boolean): Boolean {
val activeAccount = accountManager.activeAccount

Expand Down Expand Up @@ -930,16 +985,17 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje

private fun updateProfiles() {
val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
val profiles: MutableList<IProfile> = accountManager.getAllAccountsOrderedByActive().map { acc ->
ProfileDrawerItem().apply {
isSelected = acc.isActive
nameText = acc.displayName.emojify(acc.emojis, header, animateEmojis)
iconUrl = acc.profilePictureUrl
isNameShown = true
identifier = acc.id
descriptionText = acc.fullName
}
}.toMutableList()
val profiles: MutableList<IProfile> =
accountManager.getAllAccountsOrderedByActive().map { acc ->
ProfileDrawerItem().apply {
isSelected = acc.isActive
nameText = acc.displayName.emojify(acc.emojis, header, animateEmojis)
iconUrl = acc.profilePictureUrl
isNameShown = true
identifier = acc.id
descriptionText = acc.fullName
}
}.toMutableList()

// reuse the already existing "add account" item
for (profile in header.profiles.orEmpty()) {
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/com/keylesspalace/tusky/TabData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import androidx.fragment.app.Fragment
import com.keylesspalace.tusky.components.conversation.ConversationsFragment
import com.keylesspalace.tusky.components.timeline.TimelineFragment
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
import com.keylesspalace.tusky.components.trending.TrendingFragment
import com.keylesspalace.tusky.fragment.NotificationsFragment

/** this would be a good case for a sealed class, but that does not work nice with Room */
Expand All @@ -31,6 +32,7 @@ const val NOTIFICATIONS = "Notifications"
const val LOCAL = "Local"
const val FEDERATED = "Federated"
const val DIRECT = "Direct"
const val TRENDING = "Trending"
const val HASHTAG = "Hashtag"
const val LIST = "List"

Expand All @@ -43,6 +45,8 @@ data class TabData(
val title: (Context) -> String = { context -> context.getString(text) }
)

fun List<TabData>.hasTab(id: String): Boolean = this.find { it.id == id } != null

fun createTabDataFromId(id: String, arguments: List<String> = emptyList()): TabData {
return when (id) {
HOME -> TabData(
Expand Down Expand Up @@ -75,6 +79,12 @@ fun createTabDataFromId(id: String, arguments: List<String> = emptyList()): TabD
R.drawable.ic_reblog_direct_24dp,
{ ConversationsFragment.newInstance() }
)
TRENDING -> TabData(
TRENDING,
R.string.title_public_trending_hashtags,
R.drawable.ic_trending_up_24px,
{ TrendingFragment.newInstance() }
)
HASHTAG -> TabData(
HASHTAG,
R.string.hashtags,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,10 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
if (!currentTabs.contains(directMessagesTab)) {
addableTabs.add(directMessagesTab)
}
val trendingTab = createTabDataFromId(TRENDING)
if (!currentTabs.contains(trendingTab)) {
addableTabs.add(trendingTab)
}

addableTabs.add(createTabDataFromId(HASHTAG))
addableTabs.add(createTabDataFromId(LIST))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/* 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.ItemTrendingDateBinding
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.TimeZone

class TrendingDateViewHolder(
private val binding: ItemTrendingDateBinding,
) : RecyclerView.ViewHolder(binding.root) {

private val dateFormat = SimpleDateFormat("EEE dd MMM yyyy", Locale.getDefault()).apply {
this.timeZone = TimeZone.getDefault()
}

fun setup(start: Date, end: Date) {
binding.dates.text = itemView.context.getString(
R.string.date_range,
dateFormat.format(start),
dateFormat.format(end)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/* 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.viewdata.TrendingViewData
import java.text.NumberFormat
import kotlin.math.ln
import kotlin.math.pow

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

companion object {
private val numberFormatter: NumberFormat = NumberFormat.getInstance()
private val ln_1k = ln(1000.0)

/**
* Format numbers according to the current locale. Numbers < min have
* separators (',', '.', etc) inserted according to the locale.
*
* Numbers > min are scaled down to that by multiples of 1,000, and
* a suffix appropriate to the scaling is appended.
*/
private fun formatNumber(num: Long, min: Int = 100000): String {
if (num < min) return numberFormatter.format(num)

val exp = (ln(num.toDouble()) / ln_1k).toInt()

// TODO: is the choice of suffixes here locale-agnostic?
return String.format("%.1f %c", num / 1000.0.pow(exp.toDouble()), "KMGTPE"[exp - 1])
}
}
}
Loading