Show toot stat inline (#3413)

* Show toot stat inline

* Correct elements position

* Format stats and show it according to setting

* inline toot statistics setting

* Code formatting

* Use kotlin functions

* Change the statistics setting description

* Use capital letters for all variants

* increase the statistics margin

* Merge fixes

* Code review fixes

* move setReblogsCount and setFavouritedCount to StatusViewHolder

* code cleaning

* code cleaning

* import lexicographical order

---------

Co-authored-by: Grigorii Ioffe <zikasaks@gmail.com>
Co-authored-by: grigoriiioffe <zikasaks@icloud.com>
This commit is contained in:
Grigorii Ioffe 2023-03-18 09:57:26 +02:00 committed by GitHub
parent 9087d0ecdd
commit 75e7b9f1a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 146 additions and 4 deletions

View file

@ -54,6 +54,7 @@ import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.CustomEmojiHelper; import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.ImageLoadingHelper; import com.keylesspalace.tusky.util.ImageLoadingHelper;
import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.NumberUtils;
import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.util.TimestampUtils; import com.keylesspalace.tusky.util.TimestampUtils;
import com.keylesspalace.tusky.util.TouchDelegateHelper; import com.keylesspalace.tusky.util.TouchDelegateHelper;
@ -388,10 +389,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
} }
private void setReplyCount(int repliesCount) { protected void setReplyCount(int repliesCount) {
// This label only exists in the non-detailed view (to match the web ui) // This label only exists in the non-detailed view (to match the web ui)
if (replyCountLabel != null) { if (replyCountLabel != null) {
replyCountLabel.setText((repliesCount > 1 ? replyCountLabel.getContext().getString(R.string.status_count_one_plus) : Integer.toString(repliesCount))); replyCountLabel.setText(NumberUtils.shortNumber(repliesCount));
} }
} }
@ -626,12 +627,18 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
avatar.setOnClickListener(profileButtonClickListener); avatar.setOnClickListener(profileButtonClickListener);
displayName.setOnClickListener(profileButtonClickListener); displayName.setOnClickListener(profileButtonClickListener);
if (replyCountLabel != null) {
replyCountLabel.setVisibility(statusDisplayOptions.showStatsInline() ? View.VISIBLE : View.INVISIBLE);
}
replyButton.setOnClickListener(v -> { replyButton.setOnClickListener(v -> {
int position = getBindingAdapterPosition(); int position = getBindingAdapterPosition();
if (position != RecyclerView.NO_POSITION) { if (position != RecyclerView.NO_POSITION) {
listener.onReply(position); listener.onReply(position);
} }
}); });
if (reblogButton != null) { if (reblogButton != null) {
reblogButton.setEventListener((button, buttonState) -> { reblogButton.setEventListener((button, buttonState) -> {
// return true to play animation // return true to play animation
@ -650,6 +657,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}); });
} }
favouriteButton.setEventListener((button, buttonState) -> { favouriteButton.setEventListener((button, buttonState) -> {
// return true to play animation // return true to play animation
int position = getBindingAdapterPosition(); int position = getBindingAdapterPosition();

View file

@ -32,6 +32,7 @@ import com.keylesspalace.tusky.entity.Filter;
import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.CustomEmojiHelper; import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.NumberUtils;
import com.keylesspalace.tusky.util.SmartLengthInputFilter; import com.keylesspalace.tusky.util.SmartLengthInputFilter;
import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.util.StringUtils; import com.keylesspalace.tusky.util.StringUtils;
@ -47,11 +48,15 @@ public class StatusViewHolder extends StatusBaseViewHolder {
private final TextView statusInfo; private final TextView statusInfo;
private final Button contentCollapseButton; private final Button contentCollapseButton;
private final TextView favouritedCountLabel;
private final TextView reblogsCountLabel;
public StatusViewHolder(View itemView) { public StatusViewHolder(View itemView) {
super(itemView); super(itemView);
statusInfo = itemView.findViewById(R.id.status_info); statusInfo = itemView.findViewById(R.id.status_info);
contentCollapseButton = itemView.findViewById(R.id.button_toggle_content); contentCollapseButton = itemView.findViewById(R.id.button_toggle_content);
favouritedCountLabel = itemView.findViewById(R.id.status_favourites_count);
reblogsCountLabel = itemView.findViewById(R.id.status_insets);
} }
@Override @Override
@ -77,6 +82,12 @@ public class StatusViewHolder extends StatusBaseViewHolder {
} }
} }
reblogsCountLabel.setVisibility(statusDisplayOptions.showStatsInline() ? View.VISIBLE : View.INVISIBLE);
favouritedCountLabel.setVisibility(statusDisplayOptions.showStatsInline() ? View.VISIBLE : View.INVISIBLE);
setFavouritedCount(status.getActionable().getFavouritesCount());
setReblogsCount(status.getActionable().getReblogsCount());
super.setupWithStatus(status, listener, statusDisplayOptions, payloads); super.setupWithStatus(status, listener, statusDisplayOptions, payloads);
} }
@ -102,6 +113,14 @@ public class StatusViewHolder extends StatusBaseViewHolder {
statusInfo.setVisibility(View.VISIBLE); statusInfo.setVisibility(View.VISIBLE);
} }
protected void setReblogsCount(int reblogsCount) {
reblogsCountLabel.setText(NumberUtils.shortNumber(reblogsCount));
}
protected void setFavouritedCount(int favouritedCount) {
favouritedCountLabel.setText(NumberUtils.shortNumber(favouritedCount));
}
protected void hideStatusInfo() { protected void hideStatusInfo() {
statusInfo.setVisibility(View.GONE); statusInfo.setVisibility(View.GONE);
} }

