Enforce a local GV2 capacity limit driven by a feature flag.
parent
cfcd451db7
commit
9da309ca48
|
@ -29,6 +29,7 @@ import android.view.LayoutInflater;
|
|||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.view.animation.CycleInterpolator;
|
||||
import android.widget.Button;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.TextView;
|
||||
|
@ -67,7 +68,6 @@ import org.thoughtcrime.securesms.permissions.Permissions;
|
|||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.Debouncer;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
@ -81,6 +81,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
|||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Fragment for selecting a one or more contacts from a list.
|
||||
|
@ -94,13 +95,16 @@ public final class ContactSelectionListFragment extends Fragment
|
|||
@SuppressWarnings("unused")
|
||||
private static final String TAG = Log.tag(ContactSelectionListFragment.class);
|
||||
|
||||
private static final int CHIP_GROUP_EMPTY_COUNT = 1;
|
||||
private static final int CHIP_GROUP_EMPTY_CHILD_COUNT = 1;
|
||||
private static final int CHIP_GROUP_REVEAL_DURATION_MS = 150;
|
||||
|
||||
public static final String DISPLAY_MODE = "display_mode";
|
||||
public static final String MULTI_SELECT = "multi_select";
|
||||
public static final String REFRESHABLE = "refreshable";
|
||||
public static final String RECENTS = "recents";
|
||||
public static final int NO_LIMIT = Integer.MAX_VALUE;
|
||||
|
||||
public static final String DISPLAY_MODE = "display_mode";
|
||||
public static final String MULTI_SELECT = "multi_select";
|
||||
public static final String REFRESHABLE = "refreshable";
|
||||
public static final String RECENTS = "recents";
|
||||
public static final String SELECTION_LIMIT = "selection_limit";
|
||||
|
||||
private ConstraintLayout constraintLayout;
|
||||
private TextView emptyText;
|
||||
|
@ -116,12 +120,14 @@ public final class ContactSelectionListFragment extends Fragment
|
|||
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
|
||||
private ChipGroup chipGroup;
|
||||
private HorizontalScrollView chipGroupScrollContainer;
|
||||
private TextView groupLimit;
|
||||
|
||||
@Nullable private FixedViewsAdapter headerAdapter;
|
||||
@Nullable private FixedViewsAdapter footerAdapter;
|
||||
@Nullable private ListCallback listCallback;
|
||||
@Nullable private ScrollCallback scrollCallback;
|
||||
private GlideRequests glideRequests;
|
||||
private int selectionLimit;
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
|
@ -185,15 +191,29 @@ public final class ContactSelectionListFragment extends Fragment
|
|||
showContactsProgress = view.findViewById(R.id.progress);
|
||||
chipGroup = view.findViewById(R.id.chipGroup);
|
||||
chipGroupScrollContainer = view.findViewById(R.id.chipGroupScrollContainer);
|
||||
groupLimit = view.findViewById(R.id.group_limit);
|
||||
constraintLayout = view.findViewById(R.id.container);
|
||||
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
|
||||
swipeRefresh.setEnabled(requireActivity().getIntent().getBooleanExtra(REFRESHABLE, true));
|
||||
|
||||
selectionLimit = requireActivity().getIntent().getIntExtra(SELECTION_LIMIT, NO_LIMIT);
|
||||
|
||||
updateGroupLimit(getChipCount());
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private void updateGroupLimit(int childCount) {
|
||||
if (selectionLimit != NO_LIMIT) {
|
||||
groupLimit.setText(String.format(Locale.getDefault(), "%d/%d", childCount, selectionLimit));
|
||||
groupLimit.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
groupLimit.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
|
@ -408,6 +428,12 @@ public final class ContactSelectionListFragment extends Fragment
|
|||
: SelectedContact.forPhone(contact.getRecipientId().orNull(), contact.getNumber());
|
||||
|
||||
if (!isMulti() || !cursorRecyclerViewAdapter.isSelectedContact(selectedContact)) {
|
||||
if (selectionLimitReached()) {
|
||||
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_the_group_is_full, Toast.LENGTH_SHORT).show();
|
||||
groupLimit.animate().scaleX(1.3f).scaleY(1.3f).setInterpolator(new CycleInterpolator(0.5f)).start();
|
||||
return;
|
||||
}
|
||||
|
||||
if (contact.isUsernameType()) {
|
||||
AlertDialog loadingDialog = SimpleProgressDialog.show(requireContext());
|
||||
|
||||
|
@ -447,6 +473,10 @@ public final class ContactSelectionListFragment extends Fragment
|
|||
}}
|
||||
}
|
||||
|
||||
private boolean selectionLimitReached() {
|
||||
return getChipCount() >= selectionLimit;
|
||||
}
|
||||
|
||||
private void markContactSelected(@NonNull SelectedContact selectedContact, @NonNull ContactSelectionListItem listItem) {
|
||||
cursorRecyclerViewAdapter.addSelectedContact(selectedContact);
|
||||
listItem.setChecked(true);
|
||||
|
@ -469,7 +499,9 @@ public final class ContactSelectionListFragment extends Fragment
|
|||
}
|
||||
}
|
||||
|
||||
if (chipGroup.getChildCount() == CHIP_GROUP_EMPTY_COUNT) {
|
||||
updateGroupLimit(getChipCount());
|
||||
|
||||
if (getChipCount() == 0) {
|
||||
setChipGroupVisibility(ConstraintSet.GONE);
|
||||
}
|
||||
}
|
||||
|
@ -477,7 +509,7 @@ public final class ContactSelectionListFragment extends Fragment
|
|||
private void addChipForContact(@NonNull ContactSelectionListItem contact, @NonNull SelectedContact selectedContact) {
|
||||
final ContactChip chip = new ContactChip(requireContext());
|
||||
|
||||
if (chipGroup.getChildCount() == CHIP_GROUP_EMPTY_COUNT) {
|
||||
if (getChipCount() == 0) {
|
||||
setChipGroupVisibility(ConstraintSet.VISIBLE);
|
||||
}
|
||||
|
||||
|
@ -503,12 +535,23 @@ public final class ContactSelectionListFragment extends Fragment
|
|||
|
||||
LiveRecipient recipient = contact.getRecipient();
|
||||
if (recipient != null) {
|
||||
chip.setAvatar(glideRequests, recipient.get(), () -> chipGroup.addView(chip));
|
||||
chip.setAvatar(glideRequests, recipient.get(), () -> addChip(chip));
|
||||
} else {
|
||||
chipGroup.addView(chip);
|
||||
addChip(chip);
|
||||
}
|
||||
}
|
||||
|
||||
private void addChip(@NonNull ContactChip chip) {
|
||||
chipGroup.addView(chip);
|
||||
updateGroupLimit(getChipCount());
|
||||
}
|
||||
|
||||
private int getChipCount() {
|
||||
int count = chipGroup.getChildCount() - CHIP_GROUP_EMPTY_CHILD_COUNT;
|
||||
if (count < 0) throw new AssertionError();
|
||||
return count;
|
||||
}
|
||||
|
||||
private void registerChipRecipientObserver(@NonNull ContactChip chip, @Nullable LiveRecipient recipient) {
|
||||
if (recipient != null) {
|
||||
recipient.observe(getViewLifecycleOwner(), resolved -> {
|
||||
|
|
|
@ -44,6 +44,7 @@ public class CreateGroupActivity extends ContactSelectionActivity {
|
|||
: ContactsCursorLoader.DisplayMode.FLAG_PUSH;
|
||||
|
||||
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
|
||||
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMIT, FeatureFlags.gv2GroupCapacity() - 1);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
|
|
@ -23,14 +23,12 @@ import androidx.fragment.app.FragmentActivity;
|
|||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import org.thoughtcrime.securesms.AvatarPreviewActivity;
|
||||
import org.thoughtcrime.securesms.ContactSelectionListFragment;
|
||||
import org.thoughtcrime.securesms.MediaPreviewActivity;
|
||||
import org.thoughtcrime.securesms.MuteDialog;
|
||||
import org.thoughtcrime.securesms.PushContactSelectionActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.components.ThreadPhotoRailView;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
|
@ -225,11 +223,7 @@ public class ManageGroupFragment extends Fragment {
|
|||
disappearingMessagesRow.setOnClickListener(v -> viewModel.handleExpirationSelection());
|
||||
blockGroup.setOnClickListener(v -> viewModel.blockAndLeave(requireActivity()));
|
||||
|
||||
addMembers.setOnClickListener(v -> {
|
||||
Intent intent = new Intent(requireActivity(), PushContactSelectionActivity.class);
|
||||
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, ContactsCursorLoader.DisplayMode.FLAG_PUSH);
|
||||
startActivityForResult(intent, PICK_CONTACT);
|
||||
});
|
||||
addMembers.setOnClickListener(v -> viewModel.onAddMembersClick(this, PICK_CONTACT));
|
||||
|
||||
viewModel.getMembershipRights().observe(getViewLifecycleOwner(), r -> {
|
||||
if (r != null) {
|
||||
|
|
|
@ -6,7 +6,10 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.WorkerThread;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.thoughtcrime.securesms.ContactSelectionListFragment;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.groups.GroupAccessControl;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
|
||||
|
@ -21,6 +24,7 @@ import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
|
|||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
|
||||
|
@ -47,6 +51,18 @@ final class ManageGroupRepository {
|
|||
SignalExecutors.BOUNDED.execute(() -> onGroupStateLoaded.accept(getGroupState()));
|
||||
}
|
||||
|
||||
void getGroupCapacity(@NonNull Consumer<GroupCapacityResult> onGroupCapacityLoaded) {
|
||||
SimpleTask.run(SignalExecutors.BOUNDED, () -> {
|
||||
GroupDatabase.GroupRecord groupRecord = DatabaseFactory.getGroupDatabase(context).getGroup(groupId).get();
|
||||
if (groupRecord.isV2Group()) {
|
||||
DecryptedGroup decryptedGroup = groupRecord.requireV2GroupProperties().getDecryptedGroup();
|
||||
return new GroupCapacityResult(decryptedGroup.getMembersCount(), decryptedGroup.getPendingMembersCount(), FeatureFlags.gv2GroupCapacity());
|
||||
} else {
|
||||
return new GroupCapacityResult(groupRecord.getMembers().size(), 0, ContactSelectionListFragment.NO_LIMIT);
|
||||
}
|
||||
}, onGroupCapacityLoaded::accept);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private GroupStateResult getGroupState() {
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
|
@ -152,4 +168,20 @@ final class ManageGroupRepository {
|
|||
}
|
||||
}
|
||||
|
||||
static final class GroupCapacityResult {
|
||||
private final int fullMembers;
|
||||
private final int pendingMembers;
|
||||
private final int totalCapacity;
|
||||
|
||||
GroupCapacityResult(int fullMembers, int pendingMembers, int totalCapacity) {
|
||||
this.fullMembers = fullMembers;
|
||||
this.pendingMembers = pendingMembers;
|
||||
this.totalCapacity = totalCapacity;
|
||||
}
|
||||
|
||||
public int getRemainingCapacity() {
|
||||
return totalCapacity - fullMembers - pendingMembers;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package org.thoughtcrime.securesms.groups.ui.managegroup;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
@ -14,7 +16,11 @@ import androidx.lifecycle.ViewModel;
|
|||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.BlockUnblockDialog;
|
||||
import org.thoughtcrime.securesms.ContactSelectionListFragment;
|
||||
import org.thoughtcrime.securesms.ExpirationDialog;
|
||||
import org.thoughtcrime.securesms.PushContactSelectionActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.database.loaders.MediaLoader;
|
||||
import org.thoughtcrime.securesms.database.loaders.ThreadMediaLoader;
|
||||
|
@ -211,6 +217,20 @@ public class ManageGroupViewModel extends ViewModel {
|
|||
Util.runOnMain(() -> Toast.makeText(context, GroupErrors.getUserDisplayMessage(e), Toast.LENGTH_LONG).show());
|
||||
}
|
||||
|
||||
public void onAddMembersClick(@NonNull Fragment fragment, int resultCode) {
|
||||
manageGroupRepository.getGroupCapacity(capacity -> {
|
||||
int remainingCapacity = capacity.getRemainingCapacity();
|
||||
if (remainingCapacity <= 0) {
|
||||
Toast.makeText(fragment.requireContext(), R.string.ContactSelectionListFragment_the_group_is_full, Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Intent intent = new Intent(fragment.requireActivity(), PushContactSelectionActivity.class);
|
||||
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, ContactsCursorLoader.DisplayMode.FLAG_PUSH);
|
||||
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMIT, remainingCapacity);
|
||||
fragment.startActivityForResult(intent, resultCode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static final class GroupViewState {
|
||||
private final long threadId;
|
||||
@NonNull private final Recipient groupRecipient;
|
||||
|
|
|
@ -65,6 +65,7 @@ public final class FeatureFlags {
|
|||
private static final String VERSIONED_PROFILES = "android.versionedProfiles";
|
||||
private static final String GROUPS_V2 = "android.groupsv2";
|
||||
private static final String GROUPS_V2_CREATE = "android.groupsv2.create";
|
||||
private static final String GROUPS_V2_CAPACITY = "android.groupsv2.capacity";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
|
@ -87,6 +88,7 @@ public final class FeatureFlags {
|
|||
VERSIONED_PROFILES,
|
||||
GROUPS_V2,
|
||||
GROUPS_V2_CREATE,
|
||||
GROUPS_V2_CAPACITY,
|
||||
NEW_GROUP_UI
|
||||
);
|
||||
|
||||
|
@ -283,6 +285,13 @@ public final class FeatureFlags {
|
|||
return groupsV2() && getBoolean(GROUPS_V2_CREATE, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum number of members allowed in a group.
|
||||
*/
|
||||
public static int gv2GroupCapacity() {
|
||||
return getInteger(GROUPS_V2_CAPACITY, 100);
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
|
|
|
@ -110,6 +110,17 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/group_limit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="@+id/chipGroupScrollContainer"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="999/999"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/chipGroupScrollContainer"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -122,7 +133,7 @@
|
|||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/group_limit"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
|
|
|
@ -1372,6 +1372,7 @@
|
|||
<string name="ContactSelectionListFragment_username_not_found">Username not found</string>
|
||||
<string name="ContactSelectionListFragment_s_is_not_a_signal_user">"%1$s" is not a Signal user. Please check the username and try again.</string>
|
||||
<string name="ContactSelectionListFragment_okay">Okay</string>
|
||||
<string name="ContactSelectionListFragment_the_group_is_full">The group is full</string>
|
||||
|
||||
<!-- blocked_contacts_fragment -->
|
||||
<string name="blocked_contacts_fragment__no_blocked_contacts">No blocked contacts</string>
|
||||
|
|
Loading…
Reference in New Issue