Add feature flag driven group recommended size and hard size limits.

This commit is contained in:
Alan Evans 2020-10-22 14:52:05 -03:00 committed by Cody Henthorne
parent 5eace49739
commit b4b1e5b605
12 changed files with 246 additions and 54 deletions

View File

@ -21,6 +21,7 @@ import android.Manifest;
import android.animation.LayoutTransition;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.Bundle;
@ -29,7 +30,6 @@ 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;
@ -56,6 +56,7 @@ import com.google.android.material.chip.ChipGroup;
import com.pnikosis.materialishprogress.ProgressWheel;
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
import org.thoughtcrime.securesms.components.emoji.WarningTextView;
import org.thoughtcrime.securesms.contacts.ContactChip;
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
@ -63,6 +64,8 @@ import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.contacts.SelectedContact;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
@ -82,7 +85,6 @@ import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Set;
/**
@ -106,7 +108,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
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";
public static final String SELECTION_LIMITS = "selection_limits";
public static final String CURRENT_SELECTION = "current_selection";
private ConstraintLayout constraintLayout;
@ -123,15 +125,16 @@ public final class ContactSelectionListFragment extends LoggingFragment
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
private ChipGroup chipGroup;
private HorizontalScrollView chipGroupScrollContainer;
private TextView groupLimit;
private WarningTextView groupLimit;
@Nullable private FixedViewsAdapter headerAdapter;
@Nullable private FixedViewsAdapter footerAdapter;
@Nullable private ListCallback listCallback;
@Nullable private ScrollCallback scrollCallback;
private GlideRequests glideRequests;
private int selectionLimit;
private SelectionLimits selectionLimit = SelectionLimits.NO_LIMITS;
private Set<RecipientId> currentSelection;
private boolean isMulti;
@Override
public void onAttach(@NonNull Context context) {
@ -206,9 +209,25 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
});
swipeRefresh.setEnabled(requireActivity().getIntent().getBooleanExtra(REFRESHABLE, true));
Intent intent = requireActivity().getIntent();
swipeRefresh.setEnabled(intent.getBooleanExtra(REFRESHABLE, true));
selectionLimit = intent.getParcelableExtra(SELECTION_LIMITS);
isMulti = intent.getBooleanExtra(MULTI_SELECT, false);
if (isMulti) {
if (selectionLimit == null) {
throw new AssertionError("Selection limits not supplied in args for multi-select");
}
} else {
if (selectionLimit != null) {
throw new AssertionError("Selection limits supplied in args for a non-multi selection use");
} else {
selectionLimit = SelectionLimits.NO_LIMITS;
}
}
selectionLimit = requireActivity().getIntent().getIntExtra(SELECTION_LIMIT, NO_LIMIT);
currentSelection = getCurrentSelection();
updateGroupLimit(getChipCount());
@ -217,12 +236,10 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
private void updateGroupLimit(int chipCount) {
if (selectionLimit != NO_LIMIT) {
groupLimit.setText(String.format(Locale.getDefault(), "%d/%d", currentSelection.size() + chipCount, selectionLimit));
groupLimit.setVisibility(View.VISIBLE);
} else {
groupLimit.setVisibility(View.GONE);
}
int members = currentSelection.size() + chipCount;
groupLimit.setText(getResources().getQuantityString(R.plurals.ContactSelectionListFragment_d_members, members, members));
groupLimit.setVisibility(isMulti ? View.VISIBLE : View.GONE);
groupLimit.setWarning(selectionWarningLimitExceeded());
}
@Override
@ -254,7 +271,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
public boolean isMulti() {
return requireActivity().getIntent().getBooleanExtra(MULTI_SELECT, false);
return isMulti;
}
private void initializeCursor() {
@ -264,7 +281,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
glideRequests,
null,
new ListClickListener(),
isMulti(),
isMulti,
currentSelection);
RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader();
@ -450,15 +467,14 @@ public final class ContactSelectionListFragment extends LoggingFragment
SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orNull(), contact.getNumber())
: SelectedContact.forPhone(contact.getRecipientId().orNull(), contact.getNumber());
if (isMulti() && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) {
if (isMulti && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) {
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_you_do_not_need_to_add_yourself_to_the_group, Toast.LENGTH_SHORT).show();
return;
}
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();
if (!isMulti || !cursorRecyclerViewAdapter.isSelectedContact(selectedContact)) {
if (selectionHardLimitReached()) {
GroupLimitDialog.showHardLimitMessage(requireContext());
return;
}
@ -486,7 +502,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
new AlertDialog.Builder(requireContext())
.setTitle(R.string.ContactSelectionListFragment_username_not_found)
.setMessage(getString(R.string.ContactSelectionListFragment_s_is_not_a_signal_user, contact.getNumber()))
.setPositiveButton(R.string.ContactSelectionListFragment_okay, (dialog, which) -> dialog.dismiss())
.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss())
.show();
}
});
@ -508,16 +524,25 @@ public final class ContactSelectionListFragment extends LoggingFragment
if (onContactSelectedListener != null) {
onContactSelectedListener.onContactDeselected(contact.getRecipientId(), contact.getNumber());
}
}}
}
}
}
private boolean selectionLimitReached() {
return getChipCount() + currentSelection.size() >= selectionLimit;
private boolean selectionHardLimitReached() {
return getChipCount() + currentSelection.size() >= selectionLimit.getHardLimit();
}
private boolean selectionWarningLimitReachedExactly() {
return getChipCount() + currentSelection.size() == selectionLimit.getRecommendedLimit();
}
private boolean selectionWarningLimitExceeded() {
return getChipCount() + currentSelection.size() > selectionLimit.getRecommendedLimit();
}
private void markContactSelected(@NonNull SelectedContact selectedContact) {
cursorRecyclerViewAdapter.addSelectedContact(selectedContact);
if (isMulti()) {
if (isMulti) {
addChipForSelectedContact(selectedContact);
}
}
@ -588,6 +613,9 @@ public final class ContactSelectionListFragment extends LoggingFragment
private void addChip(@NonNull ContactChip chip) {
chipGroup.addView(chip);
updateGroupLimit(getChipCount());
if (selectionWarningLimitReachedExactly()) {
GroupLimitDialog.showRecommendedLimitMessage(requireContext());
}
}
private int getChipCount() {

View File

@ -0,0 +1,49 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import androidx.annotation.ColorInt;
import androidx.appcompat.widget.AppCompatTextView;
import org.thoughtcrime.securesms.R;
public final class WarningTextView extends AppCompatTextView {
@ColorInt private final int originalTextColor;
@ColorInt private final int warningTextColor;
private boolean warning;
public WarningTextView(Context context) {
this(context, null);
}
public WarningTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public WarningTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray styledAttributes = context.obtainStyledAttributes(attrs, R.styleable.WarningTextView, 0, 0);
warningTextColor = styledAttributes.getColor(R.styleable.WarningTextView_warning_text_color, 0);
styledAttributes.recycle();
styledAttributes = context.obtainStyledAttributes(attrs, new int[]{ android.R.attr.textColor });
originalTextColor = styledAttributes.getColor(0, 0);
styledAttributes.recycle();
}
public void setWarning(boolean warning) {
if (this.warning != warning) {
this.warning = warning;
setTextColor(warning ? warningTextColor : originalTextColor);
invalidate();
}
}
}

View File

@ -0,0 +1,65 @@
package org.thoughtcrime.securesms.groups;
import android.os.Parcel;
import android.os.Parcelable;
public final class SelectionLimits implements Parcelable {
public static final int NO_LIMIT = Integer.MAX_VALUE;
public static final SelectionLimits NO_LIMITS = new SelectionLimits(NO_LIMIT, NO_LIMIT);
private final int recommendedLimit;
private final int hardLimit;
public SelectionLimits(int recommendedLimit, int hardLimit) {
this.recommendedLimit = recommendedLimit;
this.hardLimit = hardLimit;
}
public int getRecommendedLimit() {
return recommendedLimit;
}
public int getHardLimit() {
return hardLimit;
}
public boolean hasRecommendedLimit() {
return recommendedLimit != NO_LIMIT;
}
public boolean hasHardLimit() {
return hardLimit != NO_LIMIT;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(recommendedLimit);
dest.writeInt(hardLimit);
}
public static final Creator<SelectionLimits> CREATOR = new Creator<SelectionLimits>() {
@Override
public SelectionLimits createFromParcel(Parcel in) {
return new SelectionLimits(in.readInt(), in.readInt());
}
@Override
public SelectionLimits[] newArray(int size) {
return new SelectionLimits[size];
}
};
public SelectionLimits excludingSelf() {
return excluding(1);
}
public SelectionLimits excluding(int count) {
return new SelectionLimits(recommendedLimit - count, hardLimit - count);
}
}

View File

@ -0,0 +1,28 @@
package org.thoughtcrime.securesms.groups.ui;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.FeatureFlags;
public final class GroupLimitDialog {
public static void showHardLimitMessage(@NonNull Context context) {
new AlertDialog.Builder(context)
.setTitle(R.string.ContactSelectionListFragment_maximum_group_size_reached)
.setMessage(context.getString(R.string.ContactSelectionListFragment_signal_groups_can_have_a_maximum_of_d_members, FeatureFlags.groupLimits().getHardLimit()))
.setPositiveButton(android.R.string.ok, null)
.show();
}
public static void showRecommendedLimitMessage(@NonNull Context context) {
new AlertDialog.Builder(context)
.setTitle(R.string.ContactSelectionListFragment_recommended_member_limit_reached)
.setMessage(context.getString(R.string.ContactSelectionListFragment_signal_groups_perform_best_with_d_members_or_fewer, FeatureFlags.groupLimits().getRecommendedLimit()))
.setPositiveButton(android.R.string.ok, null)
.show();
}
}

View File

@ -51,7 +51,6 @@ public final class AddToGroupsActivity extends ContactSelectionActivity {
intent.putExtra(EXTRA_RECIPIENT_ID, recipientId);
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, ContactsCursorLoader.DisplayMode.FLAG_ACTIVE_GROUPS);
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMIT, ContactSelectionListFragment.NO_LIMIT);
intent.putParcelableArrayListExtra(ContactSelectionListFragment.CURRENT_SELECTION, new ArrayList<>(currentGroupsMemberOf));

View File

@ -56,8 +56,7 @@ public class CreateGroupActivity extends ContactSelectionActivity {
: ContactsCursorLoader.DisplayMode.FLAG_PUSH;
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMIT, SignalStore.internalValues().gv2DoNotCreateGv2Groups() ? ContactSelectionListFragment.NO_LIMIT
: FeatureFlags.gv2GroupCapacity() - 1);
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMITS, FeatureFlags.groupLimits().excludingSelf());
return intent;
}

View File

@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.groups.GroupProtoUtil;
import org.thoughtcrime.securesms.groups.MembershipNotSuitableForV2Exception;
import org.thoughtcrime.securesms.groups.ui.GroupChangeErrorCallback;
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
@ -68,9 +69,9 @@ final class ManageGroupRepository {
members.addAll(pendingMembers);
return new GroupCapacityResult(members, FeatureFlags.gv2GroupCapacity());
return new GroupCapacityResult(members, FeatureFlags.groupLimits());
} else {
return new GroupCapacityResult(groupRecord.getMembers(), ContactSelectionListFragment.NO_LIMIT);
return new GroupCapacityResult(groupRecord.getMembers(), FeatureFlags.groupLimits());
}
}, onGroupCapacityLoaded::accept);
}
@ -186,33 +187,39 @@ final class ManageGroupRepository {
static final class GroupCapacityResult {
private final List<RecipientId> members;
private final int totalCapacity;
private final SelectionLimits selectionLimits;
GroupCapacityResult(@NonNull List<RecipientId> members, int totalCapacity) {
GroupCapacityResult(@NonNull List<RecipientId> members, @NonNull SelectionLimits selectionLimits) {
this.members = members;
this.totalCapacity = totalCapacity;
this.selectionLimits = selectionLimits;
}
public @NonNull List<RecipientId> getMembers() {
return members;
}
public int getTotalCapacity() {
return totalCapacity;
}
public int getSelectionLimit() {
if (totalCapacity == ContactSelectionListFragment.NO_LIMIT) {
return totalCapacity;
if (!selectionLimits.hasHardLimit()) {
return ContactSelectionListFragment.NO_LIMIT;
}
boolean containsSelf = members.indexOf(Recipient.self().getId()) != -1;
return totalCapacity - (containsSelf ? 1 : 0);
return selectionLimits.getHardLimit() - (containsSelf ? 1 : 0);
}
public int getSelectionWarning() {
if (!selectionLimits.hasRecommendedLimit()) {
return ContactSelectionListFragment.NO_LIMIT;
}
boolean containsSelf = members.indexOf(Recipient.self().getId()) != -1;
return selectionLimits.getRecommendedLimit() - (containsSelf ? 1 : 0);
}
public int getRemainingCapacity() {
return totalCapacity - members.size();
return selectionLimits.getHardLimit() - members.size();
}
public @NonNull ArrayList<RecipientId> getMembersWithoutSelf() {

View File

@ -27,9 +27,11 @@ import org.thoughtcrime.securesms.database.loaders.MediaLoader;
import org.thoughtcrime.securesms.database.loaders.ThreadMediaLoader;
import org.thoughtcrime.securesms.groups.GroupAccessControl;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.groups.LiveGroup;
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
import org.thoughtcrime.securesms.groups.ui.GroupErrors;
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog;
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
import org.thoughtcrime.securesms.groups.ui.addmembers.AddMembersActivity;
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupMentionSettingDialog;
@ -306,12 +308,12 @@ public class ManageGroupViewModel extends ViewModel {
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();
GroupLimitDialog.showHardLimitMessage(fragment.requireContext());
} else {
Intent intent = new Intent(fragment.requireActivity(), AddMembersActivity.class);
intent.putExtra(AddMembersActivity.GROUP_ID, manageGroupRepository.getGroupId().toString());
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, ContactsCursorLoader.DisplayMode.FLAG_PUSH);
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMIT, capacity.getSelectionLimit());
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMITS, new SelectionLimits(capacity.getSelectionWarning(), capacity.getSelectionLimit()));
intent.putParcelableArrayListExtra(ContactSelectionListFragment.CURRENT_SELECTION, capacity.getMembersWithoutSelf());
fragment.startActivityForResult(intent, resultCode);
}

View File

@ -12,6 +12,7 @@ import org.json.JSONException;
import org.json.JSONObject;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
@ -50,7 +51,8 @@ public final class FeatureFlags {
private static final String USERNAMES = "android.usernames";
private static final String GROUPS_V2_JOIN_VERSION = "android.groupsv2.joinVersion";
private static final String GROUPS_V2_LINKS_VERSION = "android.groupsv2.manageGroupLinksVersion";
private static final String GROUPS_V2_CAPACITY = "global.groupsv2.maxGroupSize";
private static final String GROUPS_V2_RECOMMENDED_LIMIT = "global.groupsv2.maxGroupSize";
private static final String GROUPS_V2_HARD_LIMIT = "global.groupsv2.groupSizeHardLimit";
private static final String INTERNAL_USER = "android.internalUser";
private static final String VERIFY_V2 = "android.verifyV2";
private static final String PHONE_NUMBER_PRIVACY_VERSION = "android.phoneNumberPrivacyVersion";
@ -64,7 +66,8 @@ public final class FeatureFlags {
*/
private static final Set<String> REMOTE_CAPABLE = Sets.newHashSet(
GROUPS_V2_CAPACITY,
GROUPS_V2_RECOMMENDED_LIMIT,
GROUPS_V2_HARD_LIMIT,
GROUPS_V2_JOIN_VERSION,
GROUPS_V2_LINKS_VERSION,
INTERNAL_USER,
@ -177,8 +180,9 @@ public final class FeatureFlags {
/**
* Maximum number of members allowed in a group.
*/
public static int gv2GroupCapacity() {
return getInteger(GROUPS_V2_CAPACITY, 151);
public static SelectionLimits groupLimits() {
return new SelectionLimits(getInteger(GROUPS_V2_RECOMMENDED_LIMIT, 151),
getInteger(GROUPS_V2_HARD_LIMIT, 1001));
}
/**

View File

@ -110,14 +110,15 @@
</LinearLayout>
<TextView
<org.thoughtcrime.securesms.components.emoji.WarningTextView
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_constraintStart_toStartOf="@+id/chipGroupScrollContainer"
app:layout_constraintTop_toTopOf="parent"
app:warning_text_color="@color/core_red"
tools:text="999/999"
tools:visibility="visible" />

View File

@ -588,4 +588,8 @@
<declare-styleable name="ConversationScrollToView">
<attr name="cstv_scroll_button_src" format="reference" />
</declare-styleable>
<declare-styleable name="WarningTextView">
<attr name="warning_text_color" format="color|reference" />
</declare-styleable>
</resources>

View File

@ -1711,9 +1711,15 @@
<string name="ContactSelectionListFragment_error_retrieving_contacts_check_your_network_connection">Error retrieving contacts, check your network connection</string>
<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>
<string name="ContactSelectionListFragment_you_do_not_need_to_add_yourself_to_the_group">You do not need to add yourself to the group</string>
<string name="ContactSelectionListFragment_maximum_group_size_reached">Maximum group size reached</string>
<string name="ContactSelectionListFragment_signal_groups_can_have_a_maximum_of_d_members">Signal groups can have a maximum of %1$d members.</string>
<string name="ContactSelectionListFragment_recommended_member_limit_reached">Recommended member limit reached</string>
<string name="ContactSelectionListFragment_signal_groups_perform_best_with_d_members_or_fewer">Signal groups perform best with %1$d members or fewer. Adding more members will cause delays sending and receiving messages.</string>
<plurals name="ContactSelectionListFragment_d_members">
<item quantity="one">%1$d member</item>
<item quantity="other">%1$d members</item>
</plurals>
<!-- blocked_contacts_fragment -->
<string name="blocked_contacts_fragment__no_blocked_contacts">No blocked contacts</string>