Fix chip jank and other groups v2 ux issues.

This commit is contained in:
Alex Hart 2020-05-27 12:19:20 -03:00 committed by Greyson Parrelli
parent 00996f0d7a
commit 903c3989b9
10 changed files with 120 additions and 151 deletions

View file

@ -3,12 +3,21 @@ package org.thoughtcrime.securesms;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.ThemeUtil;
public class ClearProfileAvatarActivity extends Activity { public class ClearProfileAvatarActivity extends Activity {
private static final String ARG_TITLE = "arg_title"; private static final String ARG_TITLE = "arg_title";
private final DynamicTheme theme = new DynamicNoActionBarTheme();
public static Intent createForUserProfilePhoto() { public static Intent createForUserProfilePhoto() {
return new Intent("org.thoughtcrime.securesms.action.CLEAR_PROFILE_PHOTO"); return new Intent("org.thoughtcrime.securesms.action.CLEAR_PROFILE_PHOTO");
} }
@ -19,14 +28,23 @@ public class ClearProfileAvatarActivity extends Activity {
return intent; return intent;
} }
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
theme.onCreate(this);
}
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
theme.onResume(this);
int titleId = getIntent().getIntExtra(ARG_TITLE, R.string.ClearProfileActivity_remove_profile_photo); int titleId = getIntent().getIntExtra(ARG_TITLE, R.string.ClearProfileActivity_remove_profile_photo);
new AlertDialog.Builder(this) new AlertDialog.Builder(this)
.setTitle(titleId) .setMessage(titleId)
.setNegativeButton(android.R.string.cancel, (dialog, which) -> finish()) .setNegativeButton(android.R.string.cancel, (dialog, which) -> finish())
.setPositiveButton(R.string.ClearProfileActivity_remove, (dialog, which) -> { .setPositiveButton(R.string.ClearProfileActivity_remove, (dialog, which) -> {
Intent result = new Intent(); Intent result = new Intent();

View file

@ -18,6 +18,7 @@ package org.thoughtcrime.securesms;
import android.Manifest; import android.Manifest;
import android.animation.LayoutTransition;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
@ -36,6 +37,8 @@ import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import androidx.loader.app.LoaderManager; import androidx.loader.app.LoaderManager;
@ -43,6 +46,8 @@ import androidx.loader.content.Loader;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.transition.AutoTransition;
import androidx.transition.TransitionManager;
import com.google.android.material.chip.ChipGroup; import com.google.android.material.chip.ChipGroup;
import com.pnikosis.materialishprogress.ProgressWheel; import com.pnikosis.materialishprogress.ProgressWheel;
@ -89,13 +94,15 @@ public final class ContactSelectionListFragment extends Fragment
@SuppressWarnings("unused") @SuppressWarnings("unused")
private static final String TAG = Log.tag(ContactSelectionListFragment.class); private static final String TAG = Log.tag(ContactSelectionListFragment.class);
private static final int CHIP_GROUP_EMPTY_COUNT = 1;
private static final int CHIP_GROUP_REVEAL_DURATION_MS = 150;
public static final String DISPLAY_MODE = "display_mode"; public static final String DISPLAY_MODE = "display_mode";
public static final String MULTI_SELECT = "multi_select"; public static final String MULTI_SELECT = "multi_select";
public static final String REFRESHABLE = "refreshable"; public static final String REFRESHABLE = "refreshable";
public static final String RECENTS = "recents"; public static final String RECENTS = "recents";
private final Debouncer scrollDebounce = new Debouncer(100); private ConstraintLayout constraintLayout;
private TextView emptyText; private TextView emptyText;
private OnContactSelectedListener onContactSelectedListener; private OnContactSelectedListener onContactSelectedListener;
private SwipeRefreshLayout swipeRefresh; private SwipeRefreshLayout swipeRefresh;
@ -178,13 +185,12 @@ public final class ContactSelectionListFragment extends Fragment
showContactsProgress = view.findViewById(R.id.progress); showContactsProgress = view.findViewById(R.id.progress);
chipGroup = view.findViewById(R.id.chipGroup); chipGroup = view.findViewById(R.id.chipGroup);
chipGroupScrollContainer = view.findViewById(R.id.chipGroupScrollContainer); chipGroupScrollContainer = view.findViewById(R.id.chipGroupScrollContainer);
constraintLayout = view.findViewById(R.id.container);
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
swipeRefresh.setEnabled(requireActivity().getIntent().getBooleanExtra(REFRESHABLE, true)); swipeRefresh.setEnabled(requireActivity().getIntent().getBooleanExtra(REFRESHABLE, true));
autoScrollOnNewItem();
return view; return view;
} }
@ -445,7 +451,7 @@ public final class ContactSelectionListFragment extends Fragment
cursorRecyclerViewAdapter.addSelectedContact(selectedContact); cursorRecyclerViewAdapter.addSelectedContact(selectedContact);
listItem.setChecked(true); listItem.setChecked(true);
if (isMulti() && FeatureFlags.newGroupUI()) { if (isMulti() && FeatureFlags.newGroupUI()) {
chipGroup.addView(newChipForContact(listItem, selectedContact)); addChipForContact(listItem, selectedContact);
} }
} }
@ -462,28 +468,65 @@ public final class ContactSelectionListFragment extends Fragment
chipGroup.removeView(v); chipGroup.removeView(v);
} }
} }
if (chipGroup.getChildCount() == CHIP_GROUP_EMPTY_COUNT) {
setChipGroupVisibility(ConstraintSet.GONE);
}
} }
private View newChipForContact(@NonNull ContactSelectionListItem contact, @NonNull SelectedContact selectedContact) { private void addChipForContact(@NonNull ContactSelectionListItem contact, @NonNull SelectedContact selectedContact) {
final ContactChip chip = new ContactChip(requireContext()); final ContactChip chip = new ContactChip(requireContext());
if (chipGroup.getChildCount() == CHIP_GROUP_EMPTY_COUNT) {
setChipGroupVisibility(ConstraintSet.VISIBLE);
}
chip.setText(contact.getChipName()); chip.setText(contact.getChipName());
chip.setContact(selectedContact); chip.setContact(selectedContact);
chip.setCloseIconVisible(true);
chip.setOnCloseIconClickListener(view -> markContactUnselected(selectedContact, contact));
chipGroup.getLayoutTransition().addTransitionListener(new LayoutTransition.TransitionListener() {
@Override
public void startTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
}
@Override
public void endTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
if (view == chip && transitionType == LayoutTransition.APPEARING) {
chipGroup.getLayoutTransition().removeTransitionListener(this);
registerChipRecipientObserver(chip, contact.getRecipient());
chipGroup.post(ContactSelectionListFragment.this::smoothScrollChipsToEnd);
}
}
});
LiveRecipient recipient = contact.getRecipient(); LiveRecipient recipient = contact.getRecipient();
if (recipient != null) { if (recipient != null) {
recipient.observe(getViewLifecycleOwner(), resolved -> { chip.setAvatar(glideRequests, recipient.get(), () -> chipGroup.addView(chip));
chip.setAvatar(glideRequests, resolved); } else {
chip.setText(resolved.getShortDisplayName(chip.getContext())); chipGroup.addView(chip);
} }
);
} }
chip.setCloseIconVisible(true); private void registerChipRecipientObserver(@NonNull ContactChip chip, @Nullable LiveRecipient recipient) {
chip.setOnCloseIconClickListener(view -> { if (recipient != null) {
markContactUnselected(selectedContact, contact); recipient.observe(getViewLifecycleOwner(), resolved -> {
chipGroup.removeView(chip); if (chip.isAttachedToWindow()) {
chip.setAvatar(glideRequests, resolved, null);
chip.setText(resolved.getShortDisplayName(chip.getContext()));
}
}); });
return chip; }
}
private void setChipGroupVisibility(int visibility) {
TransitionManager.beginDelayedTransition(constraintLayout, new AutoTransition().setDuration(CHIP_GROUP_REVEAL_DURATION_MS));
ConstraintSet constraintSet = new ConstraintSet();
constraintSet.clone(constraintLayout);
constraintSet.setVisibility(R.id.chipGroupScrollContainer, visibility);
constraintSet.applyTo(constraintLayout);
} }
public void setOnContactSelectedListener(OnContactSelectedListener onContactSelectedListener) { public void setOnContactSelectedListener(OnContactSelectedListener onContactSelectedListener) {
@ -494,14 +537,6 @@ public final class ContactSelectionListFragment extends Fragment
this.swipeRefresh.setOnRefreshListener(onRefreshListener); this.swipeRefresh.setOnRefreshListener(onRefreshListener);
} }
private void autoScrollOnNewItem() {
chipGroup.addOnLayoutChangeListener((view1, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
if (right > oldRight) {
scrollDebounce.publish(this::smoothScrollChipsToEnd);
}
});
}
private void smoothScrollChipsToEnd() { private void smoothScrollChipsToEnd() {
int x = chipGroupScrollContainer.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR ? chipGroup.getWidth() : 0; int x = chipGroupScrollContainer.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR ? chipGroup.getWidth() : 0;
chipGroupScrollContainer.smoothScrollTo(x, 0); chipGroupScrollContainer.smoothScrollTo(x, 0);

View file

@ -44,17 +44,21 @@ public final class ContactChip extends Chip {
return contact; return contact;
} }
public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient) { public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, @Nullable Runnable onAvatarSet) {
if (recipient != null) { if (recipient != null) {
requestManager.clear(this); requestManager.clear(this);
Drawable fallbackContactPhotoDrawable = recipient.getFallbackContactPhotoDrawable(getContext(), false); Drawable fallbackContactPhotoDrawable = new HalfScaleDrawable(recipient.getFallbackContactPhotoDrawable(getContext(), false));
ContactPhoto contactPhoto = recipient.getContactPhoto(); ContactPhoto contactPhoto = recipient.getContactPhoto();
if (contactPhoto == null) { if (contactPhoto == null) {
setChipIcon(new HalfScaleDrawable(fallbackContactPhotoDrawable)); setChipIcon(fallbackContactPhotoDrawable);
if (onAvatarSet != null) {
onAvatarSet.run();
}
} else { } else {
requestManager.load(contactPhoto) requestManager.load(contactPhoto)
.placeholder(fallbackContactPhotoDrawable)
.fallback(fallbackContactPhotoDrawable) .fallback(fallbackContactPhotoDrawable)
.error(fallbackContactPhotoDrawable) .error(fallbackContactPhotoDrawable)
.diskCacheStrategy(DiskCacheStrategy.ALL) .diskCacheStrategy(DiskCacheStrategy.ALL)
@ -63,6 +67,9 @@ public final class ContactChip extends Chip {
@Override @Override
public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) { public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
setChipIcon(resource); setChipIcon(resource);
if (onAvatarSet != null) {
onAvatarSet.run();
}
} }
@Override @Override

View file

@ -35,7 +35,6 @@ public abstract class GroupMemberEntry {
public final static class NewGroupCandidate extends GroupMemberEntry { public final static class NewGroupCandidate extends GroupMemberEntry {
private final DefaultValueLiveData<Boolean> isSelected = new DefaultValueLiveData<>(false);
private final Recipient member; private final Recipient member;
public NewGroupCandidate(@NonNull Recipient member) { public NewGroupCandidate(@NonNull Recipient member) {
@ -46,14 +45,6 @@ public abstract class GroupMemberEntry {
return member; return member;
} }
public @NonNull LiveData<Boolean> isSelected() {
return isSelected;
}
public void setSelected(boolean isSelected) {
this.isSelected.postValue(isSelected);
}
@Override @Override
boolean sameId(@NonNull GroupMemberEntry newItem) { boolean sameId(@NonNull GroupMemberEntry newItem) {
if (getClass() != newItem.getClass()) return false; if (getClass() != newItem.getClass()) return false;

View file

@ -246,9 +246,6 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
bindRecipient(newGroupCandidate.getMember()); bindRecipient(newGroupCandidate.getMember());
bindRecipientClick(newGroupCandidate.getMember()); bindRecipientClick(newGroupCandidate.getMember());
itemView.setSelected(false);
newGroupCandidate.isSelected().observe(this, itemView::setSelected);
int smsWarningVisibility = newGroupCandidate.getMember().isRegistered() ? View.GONE : View.VISIBLE; int smsWarningVisibility = newGroupCandidate.getMember().isRegistered() ? View.GONE : View.VISIBLE;
smsContact.setVisibility(smsWarningVisibility); smsContact.setVisibility(smsWarningVisibility);

View file

@ -20,6 +20,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders; import androidx.lifecycle.ViewModelProviders;
@ -58,38 +59,6 @@ public class AddGroupDetailsFragment extends Fragment {
private Drawable avatarPlaceholder; private Drawable avatarPlaceholder;
private EditText name; private EditText name;
private Toolbar toolbar; private Toolbar toolbar;
private ActionMode actionMode;
private ActionMode.Callback recipientActionModeCallback = new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
mode.getMenuInflater().inflate(R.menu.add_group_details_fragment_context_menu, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (item.getItemId() == R.id.action_delete) {
viewModel.deleteSelected();
mode.finish();
return true;
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
actionMode = null;
viewModel.clearSelected();
}
};
@Override @Override
public void onAttach(@NonNull Context context) { public void onAttach(@NonNull Context context) {
@ -142,7 +111,6 @@ public class AddGroupDetailsFragment extends Fragment {
avatar.setOnClickListener(v -> AvatarSelectionBottomSheetDialogFragment.create(viewModel.hasAvatar(), true, REQUEST_CODE_AVATAR, true) avatar.setOnClickListener(v -> AvatarSelectionBottomSheetDialogFragment.create(viewModel.hasAvatar(), true, REQUEST_CODE_AVATAR, true)
.show(getChildFragmentManager(), "BOTTOM")); .show(getChildFragmentManager(), "BOTTOM"));
members.setRecipientLongClickListener(this::handleRecipientLongClick);
members.setRecipientClickListener(this::handleRecipientClick); members.setRecipientClickListener(this::handleRecipientClick);
name.addTextChangedListener(new AfterTextChanged(editable -> viewModel.setName(editable.toString()))); name.addTextChangedListener(new AfterTextChanged(editable -> viewModel.setName(editable.toString())));
toolbar.setNavigationOnClickListener(unused -> callback.onNavigationButtonPressed()); toolbar.setNavigationOnClickListener(unused -> callback.onNavigationButtonPressed());
@ -219,29 +187,15 @@ public class AddGroupDetailsFragment extends Fragment {
} }
private void handleRecipientClick(@NonNull Recipient recipient) { private void handleRecipientClick(@NonNull Recipient recipient) {
if (actionMode == null) { new AlertDialog.Builder(requireContext())
return; .setMessage(getString(R.string.AddGroupDetailsFragment__remove_s_from_this_group, recipient.getDisplayName(requireContext())))
} .setCancelable(true)
.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel())
int size = viewModel.toggleSelected(recipient); .setPositiveButton(R.string.AddGroupDetailsFragment__remove, (dialog, which) -> {
if (size == 0) { viewModel.delete(recipient.getId());
actionMode.finish(); dialog.dismiss();
} })
} .show();
private boolean handleRecipientLongClick(@NonNull Recipient recipient) {
if (actionMode != null) {
return false;
}
actionMode = toolbar.startActionMode(recipientActionModeCallback);
if (actionMode != null) {
viewModel.toggleSelected(recipient);
return true;
}
return false;
} }
private void handleGroupCreateResult(@NonNull GroupCreateResult groupCreateResult) { private void handleGroupCreateResult(@NonNull GroupCreateResult groupCreateResult) {

View file

@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DefaultValueLiveData; import org.thoughtcrime.securesms.util.DefaultValueLiveData;
import org.thoughtcrime.securesms.util.SingleLiveEvent; import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import java.util.HashSet; import java.util.HashSet;
@ -28,7 +29,6 @@ import java.util.Set;
public final class AddGroupDetailsViewModel extends ViewModel { public final class AddGroupDetailsViewModel extends ViewModel {
private final LiveData<List<GroupMemberEntry.NewGroupCandidate>> members; private final LiveData<List<GroupMemberEntry.NewGroupCandidate>> members;
private final DefaultValueLiveData<Set<RecipientId>> selected = new DefaultValueLiveData<>(new HashSet<>());
private final DefaultValueLiveData<Set<RecipientId>> deleted = new DefaultValueLiveData<>(new HashSet<>()); private final DefaultValueLiveData<Set<RecipientId>> deleted = new DefaultValueLiveData<>(new HashSet<>());
private final MutableLiveData<String> name = new MutableLiveData<>(""); private final MutableLiveData<String> name = new MutableLiveData<>("");
private final MutableLiveData<byte[]> avatar = new MutableLiveData<>(); private final MutableLiveData<byte[]> avatar = new MutableLiveData<>();
@ -37,17 +37,14 @@ public final class AddGroupDetailsViewModel extends ViewModel {
private final LiveData<Boolean> canSubmitForm = Transformations.map(name, name -> !TextUtils.isEmpty(name)); private final LiveData<Boolean> canSubmitForm = Transformations.map(name, name -> !TextUtils.isEmpty(name));
private final AddGroupDetailsRepository repository; private final AddGroupDetailsRepository repository;
AddGroupDetailsViewModel(@NonNull RecipientId[] recipientIds, private AddGroupDetailsViewModel(@NonNull RecipientId[] recipientIds,
@NonNull AddGroupDetailsRepository repository) @NonNull AddGroupDetailsRepository repository)
{ {
this.repository = repository; this.repository = repository;
MutableLiveData<List<GroupMemberEntry.NewGroupCandidate>> initialMembers = new MutableLiveData<>(); MutableLiveData<List<GroupMemberEntry.NewGroupCandidate>> initialMembers = new MutableLiveData<>();
LiveData<List<GroupMemberEntry.NewGroupCandidate>> membersWithoutDeleted = LiveDataUtil.combineLatest(initialMembers,
deleted,
AddGroupDetailsViewModel::filterDeletedMembers);
members = LiveDataUtil.combineLatest(membersWithoutDeleted, selected, AddGroupDetailsViewModel::updateSelectedMembers); members = LiveDataUtil.combineLatest(initialMembers, deleted, AddGroupDetailsViewModel::filterDeletedMembers);
isMms = Transformations.map(members, this::isAnyForcedSms); isMms = Transformations.map(members, this::isAnyForcedSms);
repository.resolveMembers(recipientIds, initialMembers::postValue); repository.resolveMembers(recipientIds, initialMembers::postValue);
@ -85,27 +82,10 @@ public final class AddGroupDetailsViewModel extends ViewModel {
this.name.setValue(name); this.name.setValue(name);
} }
int toggleSelected(@NonNull Recipient recipient) { void delete(@NonNull RecipientId recipientId) {
Set<RecipientId> selected = this.selected.getValue();
if (!selected.add(recipient.getId())) {
selected.remove(recipient.getId());
}
this.selected.setValue(selected);
return selected.size();
}
void clearSelected() {
this.selected.setValue(new HashSet<>());
}
void deleteSelected() {
Set<RecipientId> selected = this.selected.getValue();
Set<RecipientId> deleted = this.deleted.getValue(); Set<RecipientId> deleted = this.deleted.getValue();
deleted.addAll(selected); deleted.add(recipientId);
this.deleted.setValue(deleted); this.deleted.setValue(deleted);
} }
@ -139,14 +119,6 @@ public final class AddGroupDetailsViewModel extends ViewModel {
.toList(); .toList();
} }
private static @NonNull List<GroupMemberEntry.NewGroupCandidate> updateSelectedMembers(@NonNull List<GroupMemberEntry.NewGroupCandidate> members, @NonNull Set<RecipientId> selected) {
for (GroupMemberEntry.NewGroupCandidate member : members) {
member.setSelected(selected.contains(member.getMember().getId()));
}
return members;
}
private boolean isAnyForcedSms(@NonNull List<GroupMemberEntry.NewGroupCandidate> members) { private boolean isAnyForcedSms(@NonNull List<GroupMemberEntry.NewGroupCandidate> members) {
return Stream.of(members) return Stream.of(members)
.anyMatch(member -> !member.getMember().isRegistered()); .anyMatch(member -> !member.getMember().isRegistered());

View file

@ -2,6 +2,7 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -112,20 +113,22 @@
<HorizontalScrollView <HorizontalScrollView
android:id="@+id/chipGroupScrollContainer" android:id="@+id/chipGroupScrollContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="56dp"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:paddingBottom="8dp" android:paddingBottom="8dp"
android:scrollbars="none" android:scrollbars="none"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5" app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible">
<com.google.android.material.chip.ChipGroup <com.google.android.material.chip.ChipGroup
android:id="@+id/chipGroup" android:id="@+id/chipGroup"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="56dp"
android:layout_marginEnd="96dp" android:layout_marginEnd="96dp"
android:animateLayoutChanges="true" android:animateLayoutChanges="true"
app:singleLine="true"> app:singleLine="true">

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_delete"
android:icon="?attr/menu_trash_icon"
android:title="@string/AddGroupDetailsFragment__remove"
app:showAsAction="always" />
</menu>

View file

@ -500,6 +500,7 @@
<string name="AddGroupDetailsFragment__youve_selected_a_contact_that_doesnt">You\'ve selected a contact that doesn\'t support Signal groups, so this group will be MMS.</string> <string name="AddGroupDetailsFragment__youve_selected_a_contact_that_doesnt">You\'ve selected a contact that doesn\'t support Signal groups, so this group will be MMS.</string>
<string name="AddGroupDetailsFragment__remove">Remove</string> <string name="AddGroupDetailsFragment__remove">Remove</string>
<string name="AddGroupDetailsFragment__sms_contact">SMS contact</string> <string name="AddGroupDetailsFragment__sms_contact">SMS contact</string>
<string name="AddGroupDetailsFragment__remove_s_from_this_group">Remove %1$s from this group?</string>
<!-- ManageGroupActivity --> <!-- ManageGroupActivity -->
<string name="ManageGroupActivity_disappearing_messages">Disappearing messages</string> <string name="ManageGroupActivity_disappearing_messages">Disappearing messages</string>