From 0b2d3edcced1cd4f322ef9a36819707b27efa20f Mon Sep 17 00:00:00 2001 From: jeffrey-signal Date: Wed, 25 Feb 2026 17:24:31 -0500 Subject: [PATCH] Add member labels education sheet. --- app/src/main/AndroidManifest.xml | 5 + .../ConversationSettingsFragment.kt | 12 ++ .../conversation/v2/ConversationFragment.kt | 31 ++- .../thoughtcrime/securesms/groups/GroupId.kt | 4 + .../groups/memberlabel/MemberLabelActivity.kt | 40 ++++ .../memberlabel/MemberLabelEducationSheet.kt | 199 ++++++++++++++++++ .../MemberLabelEducationViewModel.kt | 45 ++++ .../groups/memberlabel/MemberLabelFragment.kt | 17 +- .../recipients/ui/about/AboutSheet.kt | 9 +- .../RecipientBottomSheetDialogFragment.kt | 15 +- .../res/drawable/symbol_tag_filled_64.xml | 9 + app/src/main/res/values/strings.xml | 9 + 12 files changed, 385 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelActivity.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelEducationSheet.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelEducationViewModel.kt create mode 100644 app/src/main/res/drawable/symbol_tag_filled_64.xml 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 +