View file

@ -112,6 +112,7 @@ class ConversationsFragment :
confirmFavourites = preferences.getBoolean("confirmFavourites", false), confirmFavourites = preferences.getBoolean("confirmFavourites", false),
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false), animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false),
showStatsInline = preferences.getBoolean(PrefKeys.SHOW_STATS_INLINE, false),
showSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia, showSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia,
openSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler openSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler
) )

View file

@ -151,7 +151,7 @@ class PreferencesActivity :
} }
"statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars", "useBlurhash", "statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars", "useBlurhash",
"showSelfUsername", "showCardsInTimelines", "confirmReblogs", "confirmFavourites", "showSelfUsername", "showCardsInTimelines", "confirmReblogs", "confirmFavourites",
"enableSwipeForTabs", "mainNavPosition", PrefKeys.HIDE_TOP_TOOLBAR -> { "enableSwipeForTabs", "mainNavPosition", PrefKeys.HIDE_TOP_TOOLBAR, PrefKeys.SHOW_STATS_INLINE -> {
restartActivitiesOnBackPressedCallback.isEnabled = true restartActivitiesOnBackPressedCallback.isEnabled = true
} }
} }

View file

@ -221,6 +221,13 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
setTitle(R.string.pref_title_enable_swipe_for_tabs) setTitle(R.string.pref_title_enable_swipe_for_tabs)
isSingleLineTitle = false isSingleLineTitle = false
} }
switchPreference {
setDefaultValue(false)
key = PrefKeys.SHOW_STATS_INLINE
setTitle(R.string.pref_title_show_stat_inline)
isSingleLineTitle = false
}
} }
preferenceCategory(R.string.pref_title_browser_settings) { preferenceCategory(R.string.pref_title_browser_settings) {

View file

@ -157,6 +157,7 @@ class ReportStatusesFragment :
confirmFavourites = preferences.getBoolean("confirmFavourites", false), confirmFavourites = preferences.getBoolean("confirmFavourites", false),
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false), animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false),
showStatsInline = preferences.getBoolean(PrefKeys.SHOW_STATS_INLINE, false),
showSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia, showSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia,
openSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler openSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler
) )

View file

@ -88,6 +88,7 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
confirmFavourites = preferences.getBoolean("confirmFavourites", false), confirmFavourites = preferences.getBoolean("confirmFavourites", false),
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false), animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false),
showStatsInline = preferences.getBoolean(PrefKeys.SHOW_STATS_INLINE, false),
showSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia, showSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia,
openSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler openSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler
) )

View file

@ -197,6 +197,7 @@ class TimelineFragment :
confirmFavourites = preferences.getBoolean(PrefKeys.CONFIRM_FAVOURITES, false), confirmFavourites = preferences.getBoolean(PrefKeys.CONFIRM_FAVOURITES, false),
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false), animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false),
showStatsInline = preferences.getBoolean(PrefKeys.SHOW_STATS_INLINE, false),
showSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia, showSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia,
openSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler openSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler
) )

