From e1bb773d8562b5d2b505861ed687fa0e45ce7a66 Mon Sep 17 00:00:00 2001 From: Alan Evans Date: Tue, 9 Jun 2020 12:09:59 -0300 Subject: [PATCH] Add 'Add to a group' button to bottom sheet. --- app/src/main/AndroidManifest.xml | 3 + .../ContactSelectionListFragment.java | 2 +- .../securesms/components/AvatarImageView.java | 14 +- .../components/ContactFilterToolbar.java | 47 ++--- .../contacts/ContactSelectionListAdapter.java | 14 +- .../contacts/ContactSelectionListItem.java | 5 +- .../contacts/ContactsCursorLoader.java | 12 +- .../ui/addtogroup/AddToGroupRepository.java | 59 +++++++ .../ui/addtogroup/AddToGroupViewModel.java | 125 +++++++++++++ .../ui/addtogroup/AddToGroupsActivity.java | 164 ++++++++++++++++++ .../RecipientBottomSheetDialogFragment.java | 7 + .../RecipientDialogRepository.java | 19 ++ .../bottomsheet/RecipientDialogViewModel.java | 6 +- .../main/res/layout/add_to_group_activity.xml | 46 +++++ .../res/layout/recipient_bottom_sheet.xml | 10 ++ app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/strings.xml | 8 + 17 files changed, 505 insertions(+), 37 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/groups/ui/addtogroup/AddToGroupRepository.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/groups/ui/addtogroup/AddToGroupViewModel.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/groups/ui/addtogroup/AddToGroupsActivity.java create mode 100644 app/src/main/res/layout/add_to_group_activity.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6b644d755b..4d6bf724e0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -492,6 +492,9 @@ + + diff --git a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java index 948236a4bd..84cb24ce54 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java @@ -255,7 +255,7 @@ public final class ContactSelectionListFragment extends Fragment : Collections.unmodifiableSet(Stream.of(currentSelection).collect(Collectors.toSet())); } - private boolean isMulti() { + public boolean isMulti() { return requireActivity().getIntent().getBooleanExtra(MULTI_SELECT, false); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java b/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java index 5b4ae785a8..9e5d90e7aa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java @@ -11,6 +11,7 @@ import android.util.AttributeSet; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatImageView; +import androidx.fragment.app.FragmentActivity; 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.GlideRequests; 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.FeatureFlags; 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) { if (quickContactEnabled) { super.setOnClickListener(v -> { + Context context = getContext(); if (FeatureFlags.newGroupUI() && recipient.isPushGroup()) { - getContext().startActivity(ManageGroupActivity.newIntent(getContext(), recipient.requireGroupId().requirePush()), - ManageGroupActivity.createTransitionBundle(getContext(), this)); + context.startActivity(ManageGroupActivity.newIntent(context, recipient.requireGroupId().requirePush()), + ManageGroupActivity.createTransitionBundle(context, this)); } 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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ContactFilterToolbar.java b/app/src/main/java/org/thoughtcrime/securesms/components/ContactFilterToolbar.java index b8f32d0c7f..68a20046fd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ContactFilterToolbar.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ContactFilterToolbar.java @@ -3,11 +3,6 @@ package org.thoughtcrime.securesms.components; import android.content.Context; import android.content.res.TypedArray; 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.InputType; import android.text.TextWatcher; @@ -18,20 +13,23 @@ import android.widget.EditText; import android.widget.ImageView; 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.util.ServiceUtil; -import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.views.DarkOverflowToolbar; -public class ContactFilterToolbar extends DarkOverflowToolbar { +public final class ContactFilterToolbar extends DarkOverflowToolbar { private OnFilterChangedListener listener; - private EditText searchText; - private AnimatingToggle toggle; - private ImageView keyboardToggle; - private ImageView dialpadToggle; - private ImageView clearToggle; - private LinearLayout toggleContainer; + private final EditText searchText; + private final AnimatingToggle toggle; + private final ImageView keyboardToggle; + private final ImageView dialpadToggle; + private final ImageView clearToggle; + private final LinearLayout toggleContainer; public ContactFilterToolbar(Context context) { this(context, null); @@ -45,12 +43,12 @@ public class ContactFilterToolbar extends DarkOverflowToolbar { super(context, attrs, defStyleAttr); inflate(context, R.layout.contact_filter_toolbar, this); - this.searchText = ViewUtil.findById(this, R.id.search_view); - this.toggle = ViewUtil.findById(this, R.id.button_toggle); - this.keyboardToggle = ViewUtil.findById(this, R.id.search_keyboard); - this.dialpadToggle = ViewUtil.findById(this, R.id.search_dialpad); - this.clearToggle = ViewUtil.findById(this, R.id.search_clear); - this.toggleContainer = ViewUtil.findById(this, R.id.toggle_container); + this.searchText = findViewById(R.id.search_view); + this.toggle = findViewById(R.id.button_toggle); + this.keyboardToggle = findViewById(R.id.search_keyboard); + this.dialpadToggle = findViewById(R.id.search_dialpad); + this.clearToggle = findViewById(R.id.search_clear); + this.toggleContainer = findViewById(R.id.toggle_container); this.keyboardToggle.setOnClickListener(new View.OnClickListener() { @Override @@ -103,11 +101,11 @@ public class ContactFilterToolbar extends DarkOverflowToolbar { setLogo(null); setContentInsetStartWithNavigation(0); expandTapArea(toggleContainer, dialpadToggle); - styleSearchText(searchText, context, attrs, defStyleAttr); + applyAttributes(searchText, context, attrs, defStyleAttr); searchText.requestFocus(); } - private void styleSearchText(@NonNull EditText searchText, + private void applyAttributes(@NonNull EditText searchText, @NonNull Context context, @NonNull AttributeSet attrs, int defStyle) @@ -121,6 +119,9 @@ public class ContactFilterToolbar extends DarkOverflowToolbar { if (styleResource != -1) { TextViewCompat.setTextAppearance(searchText, styleResource); } + if (!attributes.getBoolean(R.styleable.ContactFilterToolbar_showDialpad, true)) { + dialpadToggle.setVisibility(GONE); + } attributes.recycle(); } @@ -133,6 +134,10 @@ public class ContactFilterToolbar extends DarkOverflowToolbar { this.listener = listener; } + public void setHint(@StringRes int hint) { + searchText.setHint(hint); + } + private void notifyListener() { if (listener != null) listener.onFilterChanged(searchText.getText().toString()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java index 78dcdcd293..13333b7ff2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java @@ -101,7 +101,7 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter getUnfilteredResults() { ArrayList cursorList = new ArrayList<>(); - addRecentsSection(cursorList); - addContactsSection(cursorList); + if (groupsOnly(mode)) { + addGroupsSection(cursorList); + } else { + addRecentsSection(cursorList); + addContactsSection(cursorList); + } return cursorList; } @@ -376,6 +380,10 @@ public class ContactsCursorLoader extends CursorLoader { 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) { return (mode & flag) > 0; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addtogroup/AddToGroupRepository.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addtogroup/AddToGroupRepository.java new file mode 100644 index 0000000000..a9e9852624 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addtogroup/AddToGroupRepository.java @@ -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); + } + }); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addtogroup/AddToGroupViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addtogroup/AddToGroupViewModel.java new file mode 100644 index 0000000000..dc7de1c4fe --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addtogroup/AddToGroupViewModel.java @@ -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 events = new SingleLiveEvent<>(); + + private AddToGroupViewModel(@NonNull RecipientId recipientId) { + this.context = ApplicationDependencies.getApplication(); + this.recipientId = recipientId; + this.repository = new AddToGroupRepository(); + } + + public SingleLiveEvent getEvents() { + return events; + } + + void onContinueWithSelection(@NonNull List 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 create(@NonNull Class modelClass) { + return Objects.requireNonNull(modelClass.cast(new AddToGroupViewModel(recipientId))); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addtogroup/AddToGroupsActivity.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addtogroup/AddToGroupsActivity.java new file mode 100644 index 0000000000..8de88a1721 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addtogroup/AddToGroupsActivity.java @@ -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 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, 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, 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 groupsRecipientIds = Stream.of(contactsFragment.getSelectedContacts()) + .map(selectedContact -> selectedContact.getOrCreateRecipientId(this)) + .toList(); + + viewModel.onContinueWithSelection(groupsRecipientIds); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java index 4e2616e5f8..f29edd2b47 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java @@ -50,6 +50,7 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF private Button blockButton; private Button unblockButton; private Button addContactButton; + private Button addToGroupButton; private Button viewSafetyNumberButton; private Button makeGroupAdminButton; private Button removeAdminButton; @@ -93,6 +94,7 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF blockButton = view.findViewById(R.id.rbs_block_button); unblockButton = view.findViewById(R.id.rbs_unblock_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); makeGroupAdminButton = view.findViewById(R.id.rbs_make_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)); + addToGroupButton.setOnClickListener(view -> { + dismiss(); + viewModel.onAddToGroupButton(requireActivity()); + }); + viewModel.getAdminActionBusy().observe(getViewLifecycleOwner(), busy -> { adminActionBusy.setVisibility(busy ? View.VISIBLE : View.GONE); diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogRepository.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogRepository.java index 2e494bb105..7f7157f1ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogRepository.java @@ -8,6 +8,7 @@ import androidx.core.util.Consumer; import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.IdentityDatabase; import org.thoughtcrime.securesms.groups.GroupChangeBusyException; 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 java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; final class RecipientDialogRepository { @@ -117,6 +120,22 @@ final class RecipientDialogRepository { onComplete::accept); } + void getGroupMembership(@NonNull Consumer> onComplete) { + SimpleTask.run(SignalExecutors.UNBOUNDED, + () -> { + GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); + List groupRecords = groupDatabase.getPushGroupsContainingMember(recipientId); + ArrayList groupRecipients = new ArrayList<>(groupRecords.size()); + + for (GroupDatabase.GroupRecord groupRecord : groupRecords) { + groupRecipients.add(groupRecord.getRecipientId()); + } + + return groupRecipients; + }, + onComplete::accept); + } + interface IdentityCallback { void remoteIdentity(@Nullable IdentityDatabase.IdentityRecord identityRecord); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java index c32fca8e5a..5b04ab3be1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java @@ -11,7 +11,6 @@ import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.Transformations; import androidx.lifecycle.ViewModel; 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.ui.GroupChangeFailureReason; 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.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; @@ -171,6 +171,10 @@ final class RecipientDialogViewModel extends ViewModel { recipientDialogRepository.refreshRecipient(); } + void onAddToGroupButton(@NonNull Activity activity) { + recipientDialogRepository.getGroupMembership(existingGroups -> activity.startActivity(AddToGroupsActivity.newIntent(activity, recipientDialogRepository.getRecipientId(), existingGroups))); + } + @WorkerThread private void showErrorToast(@NonNull GroupChangeFailureReason e) { Util.runOnMain(() -> Toast.makeText(context, GroupErrors.getUserDisplayMessage(e), Toast.LENGTH_LONG).show()); diff --git a/app/src/main/res/layout/add_to_group_activity.xml b/app/src/main/res/layout/add_to_group_activity.xml new file mode 100644 index 0000000000..871e8534e9 --- /dev/null +++ b/app/src/main/res/layout/add_to_group_activity.xml @@ -0,0 +1,46 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/recipient_bottom_sheet.xml b/app/src/main/res/layout/recipient_bottom_sheet.xml index 97a58534d8..6627bd910e 100644 --- a/app/src/main/res/layout/recipient_bottom_sheet.xml +++ b/app/src/main/res/layout/recipient_bottom_sheet.xml @@ -119,6 +119,16 @@ app:drawableStartCompat="?attr/recipient_add_contact_icon" tools:visibility="visible" /> +