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, +)