View file

@ -113,6 +113,7 @@ class ViewThreadFragment :
confirmFavourites = preferences.getBoolean("confirmFavourites", false), confirmFavourites = preferences.getBoolean("confirmFavourites", false),
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false), animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false),
showStatsInline = preferences.getBoolean(PrefKeys.SHOW_STATS_INLINE, false),
showSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia, showSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia,
openSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler openSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler
) )

View file

@ -67,6 +67,7 @@ object PrefKeys {
const val CONFIRM_FAVOURITES = "confirmFavourites" const val CONFIRM_FAVOURITES = "confirmFavourites"
const val ENABLE_SWIPE_FOR_TABS = "enableSwipeForTabs" const val ENABLE_SWIPE_FOR_TABS = "enableSwipeForTabs"
const val ANIMATE_CUSTOM_EMOJIS = "animateCustomEmojis" const val ANIMATE_CUSTOM_EMOJIS = "animateCustomEmojis"
const val SHOW_STATS_INLINE = "showStatsInline"
const val CUSTOM_TABS = "customTabs" const val CUSTOM_TABS = "customTabs"
const val WELLBEING_LIMITED_NOTIFICATIONS = "wellbeingModeLimitedNotifications" const val WELLBEING_LIMITED_NOTIFICATIONS = "wellbeingModeLimitedNotifications"

View file

@ -0,0 +1,26 @@
@file:JvmName("NumberUtils")
package com.keylesspalace.tusky.util
import java.text.DecimalFormat
import kotlin.math.abs
import kotlin.math.floor
import kotlin.math.log10
import kotlin.math.pow
import kotlin.math.sign
val shortLetters = arrayOf(' ', 'K', 'M', 'B', 'T', 'P', 'E')
fun shortNumber(number: Number): String {
val numberAsDouble = number.toDouble()
val nonNegativeValue = abs(numberAsDouble)
var sign = ""
if (numberAsDouble.sign < 0) { sign = "-" }
val value = floor(log10(nonNegativeValue)).toInt()
val base = value / 3
if (value >= 3 && base < shortLetters.size) {
return DecimalFormat("$sign#0.0").format(nonNegativeValue / 10.0.pow((base * 3).toDouble())) + shortLetters[base]
} else {
return DecimalFormat("$sign#,##0").format(nonNegativeValue)
}
}

View file

@ -42,6 +42,8 @@ data class StatusDisplayOptions(
val hideStats: Boolean, val hideStats: Boolean,
@get:JvmName("animateEmojis") @get:JvmName("animateEmojis")
val animateEmojis: Boolean, val animateEmojis: Boolean,
@get:JvmName("showStatsInline")
val showStatsInline: Boolean,
@get:JvmName("showSensitiveMedia") @get:JvmName("showSensitiveMedia")
val showSensitiveMedia: Boolean, val showSensitiveMedia: Boolean,
@get:JvmName("openSpoiler") @get:JvmName("openSpoiler")
@ -119,6 +121,7 @@ data class StatusDisplayOptions(
confirmReblogs = preferences.getBoolean(PrefKeys.CONFIRM_REBLOGS, true), confirmReblogs = preferences.getBoolean(PrefKeys.CONFIRM_REBLOGS, true),
confirmFavourites = preferences.getBoolean(PrefKeys.CONFIRM_FAVOURITES, false), confirmFavourites = preferences.getBoolean(PrefKeys.CONFIRM_FAVOURITES, false),
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
showStatsInline = preferences.getBoolean(PrefKeys.SHOW_STATS_INLINE, false),
showSensitiveMedia = account.alwaysShowSensitiveMedia, showSensitiveMedia = account.alwaysShowSensitiveMedia,
openSpoiler = account.alwaysOpenSpoiler openSpoiler = account.alwaysOpenSpoiler
) )

View file

@ -329,7 +329,7 @@
android:id="@+id/status_replies" android:id="@+id/status_replies"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="40dp" android:layout_marginStart="45dp"
android:textSize="?attr/status_text_medium" android:textSize="?attr/status_text_medium"
app:layout_constraintBottom_toBottomOf="@id/status_reply" app:layout_constraintBottom_toBottomOf="@id/status_reply"
app:layout_constraintStart_toStartOf="@id/status_reply" app:layout_constraintStart_toStartOf="@id/status_reply"
@ -353,6 +353,17 @@
sparkbutton:primaryColor="@color/tusky_blue" sparkbutton:primaryColor="@color/tusky_blue"
sparkbutton:secondaryColor="@color/tusky_blue_light" /> sparkbutton:secondaryColor="@color/tusky_blue_light" />
<TextView
android:id="@+id/status_insets"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="45dp"
android:textSize="?attr/status_text_medium"
app:layout_constraintBottom_toBottomOf="@id/status_inset"
app:layout_constraintStart_toStartOf="@id/status_inset"
app:layout_constraintTop_toTopOf="@id/status_inset"
tools:text="1+" />
<at.connyduck.sparkbutton.SparkButton <at.connyduck.sparkbutton.SparkButton
android:id="@+id/status_favourite" android:id="@+id/status_favourite"
android:layout_width="52dp" android:layout_width="52dp"
@ -370,6 +381,17 @@
sparkbutton:primaryColor="@color/tusky_orange" sparkbutton:primaryColor="@color/tusky_orange"
sparkbutton:secondaryColor="@color/tusky_orange_light" /> sparkbutton:secondaryColor="@color/tusky_orange_light" />
<TextView
android:id="@+id/status_favourites_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="45dp"
android:textSize="?attr/status_text_medium"
app:layout_constraintBottom_toBottomOf="@id/status_inset"
app:layout_constraintStart_toStartOf="@id/status_favourite"
app:layout_constraintTop_toTopOf="@id/status_inset"
tools:text="" />
<at.connyduck.sparkbutton.SparkButton <at.connyduck.sparkbutton.SparkButton
android:id="@+id/status_bookmark" android:id="@+id/status_bookmark"
android:layout_width="52dp" android:layout_width="52dp"

View file

@ -664,6 +664,7 @@
<string name="pref_title_show_notifications_filter">Show Notifications filter</string> <string name="pref_title_show_notifications_filter">Show Notifications filter</string>
<string name="pref_title_enable_swipe_for_tabs">Enable swipe gesture to switch between tabs</string> <string name="pref_title_enable_swipe_for_tabs">Enable swipe gesture to switch between tabs</string>
<string name="pref_title_show_stat_inline">Show post statistics in timeline</string>
<string name="create_poll_title">Poll</string> <string name="create_poll_title">Poll</string>

View file

@ -48,6 +48,7 @@ class NotificationsViewModelTestStatusDisplayOptions : NotificationsViewModelTes
confirmFavourites = false, confirmFavourites = false,
hideStats = false, hideStats = false,
animateEmojis = false, animateEmojis = false,
showStatsInline = false,
showSensitiveMedia = true, // setting in NotificationsViewModelTestBase showSensitiveMedia = true, // setting in NotificationsViewModelTestBase
openSpoiler = true // setting in NotificationsViewModelTestBase openSpoiler = true // setting in NotificationsViewModelTestBase
) )

