Add ability to set group member label from conversation settings.

This commit is contained in:
jeffrey-signal
2026-02-25 09:59:10 -05:00
committed by Cody Henthorne
parent 415dbd1b61
commit 7d1897a9d2
7 changed files with 121 additions and 51 deletions

View File

@@ -739,7 +739,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
customPref( customPref(
RecipientPreference.Model( RecipientPreference.Model(
recipient = group, recipient = group,
onClick = { onRowClick = {
CommunicationActions.startConversation(requireActivity(), group, null) CommunicationActions.startConversation(requireActivity(), group, null)
requireActivity().finish() requireActivity().finish()
} }
@@ -787,13 +787,26 @@ class ConversationSettingsFragment : DSLSettingsFragment(
) )
for (member in groupState.members) { for (member in groupState.members) {
val canSetMemberLabel = member.member.isSelf && groupState.canSetOwnMemberLabel
val memberLabel = member.getMemberLabel(groupState)
customPref( customPref(
RecipientPreference.Model( RecipientPreference.Model(
recipient = member.member, recipient = member.member,
isAdmin = member.isAdmin, isAdmin = member.isAdmin,
memberLabel = member.getMemberLabel(groupState), memberLabel = memberLabel,
canSetMemberLabel = canSetMemberLabel,
lifecycleOwner = viewLifecycleOwner, lifecycleOwner = viewLifecycleOwner,
onClick = { onRowClick = {
if (canSetMemberLabel && memberLabel == null) {
val action = ConversationSettingsFragmentDirections
.actionConversationSettingsFragmentToMemberLabelFragment(groupState.groupId)
navController.safeNavigate(action)
} else {
RecipientBottomSheetDialogFragment.show(parentFragmentManager, member.member.id, groupState.groupId)
}
},
onAvatarClick = {
RecipientBottomSheetDialogFragment.show(parentFragmentManager, member.member.id, groupState.groupId) RecipientBottomSheetDialogFragment.show(parentFragmentManager, member.member.id, groupState.groupId)
} }
) )

View File

@@ -84,7 +84,8 @@ sealed class SpecificSettingsState {
val membershipCountDescription: String = "", val membershipCountDescription: String = "",
val legacyGroupState: LegacyGroupPreference.State = LegacyGroupPreference.State.NONE, val legacyGroupState: LegacyGroupPreference.State = LegacyGroupPreference.State.NONE,
val isAnnouncementGroup: Boolean = false, val isAnnouncementGroup: Boolean = false,
val memberLabelsByRecipientId: Map<RecipientId, MemberLabel> = emptyMap() val memberLabelsByRecipientId: Map<RecipientId, MemberLabel> = emptyMap(),
val canSetOwnMemberLabel: Boolean = false
) : SpecificSettingsState() { ) : SpecificSettingsState() {
override val isLoaded: Boolean = groupTitleLoaded && groupDescriptionLoaded override val isLoaded: Boolean = groupTitleLoaded && groupDescriptionLoaded

View File

@@ -362,6 +362,7 @@ sealed class ConversationSettingsViewModel(
if (groupId.isV2) { if (groupId.isV2) {
loadMemberLabels(groupId.requireV2(), fullMembers) loadMemberLabels(groupId.requireV2(), fullMembers)
loadCanSetMemberLabel(groupId.requireV2())
} }
state.copy( state.copy(
@@ -520,6 +521,17 @@ sealed class ConversationSettingsViewModel(
) )
} }
} }
private fun loadCanSetMemberLabel(v2GroupId: GroupId.V2) = viewModelScope.launch(SignalDispatchers.IO) {
val canSetLabel = MemberLabelRepository.instance.canSetLabel(v2GroupId, Recipient.self())
store.update {
it.copy(
specificSettingsState = it.requireGroupSettingsState().copy(
canSetOwnMemberLabel = canSetLabel
)
)
}
}
} }
class Factory( class Factory(

View File

@@ -36,8 +36,10 @@ object RecipientPreference {
val recipient: Recipient, val recipient: Recipient,
val isAdmin: Boolean = false, val isAdmin: Boolean = false,
val memberLabel: StyledMemberLabel? = null, val memberLabel: StyledMemberLabel? = null,
val canSetMemberLabel: Boolean = false,
val lifecycleOwner: LifecycleOwner? = null, val lifecycleOwner: LifecycleOwner? = null,
val onClick: (() -> Unit)? = null val onRowClick: (() -> Unit)? = null,
val onAvatarClick: (() -> Unit)? = null
) : PreferenceModel<Model>() { ) : PreferenceModel<Model>() {
override fun areItemsTheSame(newItem: Model): Boolean { override fun areItemsTheSame(newItem: Model): Boolean {
return recipient.id == newItem.recipient.id return recipient.id == newItem.recipient.id
@@ -47,7 +49,8 @@ object RecipientPreference {
return super.areContentsTheSame(newItem) && return super.areContentsTheSame(newItem) &&
recipient.hasSameContent(newItem.recipient) && recipient.hasSameContent(newItem.recipient) &&
isAdmin == newItem.isAdmin && isAdmin == newItem.isAdmin &&
memberLabel == newItem.memberLabel memberLabel == newItem.memberLabel &&
canSetMemberLabel == newItem.canSetMemberLabel
} }
} }
@@ -56,28 +59,36 @@ object RecipientPreference {
private val name: TextView = itemView.findViewById(R.id.recipient_name) private val name: TextView = itemView.findViewById(R.id.recipient_name)
private val about: TextView? = itemView.findViewById(R.id.recipient_about) private val about: TextView? = itemView.findViewById(R.id.recipient_about)
private val memberLabelView: MemberLabelPillView? = itemView.findViewById(R.id.recipient_member_label) private val memberLabelView: MemberLabelPillView? = itemView.findViewById(R.id.recipient_member_label)
private val addMemberLabelView: TextView? = itemView.findViewById(R.id.add_member_label)
private val admin: View? = itemView.findViewById(R.id.admin) private val admin: View? = itemView.findViewById(R.id.admin)
private val badge: BadgeImageView = itemView.findViewById(R.id.recipient_badge) private val badge: BadgeImageView = itemView.findViewById(R.id.recipient_badge)
private var recipient: Recipient? = null private var recipient: Recipient? = null
private var canSetMemberLabel: Boolean = false
private val recipientObserver = Observer<Recipient> { recipient -> private val recipientObserver = Observer<Recipient> { recipient ->
onRecipientChanged(recipient) onRecipientChanged(recipient = recipient, memberLabel = null, canSetMemberLabel = canSetMemberLabel)
} }
override fun bind(model: Model) { override fun bind(model: Model) {
if (model.onClick != null) { if (model.onRowClick != null) {
itemView.setOnClickListener { model.onClick.invoke() } itemView.setOnClickListener { model.onRowClick.invoke() }
} else { } else {
itemView.setOnClickListener(null) itemView.setOnClickListener(null)
} }
if (model.onAvatarClick != null) {
avatar.setOnClickListener { model.onAvatarClick.invoke() }
} else {
avatar.setOnClickListener(null)
}
canSetMemberLabel = model.canSetMemberLabel
if (model.lifecycleOwner != null) { if (model.lifecycleOwner != null) {
observeRecipient(model.lifecycleOwner, model.recipient) observeRecipient(model.lifecycleOwner, model.recipient)
model.memberLabel?.let(::showMemberLabel)
} else {
onRecipientChanged(model.recipient, model.memberLabel)
} }
onRecipientChanged(model.recipient, model.memberLabel, model.canSetMemberLabel)
admin?.visible = model.isAdmin admin?.visible = model.isAdmin
} }
@@ -86,7 +97,7 @@ object RecipientPreference {
unbind() unbind()
} }
private fun onRecipientChanged(recipient: Recipient, memberLabel: StyledMemberLabel? = null) { private fun onRecipientChanged(recipient: Recipient, memberLabel: StyledMemberLabel? = null, canSetMemberLabel: Boolean = false) {
avatar.setRecipient(recipient) avatar.setRecipient(recipient)
badge.setBadgeFromRecipient(recipient) badge.setBadgeFromRecipient(recipient)
name.text = if (recipient.isSelf) { name.text = if (recipient.isSelf) {
@@ -104,17 +115,17 @@ object RecipientPreference {
} }
} }
val aboutText = recipient.combinedAboutAndEmoji
when { when {
memberLabel != null -> showMemberLabel(memberLabel) memberLabel != null -> showMemberLabel(memberLabel)
!recipient.combinedAboutAndEmoji.isNullOrEmpty() -> { recipient.isSelf && canSetMemberLabel -> showAddMemberLabel()
about?.text = recipient.combinedAboutAndEmoji
about?.visible = true !aboutText.isNullOrBlank() -> showAbout(aboutText)
memberLabelView?.visible = false
}
else -> { else -> {
memberLabelView?.visible = false memberLabelView?.visible = false
addMemberLabelView?.visible = false
about?.visible = false about?.visible = false
} }
} }
@@ -131,9 +142,24 @@ object RecipientPreference {
visible = true visible = true
} }
addMemberLabelView?.visible = false
about?.visible = false about?.visible = false
} }
private fun showAddMemberLabel() {
addMemberLabelView?.visible = true
memberLabelView?.visible = false
about?.visible = false
}
private fun showAbout(text: String) {
about?.text = text
about?.visible = true
memberLabelView?.visible = false
addMemberLabelView?.visible = false
}
private fun observeRecipient(lifecycleOwner: LifecycleOwner?, recipient: Recipient?) { private fun observeRecipient(lifecycleOwner: LifecycleOwner?, recipient: Recipient?) {
this.recipient?.live()?.liveData?.removeObserver(recipientObserver) this.recipient?.live()?.liveData?.removeObserver(recipientObserver)

View File

@@ -52,6 +52,12 @@ class MemberLabelRepository private constructor(
@WorkerThread @WorkerThread
fun getLabelJava(groupId: GroupId.V2, recipient: Recipient): MemberLabel? = runBlocking { getLabel(groupId, recipient) } fun getLabelJava(groupId: GroupId.V2, recipient: Recipient): MemberLabel? = runBlocking { getLabel(groupId, recipient) }
/**
* Checks whether the [Recipient] has permission to set their member label in the given group (blocking version for Java compatibility).
*/
@WorkerThread
fun canSetLabelJava(groupId: GroupId.V2, recipient: Recipient): Boolean = runBlocking { canSetLabel(groupId, recipient) }
/** /**
* Gets the member label for a specific recipient in the group. * Gets the member label for a specific recipient in the group.
*/ */

View File

@@ -55,7 +55,7 @@
android:textAlignment="viewStart" android:textAlignment="viewStart"
android:textAppearance="@style/Signal.Text.BodyLarge" android:textAppearance="@style/Signal.Text.BodyLarge"
app:layout_constrainedWidth="true" app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@+id/recipient_member_label" app:layout_constraintBottom_toTopOf="@+id/recipient_info_container"
app:layout_constraintEnd_toStartOf="@+id/admin" app:layout_constraintEnd_toStartOf="@+id/admin"
app:layout_constraintHorizontal_bias="0" app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@+id/recipient_avatar" app:layout_constraintStart_toEndOf="@+id/recipient_avatar"
@@ -63,47 +63,57 @@
app:layout_constraintVertical_chainStyle="packed" app:layout_constraintVertical_chainStyle="packed"
tools:text="Miles Morales" /> tools:text="Miles Morales" />
<org.thoughtcrime.securesms.groups.memberlabel.MemberLabelPillView <FrameLayout
android:id="@+id/recipient_member_label" android:id="@+id/recipient_info_container"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="1dp" android:layout_marginHorizontal="16dp"
android:layout_marginEnd="16dp" app:layout_constraintBottom_toBottomOf="parent"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/recipient_about"
app:layout_constraintEnd_toStartOf="@+id/admin" app:layout_constraintEnd_toStartOf="@+id/admin"
app:layout_constraintHorizontal_bias="0" app:layout_constraintStart_toEndOf="@+id/recipient_avatar"
app:layout_constraintStart_toStartOf="@+id/recipient_name" app:layout_constraintTop_toBottomOf="@+id/recipient_name">
app:layout_constraintTop_toBottomOf="@+id/recipient_name"
app:layout_constraintWidth_default="wrap" <org.thoughtcrime.securesms.groups.memberlabel.MemberLabelPillView
app:layout_goneMarginEnd="0dp" android:id="@+id/recipient_member_label"
tools:visibility="visible" /> android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:visibility="gone" />
<TextView
android:id="@+id/add_member_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableEnd="@drawable/symbol_chevron_right_compact_bold_16"
android:drawableTint="@color/signal_colorOnSurfaceVariant"
android:gravity="center_vertical"
android:text="@string/GroupRecipientListItem__add_member_label"
android:textAppearance="@style/Signal.Text.LabelMedium"
android:textColor="@color/signal_colorOnSurfaceVariant"
android:visibility="visible" />
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/recipient_about"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="start|center_vertical"
android:maxLines="1"
android:textAlignment="viewStart"
android:textAppearance="@style/Signal.Text.BodyMedium"
android:textColor="@color/signal_text_secondary"
android:visibility="gone"
tools:text="Hangin' around the web"
tools:visibility="gone" />
</FrameLayout>
<androidx.constraintlayout.widget.Barrier <androidx.constraintlayout.widget.Barrier
android:id="@+id/recipient_content_end_barrier" android:id="@+id/recipient_content_end_barrier"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:barrierDirection="end" app:barrierDirection="end"
app:constraint_referenced_ids="recipient_name, recipient_about" /> app:constraint_referenced_ids="recipient_name,recipient_info_container" />
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/recipient_about"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:gravity="start|center_vertical"
android:maxLines="1"
android:textAlignment="viewStart"
android:textAppearance="@style/Signal.Text.BodyMedium"
android:textColor="@color/signal_text_secondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/admin"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@+id/recipient_avatar"
app:layout_constraintTop_toBottomOf="@+id/recipient_member_label"
tools:text="Hangin' around the web" />
<TextView <TextView
android:id="@+id/admin" android:id="@+id/admin"

View File

@@ -5167,6 +5167,8 @@
<!-- Button text to approve a user that has requested to join a group --> <!-- Button text to approve a user that has requested to join a group -->
<string name="GroupRecipientListItem_approve_description">Approve</string> <string name="GroupRecipientListItem_approve_description">Approve</string>
<string name="GroupRecipientListItem_deny_description">Deny</string> <string name="GroupRecipientListItem_deny_description">Deny</string>
<!-- Placeholder shown in the group member list when the user has not yet set a member label. -->
<string name="GroupRecipientListItem__add_member_label">Add member label</string>
<!-- GroupsLearnMoreBottomSheetDialogFragment --> <!-- GroupsLearnMoreBottomSheetDialogFragment -->