Add 'Add to a group' button to bottom sheet.

This commit is contained in:
Alan Evans 2020-06-09 12:09:59 -03:00 committed by GitHub
parent 7e934eff5d
commit e1bb773d85
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 505 additions and 37 deletions

View file

@ -492,6 +492,9 @@
<activity android:name=".groups.ui.creategroup.CreateGroupActivity" <activity android:name=".groups.ui.creategroup.CreateGroupActivity"
android:theme="@style/TextSecure.LightNoActionBar" /> android:theme="@style/TextSecure.LightNoActionBar" />
<activity android:name=".groups.ui.addtogroup.AddToGroupsActivity"
android:theme="@style/TextSecure.LightNoActionBar" />
<activity android:name=".groups.ui.addmembers.AddMembersActivity" <activity android:name=".groups.ui.addmembers.AddMembersActivity"
android:theme="@style/TextSecure.LightNoActionBar" /> android:theme="@style/TextSecure.LightNoActionBar" />

View file

@ -255,7 +255,7 @@ public final class ContactSelectionListFragment extends Fragment
: Collections.unmodifiableSet(Stream.of(currentSelection).collect(Collectors.toSet())); : Collections.unmodifiableSet(Stream.of(currentSelection).collect(Collectors.toSet()));
} }
private boolean isMulti() { public boolean isMulti() {
return requireActivity().getIntent().getBooleanExtra(MULTI_SELECT, false); return requireActivity().getIntent().getBooleanExtra(MULTI_SELECT, false);
} }

View file

@ -11,6 +11,7 @@ import android.util.AttributeSet;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView; import androidx.appcompat.widget.AppCompatImageView;
import androidx.fragment.app.FragmentActivity;
import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.DiskCacheStrategy;
@ -24,6 +25,7 @@ import org.thoughtcrime.securesms.groups.ui.managegroup.ManageGroupActivity;
import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
import org.thoughtcrime.securesms.util.AvatarUtil; import org.thoughtcrime.securesms.util.AvatarUtil;
import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.ThemeUtil;
@ -162,11 +164,17 @@ public final class AvatarImageView extends AppCompatImageView {
private void setAvatarClickHandler(@NonNull final Recipient recipient, boolean quickContactEnabled) { private void setAvatarClickHandler(@NonNull final Recipient recipient, boolean quickContactEnabled) {
if (quickContactEnabled) { if (quickContactEnabled) {
super.setOnClickListener(v -> { super.setOnClickListener(v -> {
Context context = getContext();
if (FeatureFlags.newGroupUI() && recipient.isPushGroup()) { if (FeatureFlags.newGroupUI() && recipient.isPushGroup()) {
getContext().startActivity(ManageGroupActivity.newIntent(getContext(), recipient.requireGroupId().requirePush()), context.startActivity(ManageGroupActivity.newIntent(context, recipient.requireGroupId().requirePush()),
ManageGroupActivity.createTransitionBundle(getContext(), this)); ManageGroupActivity.createTransitionBundle(context, this));
} else { } else {
getContext().startActivity(RecipientPreferenceActivity.getLaunchIntent(getContext(), recipient.getId())); if (context instanceof FragmentActivity) {
RecipientBottomSheetDialogFragment.create(recipient.getId(), null)
.show(((FragmentActivity) context).getSupportFragmentManager(), "BOTTOM");
} else {
context.startActivity(RecipientPreferenceActivity.getLaunchIntent(context, recipient.getId()));
}
} }
}); });
} else { } else {

View file

@ -3,11 +3,6 @@ package org.thoughtcrime.securesms.components;
import android.content.Context; import android.content.Context;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.graphics.Rect; import android.graphics.Rect;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import androidx.core.widget.TextViewCompat;
import android.text.Editable; import android.text.Editable;
import android.text.InputType; import android.text.InputType;
import android.text.TextWatcher; import android.text.TextWatcher;
@ -18,20 +13,23 @@ import android.widget.EditText;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.core.widget.TextViewCompat;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.DarkOverflowToolbar; import org.thoughtcrime.securesms.util.views.DarkOverflowToolbar;
public class ContactFilterToolbar extends DarkOverflowToolbar { public final class ContactFilterToolbar extends DarkOverflowToolbar {
private OnFilterChangedListener listener; private OnFilterChangedListener listener;
private EditText searchText; private final EditText searchText;
private AnimatingToggle toggle; private final AnimatingToggle toggle;
private ImageView keyboardToggle; private final ImageView keyboardToggle;
private ImageView dialpadToggle; private final ImageView dialpadToggle;
private ImageView clearToggle; private final ImageView clearToggle;
private LinearLayout toggleContainer; private final LinearLayout toggleContainer;
public ContactFilterToolbar(Context context) { public ContactFilterToolbar(Context context) {
this(context, null); this(context, null);
@ -45,12 +43,12 @@ public class ContactFilterToolbar extends DarkOverflowToolbar {
super(context, attrs, defStyleAttr); super(context, attrs, defStyleAttr);
inflate(context, R.layout.contact_filter_toolbar, this); inflate(context, R.layout.contact_filter_toolbar, this);
this.searchText = ViewUtil.findById(this, R.id.search_view); this.searchText = findViewById(R.id.search_view);
this.toggle = ViewUtil.findById(this, R.id.button_toggle); this.toggle = findViewById(R.id.button_toggle);
this.keyboardToggle = ViewUtil.findById(this, R.id.search_keyboard); this.keyboardToggle = findViewById(R.id.search_keyboard);
this.dialpadToggle = ViewUtil.findById(this, R.id.search_dialpad); this.dialpadToggle = findViewById(R.id.search_dialpad);
this.clearToggle = ViewUtil.findById(this, R.id.search_clear); this.clearToggle = findViewById(R.id.search_clear);
this.toggleContainer = ViewUtil.findById(this, R.id.toggle_container); this.toggleContainer = findViewById(R.id.toggle_container);
this.keyboardToggle.setOnClickListener(new View.OnClickListener() { this.keyboardToggle.setOnClickListener(new View.OnClickListener() {
@Override @Override
@ -103,11 +101,11 @@ public class ContactFilterToolbar extends DarkOverflowToolbar {
setLogo(null); setLogo(null);
setContentInsetStartWithNavigation(0); setContentInsetStartWithNavigation(0);
expandTapArea(toggleContainer, dialpadToggle); expandTapArea(toggleContainer, dialpadToggle);
styleSearchText(searchText, context, attrs, defStyleAttr); applyAttributes(searchText, context, attrs, defStyleAttr);
searchText.requestFocus(); searchText.requestFocus();
} }
private void styleSearchText(@NonNull EditText searchText, private void applyAttributes(@NonNull EditText searchText,
@NonNull Context context, @NonNull Context context,
@NonNull AttributeSet attrs, @NonNull AttributeSet attrs,
int defStyle) int defStyle)
@ -121,6 +119,9 @@ public class ContactFilterToolbar extends DarkOverflowToolbar {
if (styleResource != -1) { if (styleResource != -1) {
TextViewCompat.setTextAppearance(searchText, styleResource); TextViewCompat.setTextAppearance(searchText, styleResource);
} }
if (!attributes.getBoolean(R.styleable.ContactFilterToolbar_showDialpad, true)) {
dialpadToggle.setVisibility(GONE);
}
attributes.recycle(); attributes.recycle();
} }
@ -133,6 +134,10 @@ public class ContactFilterToolbar extends DarkOverflowToolbar {
this.listener = listener; this.listener = listener;
} }
public void setHint(@StringRes int hint) {
searchText.setHint(hint);
}
private void notifyListener() { private void notifyListener() {
if (listener != null) listener.onFilterChanged(searchText.getText().toString()); if (listener != null) listener.onFilterChanged(searchText.getText().toString());
} }

View file

@ -101,7 +101,7 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
super(itemView); super(itemView);
} }
public abstract void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, int color, boolean multiSelect); public abstract void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, int color, boolean checkboxVisible);
public abstract void unbind(@NonNull GlideRequests glideRequests); public abstract void unbind(@NonNull GlideRequests glideRequests);
public abstract void setChecked(boolean checked); public abstract void setChecked(boolean checked);
public abstract void setEnabled(boolean enabled); public abstract void setEnabled(boolean enabled);
@ -121,8 +121,8 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
return (ContactSelectionListItem) itemView; return (ContactSelectionListItem) itemView;
} }
public void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, int color, boolean multiSelect) { public void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, int color, boolean checkBoxVisible) {
getView().set(glideRequests, recipientId, type, name, number, label, color, multiSelect); getView().set(glideRequests, recipientId, type, name, number, label, color, checkBoxVisible);
} }
@Override @Override
@ -151,7 +151,7 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
} }
@Override @Override
public void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, int color, boolean multiSelect) { public void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, int color, boolean checkboxVisible) {
this.label.setText(name); this.label.setText(name);
} }
@ -222,11 +222,13 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
int color = (contactType == ContactRepository.PUSH_TYPE) ? drawables.getColor(0, 0xa0000000) : int color = (contactType == ContactRepository.PUSH_TYPE) ? drawables.getColor(0, 0xa0000000) :
drawables.getColor(1, 0xff000000); drawables.getColor(1, 0xff000000);
boolean currentContact = currentContacts.contains(id);
viewHolder.unbind(glideRequests); viewHolder.unbind(glideRequests);
viewHolder.bind(glideRequests, id, contactType, name, number, labelText, color, multiSelect); viewHolder.bind(glideRequests, id, contactType, name, number, labelText, color, multiSelect || currentContact);
viewHolder.setEnabled(true); viewHolder.setEnabled(true);
if (currentContacts.contains(id)) { if (currentContact) {
viewHolder.setChecked(true); viewHolder.setChecked(true);
viewHolder.setEnabled(false); viewHolder.setEnabled(false);
} else if (numberType == ContactRepository.NEW_USERNAME_TYPE) { } else if (numberType == ContactRepository.NEW_USERNAME_TYPE) {

View file

@ -67,7 +67,7 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
String number, String number,
String label, String label,
int color, int color,
boolean multiSelect) boolean checkboxVisible)
{ {
this.glideRequests = glideRequests; this.glideRequests = glideRequests;
this.number = number; this.number = number;
@ -90,8 +90,7 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
setText(recipientSnapshot, type, name, number, label); setText(recipientSnapshot, type, name, number, label);
if (multiSelect) this.checkBox.setVisibility(View.VISIBLE); this.checkBox.setVisibility(checkboxVisible ? View.VISIBLE : View.GONE);
else this.checkBox.setVisibility(View.GONE);
} }
public void setChecked(boolean selected) { public void setChecked(boolean selected) {

View file

@ -115,8 +115,12 @@ public class ContactsCursorLoader extends CursorLoader {
private List<Cursor> getUnfilteredResults() { private List<Cursor> getUnfilteredResults() {
ArrayList<Cursor> cursorList = new ArrayList<>(); ArrayList<Cursor> cursorList = new ArrayList<>();
if (groupsOnly(mode)) {
addGroupsSection(cursorList);
} else {
addRecentsSection(cursorList); addRecentsSection(cursorList);
addContactsSection(cursorList); addContactsSection(cursorList);
}
return cursorList; return cursorList;
} }
@ -376,6 +380,10 @@ public class ContactsCursorLoader extends CursorLoader {
return flagSet(mode, DisplayMode.FLAG_ACTIVE_GROUPS); return flagSet(mode, DisplayMode.FLAG_ACTIVE_GROUPS);
} }
private static boolean groupsOnly(int mode) {
return mode == DisplayMode.FLAG_ACTIVE_GROUPS;
}
private static boolean flagSet(int mode, int flag) { private static boolean flagSet(int mode, int flag) {
return (mode & flag) > 0; return (mode & flag) > 0;
} }

View file

@ -0,0 +1,59 @@
package org.thoughtcrime.securesms.groups.ui.addtogroup;
import android.content.Context;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupInsufficientRightsException;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.groups.GroupNotAMemberException;
import org.thoughtcrime.securesms.groups.MembershipNotSuitableForV2Exception;
import org.thoughtcrime.securesms.groups.ui.GroupChangeErrorCallback;
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.concurrent.SignalExecutors;
import java.io.IOException;
import java.util.Collections;
final class AddToGroupRepository {
private static final String TAG = Log.tag(AddToGroupRepository.class);
private final Context context;
AddToGroupRepository() {
this.context = ApplicationDependencies.getApplication();
}
public void add(@NonNull RecipientId recipientId,
@NonNull Recipient groupRecipient,
@NonNull GroupChangeErrorCallback error,
@NonNull Runnable success)
{
SignalExecutors.UNBOUNDED.execute(() -> {
try {
GroupId.Push pushGroupId = groupRecipient.requireGroupId().requirePush();
GroupManager.addMembers(context, pushGroupId, Collections.singletonList(recipientId));
success.run();
} catch (GroupInsufficientRightsException | GroupNotAMemberException e) {
Log.w(TAG, e);
error.onError(GroupChangeFailureReason.NO_RIGHTS);
} catch (GroupChangeFailedException | GroupChangeBusyException | IOException e) {
Log.w(TAG, e);
error.onError(GroupChangeFailureReason.OTHER);
} catch (MembershipNotSuitableForV2Exception e) {
Log.w(TAG, e);
error.onError(GroupChangeFailureReason.NOT_CAPABLE);
}
});
}
}

View file

@ -0,0 +1,125 @@
package org.thoughtcrime.securesms.groups.ui.addtogroup;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.ui.GroupErrors;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import java.util.List;
import java.util.Objects;
public final class AddToGroupViewModel extends ViewModel {
private final Application context;
private final AddToGroupRepository repository;
private final RecipientId recipientId;
private final SingleLiveEvent<Event> events = new SingleLiveEvent<>();
private AddToGroupViewModel(@NonNull RecipientId recipientId) {
this.context = ApplicationDependencies.getApplication();
this.recipientId = recipientId;
this.repository = new AddToGroupRepository();
}
public SingleLiveEvent<Event> getEvents() {
return events;
}
void onContinueWithSelection(@NonNull List<RecipientId> groupRecipientIds) {
if (groupRecipientIds.isEmpty()) {
events.postValue(new Event.CloseEvent());
} else if (groupRecipientIds.size() == 1) {
SignalExecutors.BOUNDED.execute(() -> {
Recipient groupRecipient = Recipient.resolved(groupRecipientIds.get(0));
String recipientName = Recipient.resolved(recipientId).getDisplayName(context);
String groupName = groupRecipient.getDisplayName(context);
events.postValue(new Event.AddToSingleGroupConfirmationEvent(context.getResources().getString(R.string.AddToGroupActivity_add_member),
context.getResources().getString(R.string.AddToGroupActivity_add_s_to_s, recipientName, groupName),
groupRecipient, recipientName, groupName));
});
} else {
throw new AssertionError("Does not support multi-select");
}
}
void onAddToGroupsConfirmed(@NonNull Event.AddToSingleGroupConfirmationEvent event) {
repository.add(recipientId,
event.groupRecipient,
error -> events.postValue(new Event.ToastEvent(context.getResources().getString(GroupErrors.getUserDisplayMessage(error)))),
() -> {
events.postValue(new Event.ToastEvent(context.getResources().getString(R.string.AddToGroupActivity_s_added_to_s, event.recipientName, event.groupName)));
events.postValue(new Event.CloseEvent());
});
}
static abstract class Event {
static class CloseEvent extends Event {
}
static class ToastEvent extends Event {
private final String message;
ToastEvent(@NonNull String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
static class AddToSingleGroupConfirmationEvent extends Event {
private final String title;
private final String message;
private final Recipient groupRecipient;
private final String recipientName;
private final String groupName;
AddToSingleGroupConfirmationEvent(@NonNull String title,
@NonNull String message,
@NonNull Recipient groupRecipient,
@NonNull String recipientName,
@NonNull String groupName)
{
this.title = title;
this.message = message;
this.groupRecipient = groupRecipient;
this.recipientName = recipientName;
this.groupName = groupName;
}
String getTitle() {
return title;
}
String getMessage() {
return message;
}
}
}
public static class Factory implements ViewModelProvider.Factory {
private final RecipientId recipientId;
public Factory(@NonNull RecipientId recipientId) {
this.recipientId = recipientId;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
return Objects.requireNonNull(modelClass.cast(new AddToGroupViewModel(recipientId)));
}
}
}

View file

@ -0,0 +1,164 @@
package org.thoughtcrime.securesms.groups.ui.addtogroup;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.ViewModelProviders;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.ContactSelectionActivity;
import org.thoughtcrime.securesms.ContactSelectionListFragment;
import org.thoughtcrime.securesms.GroupCreateActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
import org.thoughtcrime.securesms.groups.ui.addtogroup.AddToGroupViewModel.Event;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* Group selection activity, will add a single member to selected groups.
*/
public final class AddToGroupsActivity extends ContactSelectionActivity {
private static final int MINIMUM_GROUP_SELECT_SIZE = 1;
private static final String EXTRA_RECIPIENT_ID = "RECIPIENT_ID";
private View next;
private AddToGroupViewModel viewModel;
public static Intent newIntent(@NonNull Context context,
@NonNull RecipientId recipientId,
@NonNull List<RecipientId> currentGroupsMemberOf)
{
if (!FeatureFlags.newGroupUI()) {
return new Intent(context, GroupCreateActivity.class);
}
Intent intent = new Intent(context, AddToGroupsActivity.class);
intent.putExtra(ContactSelectionListFragment.MULTI_SELECT, false);
intent.putExtra(ContactSelectionListFragment.REFRESHABLE, false);
intent.putExtra(ContactSelectionActivity.EXTRA_LAYOUT_RES_ID, R.layout.add_to_group_activity);
intent.putExtra(EXTRA_RECIPIENT_ID, recipientId);
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, ContactsCursorLoader.DisplayMode.FLAG_ACTIVE_GROUPS);
intent.putExtra(ContactSelectionListFragment.TOTAL_CAPACITY, ContactSelectionListFragment.NO_LIMIT);
intent.putParcelableArrayListExtra(ContactSelectionListFragment.CURRENT_SELECTION, new ArrayList<>(currentGroupsMemberOf));
return intent;
}
@Override
public void onCreate(Bundle bundle, boolean ready) {
super.onCreate(bundle, ready);
Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);
next = findViewById(R.id.next);
getToolbar().setHint(contactsFragment.isMulti() ? R.string.AddToGroupActivity_add_to_groups : R.string.AddToGroupActivity_add_to_group);
next.setVisibility(contactsFragment.isMulti() ? View.VISIBLE : View.GONE);
disableNext();
next.setOnClickListener(v -> handleNextPressed());
AddToGroupViewModel.Factory factory = new AddToGroupViewModel.Factory(getRecipientId());
viewModel = ViewModelProviders.of(this, factory)
.get(AddToGroupViewModel.class);
viewModel.getEvents().observe(this, event -> {
if (event instanceof Event.CloseEvent) {
finish();
} else if (event instanceof Event.ToastEvent) {
Toast.makeText(this, ((Event.ToastEvent) event).getMessage(), Toast.LENGTH_SHORT).show();
} else if (event instanceof Event.AddToSingleGroupConfirmationEvent) {
Event.AddToSingleGroupConfirmationEvent addEvent = (Event.AddToSingleGroupConfirmationEvent) event;
new AlertDialog.Builder(this)
.setTitle(addEvent.getTitle())
.setMessage(addEvent.getMessage())
.setPositiveButton(android.R.string.ok, (dialog, which) -> viewModel.onAddToGroupsConfirmed(addEvent))
.setNegativeButton(android.R.string.cancel, null)
.show();
} else {
throw new AssertionError();
}
});
}
private @NonNull RecipientId getRecipientId() {
return getIntent().getParcelableExtra(EXTRA_RECIPIENT_ID);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onContactSelected(Optional<RecipientId> recipientId, String number) {
if (contactsFragment.isMulti()) {
if (contactsFragment.hasQueryFilter()) {
getToolbar().clear();
}
if (contactsFragment.getSelectedContactsCount() >= MINIMUM_GROUP_SELECT_SIZE) {
enableNext();
}
} else {
if (recipientId.isPresent()) {
viewModel.onContinueWithSelection(Collections.singletonList(recipientId.get()));
}
}
}
@Override
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {
if (contactsFragment.hasQueryFilter()) {
getToolbar().clear();
}
if (contactsFragment.getSelectedContactsCount() < MINIMUM_GROUP_SELECT_SIZE) {
disableNext();
}
}
private void enableNext() {
next.setEnabled(true);
next.animate().alpha(1f);
}
private void disableNext() {
next.setEnabled(false);
next.animate().alpha(0.5f);
}
private void handleNextPressed() {
List<RecipientId> groupsRecipientIds = Stream.of(contactsFragment.getSelectedContacts())
.map(selectedContact -> selectedContact.getOrCreateRecipientId(this))
.toList();
viewModel.onContinueWithSelection(groupsRecipientIds);
}
}

View file

@ -50,6 +50,7 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
private Button blockButton; private Button blockButton;
private Button unblockButton; private Button unblockButton;
private Button addContactButton; private Button addContactButton;
private Button addToGroupButton;
private Button viewSafetyNumberButton; private Button viewSafetyNumberButton;
private Button makeGroupAdminButton; private Button makeGroupAdminButton;
private Button removeAdminButton; private Button removeAdminButton;
@ -93,6 +94,7 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
blockButton = view.findViewById(R.id.rbs_block_button); blockButton = view.findViewById(R.id.rbs_block_button);
unblockButton = view.findViewById(R.id.rbs_unblock_button); unblockButton = view.findViewById(R.id.rbs_unblock_button);
addContactButton = view.findViewById(R.id.rbs_add_contact_button); addContactButton = view.findViewById(R.id.rbs_add_contact_button);
addToGroupButton = view.findViewById(R.id.rbs_add_to_group_button);
viewSafetyNumberButton = view.findViewById(R.id.rbs_view_safety_number_button); viewSafetyNumberButton = view.findViewById(R.id.rbs_view_safety_number_button);
makeGroupAdminButton = view.findViewById(R.id.rbs_make_group_admin_button); makeGroupAdminButton = view.findViewById(R.id.rbs_make_group_admin_button);
removeAdminButton = view.findViewById(R.id.rbs_remove_group_admin_button); removeAdminButton = view.findViewById(R.id.rbs_remove_group_admin_button);
@ -188,6 +190,11 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
removeFromGroupButton.setOnClickListener(view -> viewModel.onRemoveFromGroupClicked(requireActivity(), this::dismiss)); removeFromGroupButton.setOnClickListener(view -> viewModel.onRemoveFromGroupClicked(requireActivity(), this::dismiss));
addToGroupButton.setOnClickListener(view -> {
dismiss();
viewModel.onAddToGroupButton(requireActivity());
});
viewModel.getAdminActionBusy().observe(getViewLifecycleOwner(), busy -> { viewModel.getAdminActionBusy().observe(getViewLifecycleOwner(), busy -> {
adminActionBusy.setVisibility(busy ? View.VISIBLE : View.GONE); adminActionBusy.setVisibility(busy ? View.VISIBLE : View.GONE);

View file

@ -8,6 +8,7 @@ import androidx.core.util.Consumer;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper; import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase; import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.groups.GroupChangeBusyException; import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
import org.thoughtcrime.securesms.groups.GroupChangeFailedException; import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
@ -24,6 +25,8 @@ import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask; import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects; import java.util.Objects;
final class RecipientDialogRepository { final class RecipientDialogRepository {
@ -117,6 +120,22 @@ final class RecipientDialogRepository {
onComplete::accept); onComplete::accept);
} }
void getGroupMembership(@NonNull Consumer<List<RecipientId>> onComplete) {
SimpleTask.run(SignalExecutors.UNBOUNDED,
() -> {
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
List<GroupDatabase.GroupRecord> groupRecords = groupDatabase.getPushGroupsContainingMember(recipientId);
ArrayList<RecipientId> groupRecipients = new ArrayList<>(groupRecords.size());
for (GroupDatabase.GroupRecord groupRecord : groupRecords) {
groupRecipients.add(groupRecord.getRecipientId());
}
return groupRecipients;
},
onComplete::accept);
}
interface IdentityCallback { interface IdentityCallback {
void remoteIdentity(@Nullable IdentityDatabase.IdentityRecord identityRecord); void remoteIdentity(@Nullable IdentityDatabase.IdentityRecord identityRecord);
} }

View file

@ -11,7 +11,6 @@ import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
@ -24,6 +23,7 @@ import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.LiveGroup; import org.thoughtcrime.securesms.groups.LiveGroup;
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason; import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
import org.thoughtcrime.securesms.groups.ui.GroupErrors; import org.thoughtcrime.securesms.groups.ui.GroupErrors;
import org.thoughtcrime.securesms.groups.ui.addtogroup.AddToGroupsActivity;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.recipients.RecipientUtil;
@ -171,6 +171,10 @@ final class RecipientDialogViewModel extends ViewModel {
recipientDialogRepository.refreshRecipient(); recipientDialogRepository.refreshRecipient();
} }
void onAddToGroupButton(@NonNull Activity activity) {
recipientDialogRepository.getGroupMembership(existingGroups -> activity.startActivity(AddToGroupsActivity.newIntent(activity, recipientDialogRepository.getRecipientId(), existingGroups)));
}
@WorkerThread @WorkerThread
private void showErrorToast(@NonNull GroupChangeFailureReason e) { private void showErrorToast(@NonNull GroupChangeFailureReason e) {
Util.runOnMain(() -> Toast.makeText(context, GroupErrors.getUserDisplayMessage(e), Toast.LENGTH_LONG).show()); Util.runOnMain(() -> Toast.makeText(context, GroupErrors.getUserDisplayMessage(e), Toast.LENGTH_LONG).show());

View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_gravity="center"
android:orientation="vertical">
<org.thoughtcrime.securesms.components.ContactFilterToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:elevation="4dp"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarStyle"
app:contentInsetStartWithNavigation="0dp"
app:layout_constraintTop_toTopOf="parent"
app:navigationIcon="@drawable/ic_arrow_left_24"
app:showDialpad="false" />
<fragment
android:id="@+id/contact_selection_list_fragment"
android:name="org.thoughtcrime.securesms.ContactSelectionListFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/next"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:tint="@color/core_white"
android:visibility="gone"
app:backgroundTint="@color/core_ultramarine"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/ic_arrow_end_24"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -119,6 +119,16 @@
app:drawableStartCompat="?attr/recipient_add_contact_icon" app:drawableStartCompat="?attr/recipient_add_contact_icon"
tools:visibility="visible" /> tools:visibility="visible" />
<Button
android:id="@+id/rbs_add_to_group_button"
style="@style/Widget.Signal.Button.TextButton.Drawable"
android:layout_width="match_parent"
android:layout_height="56dp"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:text="@string/RecipientBottomSheet_add_to_a_group"
app:drawableStartCompat="?attr/recipient_make_admin_icon" />
<Button <Button
android:id="@+id/rbs_view_safety_number_button" android:id="@+id/rbs_view_safety_number_button"
style="@style/Widget.Signal.Button.TextButton.Drawable" style="@style/Widget.Signal.Button.TextButton.Drawable"

View file

@ -484,6 +484,7 @@
<declare-styleable name="ContactFilterToolbar"> <declare-styleable name="ContactFilterToolbar">
<attr name="searchTextStyle" format="reference" /> <attr name="searchTextStyle" format="reference" />
<attr name="showDialpad" format="boolean" />
</declare-styleable> </declare-styleable>
<declare-styleable name="SquareImageView"> <declare-styleable name="SquareImageView">

View file

@ -449,6 +449,13 @@
<string name="GroupCreateActivity_youre_already_in_the_group">You\'re already in the group.</string> <string name="GroupCreateActivity_youre_already_in_the_group">You\'re already in the group.</string>
<string name="GroupCreateActivity_remove_member_description">Remove member</string> <string name="GroupCreateActivity_remove_member_description">Remove member</string>
<!-- AddToGroupActivity -->
<string name="AddToGroupActivity_add_member">Add member?</string>
<string name="AddToGroupActivity_add_s_to_s">Add \"%1$s\" to \"%2$s\"?</string>
<string name="AddToGroupActivity_s_added_to_s">\"%1$s\" added to \"%2$s\".</string>
<string name="AddToGroupActivity_add_to_group">Add to group</string>
<string name="AddToGroupActivity_add_to_groups">Add to groups</string>
<!-- GroupShareProfileView --> <!-- GroupShareProfileView -->
<string name="GroupShareProfileView_share_your_profile_name_and_photo_with_this_group">Share your profile name and photo with this group?</string> <string name="GroupShareProfileView_share_your_profile_name_and_photo_with_this_group">Share your profile name and photo with this group?</string>
<string name="GroupShareProfileView_do_you_want_to_make_your_profile_name_and_photo_visible_to_all_current_and_future_members_of_this_group">Do you want to make your profile name and photo visible to all current and future members of this group?</string> <string name="GroupShareProfileView_do_you_want_to_make_your_profile_name_and_photo_visible_to_all_current_and_future_members_of_this_group">Do you want to make your profile name and photo visible to all current and future members of this group?</string>
@ -2303,6 +2310,7 @@
<string name="RecipientBottomSheet_block">Block</string> <string name="RecipientBottomSheet_block">Block</string>
<string name="RecipientBottomSheet_unblock">Unblock</string> <string name="RecipientBottomSheet_unblock">Unblock</string>
<string name="RecipientBottomSheet_add_to_contacts">Add to contacts</string> <string name="RecipientBottomSheet_add_to_contacts">Add to contacts</string>
<string name="RecipientBottomSheet_add_to_a_group">Add to a group</string>
<string name="RecipientBottomSheet_view_safety_number">View safety number</string> <string name="RecipientBottomSheet_view_safety_number">View safety number</string>
<string name="RecipientBottomSheet_make_group_admin">Make group admin</string> <string name="RecipientBottomSheet_make_group_admin">Make group admin</string>
<string name="RecipientBottomSheet_remove_as_admin">Remove as admin</string> <string name="RecipientBottomSheet_remove_as_admin">Remove as admin</string>