diff --git a/app/src/main/java/app/pachli/components/account/AccountActivity.kt b/app/src/main/java/app/pachli/components/account/AccountActivity.kt
index ff409e740..2d90f5d18 100644
--- a/app/src/main/java/app/pachli/components/account/AccountActivity.kt
+++ b/app/src/main/java/app/pachli/components/account/AccountActivity.kt
@@ -22,10 +22,14 @@ import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
+import android.content.res.Configuration
import android.graphics.Color
+import android.graphics.Typeface
import android.graphics.drawable.LayerDrawable
import android.os.Bundle
+import android.text.SpannableStringBuilder
import android.text.TextWatcher
+import android.text.style.StyleSpan
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
@@ -33,10 +37,12 @@ import android.view.View
import android.view.ViewGroup
import androidx.activity.viewModels
import androidx.annotation.ColorInt
+import androidx.annotation.DrawableRes
import androidx.annotation.Px
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.app.ActivityOptionsCompat
+import androidx.core.graphics.ColorUtils
import androidx.core.view.MenuProvider
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
@@ -61,6 +67,7 @@ import app.pachli.core.navigation.ViewMediaActivityIntent
import app.pachli.core.network.model.Account
import app.pachli.core.network.model.Relationship
import app.pachli.core.network.parseAsMastodonHtml
+import app.pachli.core.preferences.AppTheme
import app.pachli.core.preferences.PrefKeys
import app.pachli.databinding.ActivityAccountBinding
import app.pachli.db.DraftsAlert
@@ -83,6 +90,7 @@ import app.pachli.util.visible
import app.pachli.view.showMuteAccountDialog
import com.bumptech.glide.Glide
import com.google.android.material.appbar.AppBarLayout
+import com.google.android.material.chip.Chip
import com.google.android.material.color.MaterialColors
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.shape.MaterialShapeDrawable
@@ -217,7 +225,7 @@ class AccountActivity :
binding.accountFloatingActionButton.hide()
binding.accountFollowButton.hide()
binding.accountMuteButton.hide()
- binding.accountFollowsYouTextView.hide()
+ binding.accountFollowsYouChip.hide()
// setup the RecyclerView for the account fields
accountFieldAdapter = AccountFieldAdapter(this, animateEmojis)
@@ -484,10 +492,10 @@ class AccountActivity :
accountFieldAdapter.notifyDataSetChanged()
binding.accountLockedImageView.visible(account.locked)
- binding.accountBadgeTextView.visible(account.bot)
updateAccountAvatar()
updateToolbar()
+ updateBadges()
updateMovedAccount()
updateRemoteAccount()
updateAccountJoinedDate()
@@ -654,7 +662,7 @@ class AccountActivity :
// If wellbeing mode is enabled, "follows you" text should not be visible
val wellbeingEnabled = sharedPreferencesRepository.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_PROFILE, false)
- binding.accountFollowsYouTextView.visible(relation.followedBy && !wellbeingEnabled)
+ binding.accountFollowsYouChip.visible(relation.followedBy && !wellbeingEnabled)
// because subscribing is Pleroma extension, enable it __only__ when we have non-null subscribing field
// it's also now supported in Mastodon 3.3.0rc but called notifying and use different API call
@@ -753,6 +761,48 @@ class AccountActivity :
}
}
+ private fun updateBadges() {
+ binding.accountBadgeContainer.removeAllViews()
+
+ val isLight = when (AppTheme.from(sharedPreferencesRepository)) {
+ AppTheme.DAY -> true
+ AppTheme.NIGHT, AppTheme.BLACK -> false
+ AppTheme.AUTO, AppTheme.AUTO_SYSTEM -> {
+ (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_NO
+ }
+ }
+
+ if (loadedAccount?.bot == false) {
+ val badgeView = getBadge(
+ MaterialColors.getColor(
+ binding.accountBadgeContainer,
+ com.google.android.material.R.attr.colorSurfaceVariant,
+ ),
+ R.drawable.ic_bot_24dp,
+ getString(R.string.profile_badge_bot_text),
+ isLight,
+ )
+ binding.accountBadgeContainer.addView(badgeView)
+ }
+
+ // Display badges for any roles. Per the API spec this should only include
+ // roles with a true `highlighted` property, but the web UI doesn't do that,
+ // so follow suit for the moment, https://github.com/mastodon/mastodon/issues/28327
+ loadedAccount?.roles?.forEach { role ->
+ val badgeColor = if (role.color.isNotBlank()) {
+ Color.parseColor(role.color)
+ } else {
+ MaterialColors.getColor(binding.accountBadgeContainer, android.R.attr.colorPrimary)
+ }
+
+ val sb = SpannableStringBuilder("${role.name} ${viewModel.domain}")
+ sb.setSpan(StyleSpan(Typeface.BOLD), 0, role.name.length, 0)
+
+ val badgeView = getBadge(badgeColor, R.drawable.profile_role_badge, sb, isLight)
+ binding.accountBadgeContainer.addView(badgeView)
+ }
+ }
+
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.account_toolbar, menu)
@@ -1004,6 +1054,53 @@ class AccountActivity :
}
}
+ private fun getBadge(
+ @ColorInt baseColor: Int,
+ @DrawableRes icon: Int,
+ text: CharSequence,
+ isLight: Boolean,
+ ): Chip {
+ val badge = Chip(this)
+
+ // Text colour is black or white with ~ 70% opacity
+ // Experiments with the Palette library to extract the colour and pick an
+ // appropriate text colour showed that although the resulting colour could
+ // have marginally more contrast you could get a dark text colour when the
+ // other text colours were light, and vice-versa, making the badge text
+ // appear to be more prominent/important in the information hierarchy.
+ val textColor = if (isLight) Color.argb(178, 0, 0, 0) else Color.argb(178, 255, 255, 255)
+
+ // Badge background colour with 50% transparency so it blends in with the theme background
+ val backgroundColor = Color.argb(128, Color.red(baseColor), Color.green(baseColor), Color.blue(baseColor))
+
+ // Outline colour blends the two
+ val outlineColor = ColorUtils.blendARGB(textColor, baseColor, 0.7f)
+
+ // Configure the badge
+ badge.text = text
+ badge.setTextColor(textColor)
+ badge.chipStrokeWidth = resources.getDimension(R.dimen.profile_badge_stroke_width)
+ badge.chipStrokeColor = ColorStateList.valueOf(outlineColor)
+ badge.setChipIconResource(icon)
+ badge.isChipIconVisible = true
+ badge.chipIconSize = resources.getDimension(R.dimen.profile_badge_icon_size)
+ badge.chipIconTint = ColorStateList.valueOf(textColor)
+ badge.chipBackgroundColor = ColorStateList.valueOf(backgroundColor)
+
+ // Badge isn't clickable, so disable all related behavior
+ badge.isClickable = false
+ badge.isFocusable = false
+ badge.setEnsureMinTouchTargetSize(false)
+
+ // Reset some chip defaults so it looks better for our badge usecase
+ badge.iconStartPadding = resources.getDimension(R.dimen.profile_badge_icon_start_padding)
+ badge.iconEndPadding = resources.getDimension(R.dimen.profile_badge_icon_end_padding)
+ badge.minHeight = resources.getDimensionPixelSize(R.dimen.profile_badge_min_height)
+ badge.chipMinHeight = resources.getDimension(R.dimen.profile_badge_min_height)
+ badge.updatePadding(top = 0, bottom = 0)
+ return badge
+ }
+
companion object {
private val argbEvaluator = ArgbEvaluator()
}
diff --git a/app/src/main/java/app/pachli/components/account/AccountViewModel.kt b/app/src/main/java/app/pachli/components/account/AccountViewModel.kt
index b34b0b660..a3c5c33ee 100644
--- a/app/src/main/java/app/pachli/components/account/AccountViewModel.kt
+++ b/app/src/main/java/app/pachli/components/account/AccountViewModel.kt
@@ -44,6 +44,9 @@ class AccountViewModel @Inject constructor(
lateinit var accountId: String
var isSelf = false
+ /** The domain of the viewed account **/
+ var domain = ""
+
/** True if the viewed account has the same domain as the active account */
var isFromOwnDomain = false
@@ -70,11 +73,12 @@ class AccountViewModel @Inject constructor(
mastodonApi.account(accountId)
.fold(
{ account ->
+ domain = getDomain(account.url)
accountData.postValue(Success(account))
isDataLoading = false
isRefreshing.postValue(false)
- isFromOwnDomain = getDomain(account.url) == activeAccount.domain
+ isFromOwnDomain = domain == activeAccount.domain
},
{ t ->
Timber.w("failed obtaining account", t)
diff --git a/app/src/main/res/drawable/profile_role_badge.xml b/app/src/main/res/drawable/profile_role_badge.xml
new file mode 100644
index 000000000..7ff0934c6
--- /dev/null
+++ b/app/src/main/res/drawable/profile_role_badge.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_account.xml b/app/src/main/res/layout/activity_account.xml
index 2229ffc78..6e70de0e1 100644
--- a/app/src/main/res/layout/activity_account.xml
+++ b/app/src/main/res/layout/activity_account.xml
@@ -150,42 +150,43 @@
app:srcCompat="@drawable/ic_reblog_private_24dp"
tools:visibility="visible" />
-
-
+ app:chipSpacingVertical="4dp"
+ app:layout_constraintStart_toEndOf="@id/accountUsernameTextView"
+ app:layout_constraintTop_toBottomOf="@id/accountFollowsYouChip"
+ app:layout_goneMarginStart="0dp" />
+ app:constraint_referenced_ids="accountFollowsYouChip,accountBadgeContainer" />
160dp
14dp
+ 1dp
+ 24dp
+ 16dp
+ 4dp
+ 0dp
5dp
diff --git a/core/network/src/main/kotlin/app/pachli/core/network/model/Account.kt b/core/network/src/main/kotlin/app/pachli/core/network/model/Account.kt
index 11e7a56ba..2cc6f7c46 100644
--- a/core/network/src/main/kotlin/app/pachli/core/network/model/Account.kt
+++ b/core/network/src/main/kotlin/app/pachli/core/network/model/Account.kt
@@ -38,7 +38,7 @@ data class Account(
val emojis: List? = emptyList(), // nullable for backward compatibility
val fields: List? = emptyList(), // nullable for backward compatibility
val moved: Account? = null,
-
+ val roles: List? = emptyList()
) {
val name: String
@@ -69,3 +69,13 @@ data class StringField(
val name: String,
val value: String,
)
+
+/** [Mastodon Entities: Role](https://docs.joinmastodon.org/entities/Role) */
+data class Role(
+ /** Displayable name of the role */
+ val name: String,
+ /** Colour to use for the role badge, may be the empty string */
+ val color: String,
+ /** True if the badge should be displayed on the account profile */
+ val highlighted: Boolean,
+)