diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e31c4f5c6d..1f2d99bc0d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -933,6 +933,11 @@
android:exported="false"
android:theme="@style/Signal.DayNight.NoActionBar" />
+
+
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt
index ca9628d325..78be36b9a2 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt
@@ -35,6 +35,7 @@ import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.concurrent.addTo
import org.signal.core.util.getParcelableArrayListExtraCompat
import org.signal.core.util.orNull
+import org.signal.core.util.requireParcelableCompat
import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.AvatarPreviewActivity
import org.thoughtcrime.securesms.BlockUnblockDialog
@@ -71,6 +72,7 @@ import org.thoughtcrime.securesms.conversation.ConversationIntents
import org.thoughtcrime.securesms.conversation.colors.ColorizerV2
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.groups.GroupId
+import org.thoughtcrime.securesms.groups.memberlabel.MemberLabelEducationSheet
import org.thoughtcrime.securesms.groups.memberlabel.StyledMemberLabel
import org.thoughtcrime.securesms.groups.ui.GroupErrors
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog
@@ -189,6 +191,16 @@ class ConversationSettingsFragment :
super.onViewCreated(view, savedInstanceState)
+ parentFragmentManager.setFragmentResultListener(MemberLabelEducationSheet.RESULT_EDIT_MEMBER_LABEL, viewLifecycleOwner) { _, bundle ->
+ val groupId = bundle.requireParcelableCompat(MemberLabelEducationSheet.KEY_GROUP_ID, GroupId.V2::class.java)
+ navController.safeNavigate(ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToMemberLabelFragment(groupId))
+ }
+
+ parentFragmentManager.setFragmentResultListener(AboutSheet.RESULT_EDIT_MEMBER_LABEL, viewLifecycleOwner) { _, bundle ->
+ val groupId = bundle.requireParcelableCompat(AboutSheet.RESULT_GROUP_ID, GroupId.V2::class.java)
+ navController.safeNavigate(ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToMemberLabelFragment(groupId))
+ }
+
recyclerView?.addOnScrollListener(ConversationSettingsOnUserScrolledAnimationHelper(toolbarAvatarContainer, toolbarTitle, toolbarBackground))
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt
index 15bdeddb4f..730085d139 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt
@@ -120,6 +120,7 @@ import org.signal.core.util.concurrent.addTo
import org.signal.core.util.dp
import org.signal.core.util.logging.Log
import org.signal.core.util.orNull
+import org.signal.core.util.requireParcelableCompat
import org.signal.core.util.setActionItemTint
import org.signal.donations.InAppPaymentType
import org.signal.ringrtc.CallLinkRootKey
@@ -158,7 +159,6 @@ import org.thoughtcrime.securesms.components.mention.MentionAnnotation
import org.thoughtcrime.securesms.components.menu.ActionItem
import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
-import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity.Companion.remoteBackups
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.CheckoutFlowActivity
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalFragment
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity
@@ -257,6 +257,8 @@ import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ProjectionPlayerHolder
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ProjectionRecycler
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange
+import org.thoughtcrime.securesms.groups.memberlabel.MemberLabelActivity
+import org.thoughtcrime.securesms.groups.memberlabel.MemberLabelEducationSheet
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
import org.thoughtcrime.securesms.groups.ui.GroupErrors
import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog
@@ -319,6 +321,7 @@ import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDial
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientExporter
import org.thoughtcrime.securesms.recipients.RecipientId
+import org.thoughtcrime.securesms.recipients.ui.about.AboutSheet
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment
import org.thoughtcrime.securesms.recipients.ui.disappearingmessages.RecipientDisappearingMessagesActivity
import org.thoughtcrime.securesms.registration.ui.RegistrationActivity
@@ -686,6 +689,16 @@ class ConversationFragment :
container.fragmentManager = childFragmentManager
+ childFragmentManager.setFragmentResultListener(MemberLabelEducationSheet.RESULT_EDIT_MEMBER_LABEL, viewLifecycleOwner) { _, bundle ->
+ val groupId = bundle.requireParcelableCompat(MemberLabelEducationSheet.KEY_GROUP_ID, GroupId.V2::class.java)
+ startActivity(MemberLabelActivity.createIntent(requireContext(), groupId))
+ }
+
+ childFragmentManager.setFragmentResultListener(AboutSheet.RESULT_EDIT_MEMBER_LABEL, viewLifecycleOwner) { _, bundle ->
+ val groupId = bundle.requireParcelableCompat(AboutSheet.RESULT_GROUP_ID, GroupId.V2::class.java)
+ startActivity(MemberLabelActivity.createIntent(requireContext(), groupId))
+ }
+
ToolbarDependentMarginListener(binding.toolbar)
initializeMediaKeyboard()
@@ -1004,9 +1017,13 @@ class ConversationFragment :
when {
state.isReactionDelegateShowing -> reactionDelegate.hide()
+
state.isSearchRequested -> searchMenuItem?.collapseActionView()
+
state.isInActionMode -> finishActionMode()
+
state.isMediaKeyboardShowing -> container.hideInput()
+
else -> {
// State has changed since the back handler was enabled. Let the back press proceed
// to the next handler by triggering onBackPressed again after setting a skip flag
@@ -1886,13 +1903,16 @@ class ConversationFragment :
when (data) {
is ShareOrDraftData.SendKeyboardImage -> sendMessageWithoutComposeInput(slide = data.slide, clearCompose = false)
+
is ShareOrDraftData.SendSticker -> sendMessageWithoutComposeInput(slide = data.slide, clearCompose = true)
+
is ShareOrDraftData.SetText -> {
composeText.setDraftText(data.text)
inputPanel.clickOnComposeInput()
}
is ShareOrDraftData.SetLocation -> attachmentManager.setLocation(data.location, MediaConstraints.getPushMediaConstraints())
+
is ShareOrDraftData.SetEditMessage -> {
composeText.setDraftText(data.draftText)
inputPanel.enterEditMessageMode(Glide.with(this), data.messageEdit, true, data.clearQuote)
@@ -2689,6 +2709,7 @@ class ConversationFragment :
.subscribeBy { result ->
when (result) {
is Result.Success -> Log.d(TAG, "$logMessage complete")
+
is Result.Failure -> {
Log.d(TAG, "$logMessage failed ${result.failure}")
toast(GroupErrors.getUserDisplayMessage(result.failure))
@@ -4156,8 +4177,11 @@ class ConversationFragment :
val slides: List = result.nonUploadedMedia.mapNotNull {
when {
MediaUtil.isVideoType(it.contentType) -> VideoSlide(requireContext(), it.uri, it.size, it.isVideoGif, it.width, it.height, it.caption, it.transformProperties)
+
MediaUtil.isGif(it.contentType) -> GifSlide(requireContext(), it.uri, it.size, it.width, it.height, it.isBorderless, it.caption)
+
MediaUtil.isImageType(it.contentType) -> ImageSlide(requireContext(), it.uri, it.contentType, it.size, it.width, it.height, it.isBorderless, it.caption, null, it.transformProperties)
+
MediaUtil.isDocumentType(it.contentType) -> {
DocumentSlide(requireContext(), it.uri, it.contentType!!, it.size, it.fileName)
}
@@ -4359,6 +4383,7 @@ class ConversationFragment :
.subscribeBy { result ->
when (result) {
is Result.Success -> Log.d(TAG, "Cancel request complete")
+
is Result.Failure -> {
Log.d(TAG, "Cancel join request failed ${result.failure}")
toast(GroupErrors.getUserDisplayMessage(result.failure))
@@ -4737,9 +4762,13 @@ class ConversationFragment :
if (button != null) {
when (button) {
AttachmentKeyboardButton.GALLERY -> conversationActivityResultContracts.launchGallery(recipient.id, composeText.textTrimmed, inputPanel.quote.isPresent)
+
AttachmentKeyboardButton.CONTACT -> conversationActivityResultContracts.launchSelectContact()
+
AttachmentKeyboardButton.LOCATION -> conversationActivityResultContracts.launchSelectLocation(recipient.chatColors)
+
AttachmentKeyboardButton.PAYMENT -> AttachmentManager.selectPayment(this@ConversationFragment, recipient)
+
AttachmentKeyboardButton.FILE -> {
if (!conversationActivityResultContracts.launchSelectFile()) {
toast(R.string.AttachmentManager_cant_open_media_selection, Toast.LENGTH_LONG)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupId.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupId.kt
index dd22c46b8e..0bd9993561 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupId.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupId.kt
@@ -244,6 +244,10 @@ sealed class GroupId(private val encodedId: String) : DatabaseId, Parcelable {
return this as V2
}
+ fun v2OrNull(): V2? {
+ return if (isV2) (this as V2) else null
+ }
+
fun requirePush(): Push {
assert(this is Push)
return this as Push
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelActivity.kt
new file mode 100644
index 0000000000..8c6c8bdcf2
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelActivity.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2026 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.thoughtcrime.securesms.groups.memberlabel
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import org.signal.core.util.getParcelableExtraCompat
+import org.thoughtcrime.securesms.PassphraseRequiredActivity
+import org.thoughtcrime.securesms.groups.GroupId
+
+/**
+ * Hosts [MemberLabelFragment], allowing navigation to the member label editor from any context.
+ */
+class MemberLabelActivity : PassphraseRequiredActivity() {
+ companion object {
+ private const val EXTRA_GROUP_ID = "group_id"
+
+ fun createIntent(context: Context, groupId: GroupId.V2): Intent {
+ return Intent(context, MemberLabelActivity::class.java).apply {
+ putExtra(EXTRA_GROUP_ID, groupId)
+ }
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
+ super.onCreate(savedInstanceState, ready)
+
+ if (savedInstanceState == null) {
+ val groupId = intent.getParcelableExtraCompat(EXTRA_GROUP_ID, GroupId.V2::class.java)!!
+ val fragment = MemberLabelFragment.newInstance(groupId)
+ supportFragmentManager.beginTransaction()
+ .replace(android.R.id.content, fragment)
+ .commit()
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelEducationSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelEducationSheet.kt
new file mode 100644
index 0000000000..917a2e6e95
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelEducationSheet.kt
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2026 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.thoughtcrime.securesms.groups.memberlabel
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.core.os.bundleOf
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.setFragmentResult
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import org.signal.core.ui.compose.AllDevicePreviews
+import org.signal.core.ui.compose.BottomSheets
+import org.signal.core.ui.compose.Buttons
+import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment
+import org.signal.core.ui.compose.DayNightPreviews
+import org.signal.core.ui.compose.Previews
+import org.signal.core.util.requireParcelableCompat
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.groups.GroupId
+import org.thoughtcrime.securesms.util.viewModel
+
+/**
+ * Explains what member labels are and provides options to edit the current user's label.
+ */
+class MemberLabelEducationSheet : ComposeBottomSheetDialogFragment() {
+ companion object {
+ const val RESULT_EDIT_MEMBER_LABEL = "edit_member_label"
+ const val KEY_GROUP_ID = "group_id"
+
+ private const val FRAGMENT_TAG = "MemberLabelEducationSheet"
+ private const val ARGS_GROUP_ID = "group_id"
+
+ fun show(fragmentManager: FragmentManager, groupId: GroupId.V2) {
+ val fragment = MemberLabelEducationSheet().apply {
+ arguments = bundleOf(ARGS_GROUP_ID to groupId)
+ }
+ fragment.show(fragmentManager, FRAGMENT_TAG)
+ }
+ }
+
+ private val groupId: GroupId.V2 by lazy {
+ requireArguments().requireParcelableCompat(ARGS_GROUP_ID, GroupId.V2::class.java)
+ }
+
+ private val viewModel: MemberLabelEducationViewModel by viewModel {
+ MemberLabelEducationViewModel(groupId)
+ }
+
+ override val peekHeightPercentage: Float = 1f
+
+ @Composable
+ override fun SheetContent() {
+ val state by viewModel.uiState.collectAsStateWithLifecycle()
+
+ val callbacks = remember {
+ object : MemberLabelEducationUiCallbacks {
+ override fun onSetLabelClicked() {
+ setFragmentResult(RESULT_EDIT_MEMBER_LABEL, bundleOf(KEY_GROUP_ID to groupId))
+ dismiss()
+ }
+
+ override fun onDismiss() = dismiss()
+ }
+ }
+
+ MemberLabelEducationSheetContent(
+ state = state,
+ callbacks = callbacks
+ )
+ }
+}
+
+@Composable
+private fun MemberLabelEducationSheetContent(
+ state: MemberLabelEducationViewModel.UiState,
+ callbacks: MemberLabelEducationUiCallbacks = MemberLabelEducationUiCallbacks.Empty
+) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier
+ .padding(top = 4.dp, bottom = 28.dp, start = 28.dp, end = 28.dp)
+ .verticalScroll(rememberScrollState())
+ ) {
+ BottomSheets.Handle()
+
+ Image(
+ painter = painterResource(R.drawable.symbol_tag_filled_64),
+ contentDescription = null,
+ modifier = Modifier
+ .padding(top = 24.dp)
+ .size(64.dp)
+ )
+
+ Text(
+ text = stringResource(R.string.MemberLabelsEducation__title),
+ style = MaterialTheme.typography.titleLarge,
+ color = MaterialTheme.colorScheme.onSurface,
+ modifier = Modifier.padding(top = 16.dp)
+ )
+
+ Text(
+ text = stringResource(R.string.MemberLabelsEducation__body),
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.padding(top = 12.dp)
+ )
+
+ if (state.selfCanSetLabel) {
+ TextButton(
+ onClick = callbacks::onSetLabelClicked,
+ modifier = Modifier.padding(top = 56.dp)
+ ) {
+ Text(
+ text = stringResource(
+ if (state.selfHasLabel) R.string.MemberLabelsEducation__edit_label
+ else R.string.MemberLabelsEducation__set_label
+ )
+ )
+ }
+ } else {
+ Spacer(modifier = Modifier.height(56.dp))
+ }
+
+ Buttons.LargeTonal(
+ onClick = callbacks::onDismiss,
+ modifier = Modifier
+ .padding(top = 16.dp)
+ .defaultMinSize(minWidth = 220.dp)
+ ) {
+ Text(text = stringResource(android.R.string.ok))
+ }
+ }
+}
+
+private interface MemberLabelEducationUiCallbacks {
+ fun onSetLabelClicked()
+ fun onDismiss()
+
+ object Empty : MemberLabelEducationUiCallbacks {
+ override fun onSetLabelClicked() = Unit
+ override fun onDismiss() = Unit
+ }
+}
+
+@AllDevicePreviews
+@Composable
+private fun MemberLabelEducationSheetPreviewCanSetNoLabel() = Previews.Preview {
+ MemberLabelEducationSheetContent(
+ state = MemberLabelEducationViewModel.UiState(
+ selfHasLabel = false,
+ selfCanSetLabel = true
+ )
+ )
+}
+
+@DayNightPreviews
+@Composable
+private fun MemberLabelEducationSheetPreviewCanSetHasLabel() = Previews.Preview {
+ MemberLabelEducationSheetContent(
+ state = MemberLabelEducationViewModel.UiState(
+ selfHasLabel = true,
+ selfCanSetLabel = true
+ )
+ )
+}
+
+@DayNightPreviews
+@Composable
+private fun MemberLabelEducationSheetPreviewCannotSet() = Previews.Preview {
+ MemberLabelEducationSheetContent(
+ state = MemberLabelEducationViewModel.UiState(
+ selfHasLabel = false,
+ selfCanSetLabel = false
+ )
+ )
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelEducationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelEducationViewModel.kt
new file mode 100644
index 0000000000..34cdb4b760
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelEducationViewModel.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2026 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.thoughtcrime.securesms.groups.memberlabel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import org.signal.core.util.concurrent.SignalDispatchers
+import org.thoughtcrime.securesms.groups.GroupId
+import org.thoughtcrime.securesms.recipients.Recipient
+
+class MemberLabelEducationViewModel(
+ private val groupId: GroupId.V2,
+ private val repository: MemberLabelRepository = MemberLabelRepository.instance
+) : ViewModel() {
+
+ data class UiState(
+ val selfHasLabel: Boolean = false,
+ val selfCanSetLabel: Boolean = false
+ )
+
+ private val _uiState = MutableStateFlow(UiState())
+ val uiState: StateFlow = _uiState.asStateFlow()
+
+ init {
+ viewModelScope.launch(SignalDispatchers.IO) {
+ val self = Recipient.self()
+ val selfMemberLabel = repository.getLabel(groupId, self.id)
+ val selfCanSetLabel = repository.canSetLabel(groupId, self)
+ _uiState.update {
+ it.copy(
+ selfHasLabel = selfMemberLabel != null,
+ selfCanSetLabel = selfCanSetLabel
+ )
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelFragment.kt
index 07e6e8a452..36e7870e9d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelFragment.kt
@@ -38,8 +38,8 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
+import androidx.core.os.bundleOf
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.navigation.fragment.navArgs
import org.signal.core.ui.compose.AllDevicePreviews
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.ClearableTextField
@@ -48,6 +48,7 @@ import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalIcons
import org.signal.core.util.isNotNullOrBlank
+import org.signal.core.util.requireParcelableCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.memberlabel.MemberLabelUiState.SaveState
@@ -62,12 +63,22 @@ import org.thoughtcrime.securesms.util.viewModel
class MemberLabelFragment : ComposeFragment(), ReactWithAnyEmojiBottomSheetDialogFragment.Callback {
companion object {
private const val EMOJI_PICKER_DIALOG_TAG = "emoji_picker_dialog"
+ private const val ARG_GROUP_ID = "group_id"
+
+ fun newInstance(groupId: GroupId.V2): MemberLabelFragment {
+ return MemberLabelFragment().apply {
+ arguments = bundleOf(ARG_GROUP_ID to groupId)
+ }
+ }
+ }
+
+ private val groupId: GroupId.V2 by lazy {
+ requireArguments().requireParcelableCompat(ARG_GROUP_ID, GroupId.V2::class.java)
}
- private val args: MemberLabelFragmentArgs by navArgs()
private val viewModel: MemberLabelViewModel by viewModel {
MemberLabelViewModel(
- groupId = (args.groupId as GroupId).requireV2(),
+ groupId = groupId,
recipientId = Recipient.self().id
)
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/about/AboutSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/about/AboutSheet.kt
index 6d03593f29..21a74880dc 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/about/AboutSheet.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/about/AboutSheet.kt
@@ -40,8 +40,8 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.os.bundleOf
import androidx.core.widget.TextViewCompat
+import androidx.fragment.app.setFragmentResult
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.navigation.Navigation
import org.signal.core.ui.compose.BottomSheets
import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment
import org.signal.core.ui.compose.DayNightPreviews
@@ -53,7 +53,6 @@ import org.thoughtcrime.securesms.AvatarPreviewActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.avatar.AvatarImage
import org.thoughtcrime.securesms.components.emoji.EmojiTextView
-import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsFragmentDirections
import org.thoughtcrime.securesms.conversation.v2.UnverifiedProfileNameBottomSheet
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.memberlabel.MemberLabel
@@ -72,6 +71,9 @@ import org.signal.core.ui.R as CoreUiR
class AboutSheet : ComposeBottomSheetDialogFragment() {
companion object {
+ const val RESULT_EDIT_MEMBER_LABEL = "edit_member_label"
+ const val RESULT_GROUP_ID = "group_id"
+
private const val RECIPIENT_ID = "recipient_id"
private const val VIEWING_FROM_GROUP_ID = "viewing_from_group_id"
@@ -157,8 +159,7 @@ class AboutSheet : ComposeBottomSheetDialogFragment() {
private fun openMemberLabelScreen() {
viewingFromGroupId?.let { groupId ->
- val navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment)
- navController.navigate(ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToMemberLabelFragment(groupId))
+ setFragmentResult(RESULT_EDIT_MEMBER_LABEL, bundleOf(RESULT_GROUP_ID to groupId))
dismiss()
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.kt
index a4f5a7414a..23519c0051 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.kt
@@ -36,6 +36,7 @@ import org.thoughtcrime.securesms.components.settings.conversation.preferences.B
import org.thoughtcrime.securesms.conversation.v2.data.AvatarDownloadStateCache
import org.thoughtcrime.securesms.fonts.SignalSymbols
import org.thoughtcrime.securesms.groups.GroupId
+import org.thoughtcrime.securesms.groups.memberlabel.MemberLabelEducationSheet
import org.thoughtcrime.securesms.groups.memberlabel.MemberLabelPillView
import org.thoughtcrime.securesms.nicknames.NicknameActivity
import org.thoughtcrime.securesms.recipients.Recipient
@@ -334,7 +335,7 @@ class RecipientBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr
}
viewModel.recipientDetails.observe(viewLifecycleOwner) { state ->
- updateRecipientDetails(state, memberLabelView, aboutView)
+ updateRecipientDetails(state, memberLabelView, aboutView, groupId?.v2OrNull())
}
viewModel.canAddToAGroup.observe(getViewLifecycleOwner()) { canAdd: Boolean ->
@@ -450,13 +451,23 @@ class RecipientBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr
private fun updateRecipientDetails(
state: RecipientDetailsState,
memberLabelView: MemberLabelPillView,
- aboutView: TextView
+ aboutView: TextView,
+ groupId: GroupId.V2?
) {
when {
state.memberLabel != null -> {
memberLabelView.setLabel(state.memberLabel.label, state.memberLabel.tintColor)
memberLabelView.visible = true
aboutView.visible = false
+
+ if (groupId != null) {
+ memberLabelView.setOnClickListener {
+ dismiss()
+ MemberLabelEducationSheet.show(parentFragmentManager, groupId)
+ }
+ } else {
+ memberLabelView.setOnClickListener(null)
+ }
}
!state.aboutText.isNullOrBlank() -> {
diff --git a/app/src/main/res/drawable/symbol_tag_filled_64.xml b/app/src/main/res/drawable/symbol_tag_filled_64.xml
new file mode 100644
index 0000000000..a766031d91
--- /dev/null
+++ b/app/src/main/res/drawable/symbol_tag_filled_64.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a191db5655..e91a05e344 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -9385,5 +9385,14 @@
Clear label
Add a member label to describe yourself or your role in this group. Labels are only visible within this group.
+
+ Member labels
+
+ Use a member label to describe yourself or your role in this group. Member labels are only visible within this group.
+
+ Set a member label
+
+ Edit your label
+