mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-02 00:17:41 +01:00
Show groups that have the same member list during group creation.
This commit is contained in:
committed by
Cody Henthorne
parent
f09bf5b14c
commit
9de75b3e1f
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,6 +506,17 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) :
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun getGroupsWithExactMembers(memberIds: Set<RecipientId>): List<GroupRecord> {
|
||||
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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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<RecipientId> memberIds, Consumer<List<Recipient>> consumer) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
List<GroupRecord> groups = SignalDatabase.groups().getGroupsWithExactMembers(memberIds);
|
||||
List<Recipient> recipients = new ArrayList<>(groups.size());
|
||||
|
||||
for (GroupRecord group : groups) {
|
||||
recipients.add(Recipient.resolved(group.getRecipientId()));
|
||||
}
|
||||
|
||||
consumer.accept(recipients);
|
||||
});
|
||||
}
|
||||
|
||||
void createGroup(@NonNull Set<RecipientId> members,
|
||||
@Nullable byte[] avatar,
|
||||
@Nullable String name,
|
||||
|
||||
@@ -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<Integer> disappearingMessagesTimer = new MutableLiveData<>(SignalStore.settings().getUniversalExpireTimer());
|
||||
private final LiveData<Boolean> isMms;
|
||||
private final LiveData<Boolean> canSubmitForm;
|
||||
private final LiveData<List<Recipient>> 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<List<Recipient>> result = new MutableLiveData<>(Collections.emptyList());
|
||||
if (!memberList.isEmpty()) {
|
||||
Set<RecipientId> 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<List<Recipient>> getSameGroups() {
|
||||
return sameGroups;
|
||||
}
|
||||
|
||||
@NonNull LiveData<Integer> getDisappearingMessagesTimer() {
|
||||
return disappearingMessagesTimer;
|
||||
}
|
||||
|
||||
@@ -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<Boolean>.falseForExternalUsers(): SignalStoreValueDelegate<Boolean> {
|
||||
return this.map { actualValue -> RemoteConfig.internalUser && actualValue }
|
||||
}
|
||||
|
||||
@@ -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 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/same_groups_section"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/mms_warning"
|
||||
tools:visibility="visible">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="40dp"
|
||||
android:paddingHorizontal="@dimen/dsl_settings_gutter"
|
||||
android:text="@string/AddGroupDetailsFragment__groups_with_same_members"
|
||||
android:textAppearance="@style/Signal.Text.TitleSmall" />
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:paddingHorizontal="@dimen/dsl_settings_gutter"
|
||||
android:paddingBottom="8dp"
|
||||
android:scrollbars="none">
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/same_groups_chip_group"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:singleLine="true"
|
||||
app:chipSpacingHorizontal="8dp" />
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/member_list_header"
|
||||
android:layout_width="match_parent"
|
||||
@@ -128,7 +165,7 @@
|
||||
android:minHeight="52sp"
|
||||
android:text="@string/AddGroupDetailsFragment__members"
|
||||
android:textAppearance="@style/Signal.Text.TitleSmall"
|
||||
app:layout_constraintTop_toBottomOf="@id/mms_warning" />
|
||||
app:layout_constraintTop_toBottomOf="@id/same_groups_section" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/add_later"
|
||||
|
||||
@@ -9615,5 +9615,8 @@
|
||||
<!-- Body for screen shown to let the user know that displaying their member label will take priority over their about text. -->
|
||||
<string name="MemberLabelsAboutOverride__body">In this group, your Member Label will be displayed beside your photo in place of your About.</string>
|
||||
|
||||
<!-- Label for internal-only section showing groups with same members -->
|
||||
<string name="AddGroupDetailsFragment__groups_with_same_members" translatable="false">Groups with same members (Internal Only)</string>
|
||||
|
||||
<!-- EOF -->
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user