diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsEvents.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsEvents.kt index d0791fbe71..32307b024f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsEvents.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsEvents.kt @@ -9,4 +9,5 @@ sealed interface LabsSettingsEvents { data class ToggleIndividualChatPlaintextExport(val enabled: Boolean) : LabsSettingsEvents data class ToggleStoryArchive(val enabled: Boolean) : LabsSettingsEvents data class ToggleIncognito(val enabled: Boolean) : LabsSettingsEvents + data class ToggleGroupSuggestionsForMembers(val enabled: Boolean) : LabsSettingsEvents } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsFragment.kt index 1849fec00c..c734b43538 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsFragment.kt @@ -115,6 +115,15 @@ private fun LabsSettingsContent( onCheckChanged = { onEvent(LabsSettingsEvents.ToggleIncognito(it)) } ) } + + item { + Rows.ToggleRow( + checked = state.groupSuggestionsForMembers, + text = "Group Suggestions for Members", + label = "When creating a group, show existing groups that have the exact same members.", + onCheckChanged = { onEvent(LabsSettingsEvents.ToggleGroupSuggestionsForMembers(it)) } + ) + } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsState.kt index f89b2e3a0c..e52f274e96 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsState.kt @@ -11,5 +11,6 @@ import androidx.compose.runtime.Immutable data class LabsSettingsState( val individualChatPlaintextExport: Boolean = false, val storyArchive: Boolean = false, - val incognito: Boolean = false + val incognito: Boolean = false, + val groupSuggestionsForMembers: Boolean = false ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsViewModel.kt index 480806a5eb..3803a9effb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsViewModel.kt @@ -29,6 +29,10 @@ class LabsSettingsViewModel : ViewModel() { SignalStore.labs.incognito = event.enabled _state.value = _state.value.copy(incognito = event.enabled) } + is LabsSettingsEvents.ToggleGroupSuggestionsForMembers -> { + SignalStore.labs.groupSuggestionsForMembers = event.enabled + _state.value = _state.value.copy(groupSuggestionsForMembers = event.enabled) + } } } @@ -36,7 +40,8 @@ class LabsSettingsViewModel : ViewModel() { return LabsSettingsState( individualChatPlaintextExport = SignalStore.labs.individualChatPlaintextExport, storyArchive = SignalStore.labs.storyArchive, - incognito = SignalStore.labs.incognito + incognito = SignalStore.labs.incognito, + groupSuggestionsForMembers = SignalStore.labs.groupSuggestionsForMembers ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt index f582fc05b1..daef59f7c3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt @@ -506,6 +506,17 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : } } + @WorkerThread + fun getGroupsWithExactMembers(memberIds: Set): List { + if (memberIds.isEmpty()) return emptyList() + + val selfId = Recipient.self().id + val expectedMembers = if (selfId in memberIds) memberIds else memberIds + selfId + + return getGroupsContainingMember(selfId, pushOnly = true, includeInactive = false) + .filter { it.members.toSet() == expectedMembers } + } + fun getGroups(): Reader { val cursor = readableDatabase.query(joinedGroupSelect()) return Reader(cursor) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsFragment.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsFragment.java index 2b7fd2d2cb..4267cea2c1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsFragment.java @@ -29,12 +29,16 @@ import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; +import com.bumptech.glide.RequestManager; +import com.google.android.material.chip.ChipGroup; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import org.signal.core.util.EditTextUtil; import org.signal.core.ui.logging.LoggingFragment; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.avatar.picker.AvatarPickerFragment; +import org.thoughtcrime.securesms.contacts.ContactChip; +import org.thoughtcrime.securesms.conversation.ConversationIntents; import org.thoughtcrime.securesms.components.settings.app.privacy.expire.ExpireTimerSettingsFragment; import org.thoughtcrime.securesms.groups.ui.GroupMemberListView; import org.thoughtcrime.securesms.keyvalue.SignalStore; @@ -150,6 +154,29 @@ public class AddGroupDetailsFragment extends LoggingFragment { startActivityForResult(RecipientDisappearingMessagesActivity.forCreateGroup(requireContext(), viewModel.getDisappearingMessagesTimer().getValue()), REQUEST_DISAPPEARING_TIMER); }); + View sameGroupsSection = view.findViewById(R.id.same_groups_section); + ChipGroup sameGroupsChipGroup = view.findViewById(R.id.same_groups_chip_group); + + if (SignalStore.labs().getGroupSuggestionsForMembers()) { + viewModel.getSameGroups().observe(getViewLifecycleOwner(), groups -> { + sameGroupsChipGroup.removeAllViews(); + if (groups.isEmpty()) { + sameGroupsSection.setVisibility(View.GONE); + } else { + sameGroupsSection.setVisibility(View.VISIBLE); + RequestManager requestManager = Glide.with(this); + for (Recipient group : groups) { + ContactChip chip = new ContactChip(requireContext()); + chip.setText(group.getDisplayName(requireContext())); + chip.setAvatar(requestManager, group, null); + chip.setCloseIconVisible(false); + chip.setOnClickListener(v -> navigateToConversation(group.getId())); + sameGroupsChipGroup.addView(chip); + } + } + }); + } + name.requestFocus(); getParentFragmentManager().setFragmentResultListener(AvatarPickerFragment.REQUEST_KEY_SELECT_AVATAR, @@ -266,6 +293,14 @@ public class AddGroupDetailsFragment extends LoggingFragment { create.setEnabled(isEnabled); } + private void navigateToConversation(@NonNull RecipientId groupRecipientId) { + ConversationIntents.createBuilder(requireContext(), groupRecipientId, -1L) + .subscribe(builder -> { + startActivity(builder.build()); + requireActivity().finish(); + }); + } + private void showAvatarPicker() { Media media = viewModel.getAvatarMedia(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsRepository.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsRepository.java index b54ba3b6e1..011afa1b8e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsRepository.java @@ -8,6 +8,8 @@ import androidx.core.util.Consumer; import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.database.SignalDatabase; +import org.thoughtcrime.securesms.database.model.GroupRecord; import org.thoughtcrime.securesms.groups.GroupChangeBusyException; import org.thoughtcrime.securesms.groups.GroupChangeException; import org.thoughtcrime.securesms.groups.GroupManager; @@ -44,6 +46,19 @@ final class AddGroupDetailsRepository { }); } + void getGroupsWithSameMembers(@NonNull Set memberIds, Consumer> consumer) { + SignalExecutors.BOUNDED.execute(() -> { + List groups = SignalDatabase.groups().getGroupsWithExactMembers(memberIds); + List recipients = new ArrayList<>(groups.size()); + + for (GroupRecord group : groups) { + recipients.add(Recipient.resolved(group.getRecipientId())); + } + + consumer.accept(recipients); + }); + } + void createGroup(@NonNull Set members, @Nullable byte[] avatar, @Nullable String name, diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsViewModel.java index 7e79c0f766..bbc41aadcf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsViewModel.java @@ -16,6 +16,7 @@ import com.annimon.stream.Stream; import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.signal.core.models.media.Media; +import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.DefaultValueLiveData; import org.thoughtcrime.securesms.util.SingleLiveEvent; @@ -38,6 +39,7 @@ public final class AddGroupDetailsViewModel extends ViewModel { private final MutableLiveData disappearingMessagesTimer = new MutableLiveData<>(SignalStore.settings().getUniversalExpireTimer()); private final LiveData isMms; private final LiveData canSubmitForm; + private final LiveData> sameGroups; private final AddGroupDetailsRepository repository; private Media avatarMedia; @@ -53,6 +55,16 @@ public final class AddGroupDetailsViewModel extends ViewModel { members = LiveDataUtil.combineLatest(initialMembers, deleted, AddGroupDetailsViewModel::filterDeletedMembers); isMms = Transformations.map(members, AddGroupDetailsViewModel::isAnyForcedSms); canSubmitForm = LiveDataUtil.combineLatest(isMms, isValidName, (mms, validName) -> mms || validName); + sameGroups = Transformations.switchMap(members, memberList -> { + MutableLiveData> result = new MutableLiveData<>(Collections.emptyList()); + if (!memberList.isEmpty()) { + Set memberIds = Stream.of(memberList) + .map(member -> member.getMember().getId()) + .collect(Collectors.toSet()); + repository.getGroupsWithSameMembers(memberIds, result::postValue); + } + return result; + }); repository.resolveMembers(recipientIds, initialMembers::postValue); } @@ -77,6 +89,10 @@ public final class AddGroupDetailsViewModel extends ViewModel { return isMms; } + @NonNull LiveData> getSameGroups() { + return sameGroups; + } + @NonNull LiveData getDisappearingMessagesTimer() { return disappearingMessagesTimer; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/LabsValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/LabsValues.kt index 49870d2588..94bcb396bd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/LabsValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/LabsValues.kt @@ -7,6 +7,7 @@ class LabsValues internal constructor(store: KeyValueStore) : SignalStoreValues( const val INDIVIDUAL_CHAT_PLAINTEXT_EXPORT: String = "labs.individual_chat_plaintext_export" const val STORY_ARCHIVE: String = "labs.story_archive" const val INCOGNITO: String = "labs.incognito" + const val GROUP_SUGGESTIONS_FOR_MEMBERS: String = "labs.group_suggestions_for_members" } public override fun onFirstEverAppLaunch() = Unit @@ -19,6 +20,8 @@ class LabsValues internal constructor(store: KeyValueStore) : SignalStoreValues( var incognito by booleanValue(INCOGNITO, true).falseForExternalUsers() + var groupSuggestionsForMembers by booleanValue(GROUP_SUGGESTIONS_FOR_MEMBERS, true).falseForExternalUsers() + private fun SignalStoreValueDelegate.falseForExternalUsers(): SignalStoreValueDelegate { return this.map { actualValue -> RemoteConfig.internalUser && actualValue } } diff --git a/app/src/main/res/layout/add_group_details_fragment.xml b/app/src/main/res/layout/add_group_details_fragment.xml index b249b61b21..ba211dd714 100644 --- a/app/src/main/res/layout/add_group_details_fragment.xml +++ b/app/src/main/res/layout/add_group_details_fragment.xml @@ -98,7 +98,7 @@ android:layout_marginEnd="@dimen/dsl_settings_gutter" android:orientation="vertical" android:visibility="gone" - app:layout_constraintBottom_toTopOf="@id/member_list_header" + app:layout_constraintBottom_toTopOf="@id/same_groups_section" app:layout_constraintTop_toBottomOf="@id/divider" app:layout_goneMarginTop="23dp" tools:visibility="visible"> @@ -118,6 +118,43 @@ + + + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/same_groups_section" /> In this group, your Member Label will be displayed beside your photo in place of your About. + + Groups with same members (Internal Only) +