feat: Show roles on profiles (#312)
Roles for the logged in user appeared in Mastodon 4.0.0 and can be displayed on the user's profile screen. Show them as chips, adjusting the display of the existing "Follows you" and "Bot" indicators to make allowances for this. Roles can have a custom colour assigned by the server admin. This is blended with the app colour so it is not too jarring in the display. See also https://github.com/tuskyapp/Tusky/pull/4029 Co-authored-by: Konrad Pozniak <opensource@connyduck.at>
This commit is contained in:
parent
60cfa99f17
commit
cbecfa3117
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<!--
|
||||
~ Copyright 2023 Pachli Association
|
||||
~
|
||||
~ This file is a part of Pachli.
|
||||
~
|
||||
~ 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.
|
||||
~
|
||||
~ Pachli 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 Pachli; if not,
|
||||
~ see <http://www.gnu.org/licenses>.
|
||||
-->
|
||||
|
||||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M20,7h-5V4c0,-1.1 -0.9,-2 -2,-2h-2C9.9,2 9,2.9 9,4v3H4C2.9,7 2,7.9 2,9v11c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V9C22,7.9 21.1,7 20,7zM9,12c0.83,0 1.5,0.67 1.5,1.5S9.83,15 9,15s-1.5,-0.67 -1.5,-1.5S8.17,12 9,12zM12,18H6v-0.75c0,-1 2,-1.5 3,-1.5s3,0.5 3,1.5V18zM13,9h-2V4h2V9zM18,16.5h-4V15h4V16.5zM18,13.5h-4V12h4V13.5z"/>
|
||||
</vector>
|
|
@ -150,42 +150,43 @@
|
|||
app:srcCompat="@drawable/ic_reblog_private_24dp"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/accountFollowsYouTextView"
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/accountFollowsYouChip"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:background="@drawable/profile_badge_background"
|
||||
android:clickable="false"
|
||||
android:focusable="false"
|
||||
android:minHeight="@dimen/profile_badge_min_height"
|
||||
android:text="@string/follows_you"
|
||||
android:textSize="?attr/status_text_small"
|
||||
android:textColor="?colorOnPrimaryContainer"
|
||||
android:visibility="gone"
|
||||
app:chipBackgroundColor="#0000"
|
||||
app:chipMinHeight="@dimen/profile_badge_min_height"
|
||||
app:chipStrokeColor="?android:attr/textColorTertiary"
|
||||
app:chipStrokeWidth="@dimen/profile_badge_stroke_width"
|
||||
app:ensureMinTouchTargetSize="false"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/accountUsernameTextView"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/accountBadgeTextView"
|
||||
android:layout_width="wrap_content"
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/accountBadgeContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:background="@drawable/profile_badge_background"
|
||||
android:textColor="?colorOnPrimaryContainer"
|
||||
android:text="@string/profile_badge_bot_text"
|
||||
android:textSize="?attr/status_text_small"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toEndOf="@id/accountFollowsYouTextView"
|
||||
app:layout_constraintTop_toBottomOf="@id/accountUsernameTextView"
|
||||
app:layout_goneMarginStart="0dp"
|
||||
tools:visibility="visible" />
|
||||
app:chipSpacingVertical="4dp"
|
||||
app:layout_constraintStart_toEndOf="@id/accountUsernameTextView"
|
||||
app:layout_constraintTop_toBottomOf="@id/accountFollowsYouChip"
|
||||
app:layout_goneMarginStart="0dp" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/labelBarrier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="accountFollowsYouTextView,accountBadgeTextView" />
|
||||
app:constraint_referenced_ids="accountFollowsYouChip,accountBadgeContainer" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/accountNoteTextInputLayout"
|
||||
|
|
|
@ -43,6 +43,11 @@
|
|||
<dimen name="min_report_button_width">160dp</dimen>
|
||||
<dimen name="account_avatar_background_radius">14dp</dimen>
|
||||
|
||||
<dimen name="profile_badge_stroke_width">1dp</dimen>
|
||||
<dimen name="profile_badge_min_height">24dp</dimen>
|
||||
<dimen name="profile_badge_icon_size">16dp</dimen>
|
||||
<dimen name="profile_badge_icon_start_padding">4dp</dimen>
|
||||
<dimen name="profile_badge_icon_end_padding">0dp</dimen>
|
||||
|
||||
<dimen name="card_radius">5dp</dimen>
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ data class Account(
|
|||
val emojis: List<Emoji>? = emptyList(), // nullable for backward compatibility
|
||||
val fields: List<Field>? = emptyList(), // nullable for backward compatibility
|
||||
val moved: Account? = null,
|
||||
|
||||
val roles: List<Role>? = 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,
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue