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(
RecipientPreference.Model(
recipient = group,
onClick = {
onRowClick = {
CommunicationActions.startConversation(requireActivity(), group, null)
requireActivity().finish()
}
@@ -787,13 +787,26 @@ class ConversationSettingsFragment : DSLSettingsFragment(
)
for (member in groupState.members) {
val canSetMemberLabel = member.member.isSelf && groupState.canSetOwnMemberLabel
val memberLabel = member.getMemberLabel(groupState)
customPref(
RecipientPreference.Model(
recipient = member.member,
isAdmin = member.isAdmin,
memberLabel = member.getMemberLabel(groupState),
memberLabel = memberLabel,
canSetMemberLabel = canSetMemberLabel,
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)
}
)

View File

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

View File

@@ -362,6 +362,7 @@ sealed class ConversationSettingsViewModel(
if (groupId.isV2) {
loadMemberLabels(groupId.requireV2(), fullMembers)
loadCanSetMemberLabel(groupId.requireV2())
}
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(

View File

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

View File

@@ -52,6 +52,12 @@ class MemberLabelRepository private constructor(
@WorkerThread
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.
*/

View File

@@ -55,7 +55,7 @@
android:textAlignment="viewStart"
android:textAppearance="@style/Signal.Text.BodyLarge"
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_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@+id/recipient_avatar"
@@ -63,47 +63,57 @@
app:layout_constraintVertical_chainStyle="packed"
tools:text="Miles Morales" />
<org.thoughtcrime.securesms.groups.memberlabel.MemberLabelPillView
android:id="@+id/recipient_member_label"
<FrameLayout
android:id="@+id/recipient_info_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="1dp"
android:layout_marginEnd="16dp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/recipient_about"
android:layout_marginHorizontal="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/admin"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="@+id/recipient_name"
app:layout_constraintTop_toBottomOf="@+id/recipient_name"
app:layout_constraintWidth_default="wrap"
app:layout_goneMarginEnd="0dp"
tools:visibility="visible" />
app:layout_constraintStart_toEndOf="@+id/recipient_avatar"
app:layout_constraintTop_toBottomOf="@+id/recipient_name">
<org.thoughtcrime.securesms.groups.memberlabel.MemberLabelPillView
android:id="@+id/recipient_member_label"
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
android:id="@+id/recipient_content_end_barrier"
android:layout_width="0dp"
android:layout_height="0dp"
app:barrierDirection="end"
app:constraint_referenced_ids="recipient_name, recipient_about" />
<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" />
app:constraint_referenced_ids="recipient_name,recipient_info_container" />
<TextView
android:id="@+id/admin"

View File

@@ -5167,6 +5167,8 @@
<!-- Button text to approve a user that has requested to join a group -->
<string name="GroupRecipientListItem_approve_description">Approve</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 -->