View file

@ -0,0 +1,49 @@
package com.keylesspalace.tusky.util
import org.junit.Assert
import org.junit.Test
import kotlin.math.pow
class NumberUtilsTest {
@Test
fun zeroShouldBeFormattedAsZero() {
val shortNumber = shortNumber(0)
Assert.assertEquals("0", shortNumber)
}
@Test
fun negativeValueShouldBeFormattedToNegativeValue() {
val shortNumber = shortNumber(-1)
Assert.assertEquals("-1", shortNumber)
}
@Test
fun positiveValueShouldBeFormattedToPositiveValue() {
val shortNumber = shortNumber(1)
Assert.assertEquals("1", shortNumber)
}
@Test
fun bigNumbersShouldBeShortened() {
var shortNumber = 1L
Assert.assertEquals("1", shortNumber(shortNumber))
for (i in shortLetters.indices) {
if (i == 0) {
continue
}
shortNumber = 1000.0.pow(i.toDouble()).toLong()
Assert.assertEquals("1.0" + shortLetters[i], shortNumber(shortNumber))
}
}
@Test
fun roundingForNegativeAndPositiveValuesShouldBeTheSame() {
var value = 3492
Assert.assertEquals("-3.5K", shortNumber(-value))
Assert.assertEquals("3.5K", shortNumber(value))
value = 1501
Assert.assertEquals("-1.5K", shortNumber(-value))
Assert.assertEquals("1.5K", shortNumber(value))
}
}