Implement new group creation screens behind flag.

This commit is contained in:
Alex Hart
2020-05-13 13:41:36 -03:00
parent ed0825112d
commit ccff7b1148
42 changed files with 1422 additions and 84 deletions

View File

@@ -31,7 +31,49 @@ public abstract class GroupMemberEntry {
@Override
public abstract int hashCode();
abstract boolean sameId(GroupMemberEntry newItem);
abstract boolean sameId(@NonNull GroupMemberEntry newItem);
public final static class NewGroupCandidate extends GroupMemberEntry {
private final DefaultValueLiveData<Boolean> isSelected = new DefaultValueLiveData<>(false);
private final Recipient member;
public NewGroupCandidate(@NonNull Recipient member) {
this.member = member;
}
public @NonNull Recipient getMember() {
return member;
}
public @NonNull LiveData<Boolean> isSelected() {
return isSelected;
}
public void setSelected(boolean isSelected) {
this.isSelected.postValue(isSelected);
}
@Override
boolean sameId(@NonNull GroupMemberEntry newItem) {
if (getClass() != newItem.getClass()) return false;
return member.getId().equals(((NewGroupCandidate) newItem).member.getId());
}
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof NewGroupCandidate)) return false;
NewGroupCandidate other = (NewGroupCandidate) obj;
return other.member.equals(member);
}
@Override
public int hashCode() {
return member.hashCode();
}
}
public final static class FullMember extends GroupMemberEntry {
@@ -52,7 +94,7 @@ public abstract class GroupMemberEntry {
}
@Override
boolean sameId(GroupMemberEntry newItem) {
boolean sameId(@NonNull GroupMemberEntry newItem) {
if (getClass() != newItem.getClass()) return false;
return member.getId().equals(((GroupMemberEntry.FullMember) newItem).member.getId());
@@ -97,7 +139,7 @@ public abstract class GroupMemberEntry {
}
@Override
boolean sameId(GroupMemberEntry newItem) {
boolean sameId(@NonNull GroupMemberEntry newItem) {
if (getClass() != newItem.getClass()) return false;
return invitee.getId().equals(((GroupMemberEntry.PendingMember) newItem).invitee.getId());
@@ -153,7 +195,7 @@ public abstract class GroupMemberEntry {
}
@Override
boolean sameId(GroupMemberEntry newItem) {
boolean sameId(@NonNull GroupMemberEntry newItem) {
if (getClass() != newItem.getClass()) return false;
return inviter.getId().equals(((GroupMemberEntry.UnknownPendingMemberCount) newItem).inviter.getId());

View File

@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.LifecycleRecyclerAdapter;
import org.thoughtcrime.securesms.util.LifecycleViewHolder;
import org.thoughtcrime.securesms.util.ThemeUtil;
import java.util.ArrayList;
import java.util.List;
@@ -26,11 +27,13 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
private static final int FULL_MEMBER = 0;
private static final int OWN_INVITE_PENDING = 1;
private static final int OTHER_INVITE_PENDING_COUNT = 2;
private static final int NEW_GROUP_CANDIDATE = 3;
private final ArrayList<GroupMemberEntry> data = new ArrayList<>();
@Nullable private AdminActionsListener adminActionsListener;
@Nullable private RecipientClickListener recipientClickListener;
@Nullable private AdminActionsListener adminActionsListener;
@Nullable private RecipientClickListener recipientClickListener;
@Nullable private RecipientLongClickListener recipientLongClickListener;
void updateData(@NonNull List<? extends GroupMemberEntry> recipients) {
if (data.isEmpty()) {
@@ -49,16 +52,25 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
switch (viewType) {
case FULL_MEMBER:
return new FullMemberViewHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.group_recipient_list_item,
parent, false), recipientClickListener, adminActionsListener);
.inflate(R.layout.group_recipient_list_item, parent, false),
recipientClickListener,
recipientLongClickListener,
adminActionsListener);
case OWN_INVITE_PENDING:
return new OwnInvitePendingMemberViewHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.group_recipient_list_item,
parent, false), recipientClickListener, adminActionsListener);
.inflate(R.layout.group_recipient_list_item, parent, false),
recipientClickListener,
recipientLongClickListener,
adminActionsListener);
case OTHER_INVITE_PENDING_COUNT:
return new UnknownPendingMemberCountViewHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.group_recipient_list_item,
parent, false), adminActionsListener);
.inflate(R.layout.group_recipient_list_item, parent, false),
adminActionsListener);
case NEW_GROUP_CANDIDATE:
return new NewGroupInviteeViewHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.group_new_candidate_recipient_list_item, parent, false),
recipientClickListener,
recipientLongClickListener);
default:
throw new AssertionError();
}
@@ -72,6 +84,10 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
this.recipientClickListener = recipientClickListener;
}
void setRecipientLongClickListener(@Nullable RecipientLongClickListener recipientLongClickListener) {
this.recipientLongClickListener = recipientLongClickListener;
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.bind(data.get(position));
@@ -87,6 +103,8 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
return OWN_INVITE_PENDING;
} else if (groupMemberEntry instanceof GroupMemberEntry.UnknownPendingMemberCount) {
return OTHER_INVITE_PENDING_COUNT;
} else if (groupMemberEntry instanceof GroupMemberEntry.NewGroupCandidate) {
return NEW_GROUP_CANDIDATE;
}
throw new AssertionError();
@@ -99,31 +117,34 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
static abstract class ViewHolder extends LifecycleViewHolder {
final Context context;
final AvatarImageView avatar;
final TextView recipient;
final PopupMenuView popupMenu;
final View popupMenuContainer;
final ProgressBar busyProgress;
final View admin;
@Nullable final RecipientClickListener recipientClickListener;
@Nullable final AdminActionsListener adminActionsListener;
final Context context;
final AvatarImageView avatar;
final TextView recipient;
final PopupMenuView popupMenu;
final View popupMenuContainer;
final ProgressBar busyProgress;
final View admin;
@Nullable final RecipientClickListener recipientClickListener;
@Nullable final AdminActionsListener adminActionsListener;
@Nullable final RecipientLongClickListener recipientLongClickListener;
ViewHolder(@NonNull View itemView,
@Nullable RecipientClickListener recipientClickListener,
@Nullable RecipientLongClickListener recipientLongClickListener,
@Nullable AdminActionsListener adminActionsListener)
{
super(itemView);
this.context = itemView.getContext();
this.avatar = itemView.findViewById(R.id.recipient_avatar);
this.recipient = itemView.findViewById(R.id.recipient_name);
this.popupMenu = itemView.findViewById(R.id.popupMenu);
this.popupMenuContainer = itemView.findViewById(R.id.popupMenuProgressContainer);
this.busyProgress = itemView.findViewById(R.id.menuBusyProgress);
this.admin = itemView.findViewById(R.id.admin);
this.recipientClickListener = recipientClickListener;
this.adminActionsListener = adminActionsListener;
this.context = itemView.getContext();
this.avatar = itemView.findViewById(R.id.recipient_avatar);
this.recipient = itemView.findViewById(R.id.recipient_name);
this.popupMenu = itemView.findViewById(R.id.popupMenu);
this.popupMenuContainer = itemView.findViewById(R.id.popupMenuProgressContainer);
this.busyProgress = itemView.findViewById(R.id.menuBusyProgress);
this.admin = itemView.findViewById(R.id.admin);
this.recipientClickListener = recipientClickListener;
this.recipientLongClickListener = recipientLongClickListener;
this.adminActionsListener = adminActionsListener;
}
void bindRecipient(@NonNull Recipient recipient) {
@@ -149,6 +170,13 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
recipientClickListener.onClick(recipient);
}
});
this.itemView.setOnLongClickListener(v -> {
if (recipientLongClickListener != null && getAdapterPosition() != RecyclerView.NO_POSITION) {
return recipientLongClickListener.onLongClick(recipient);
}
return false;
});
}
void bind(@NonNull GroupMemberEntry memberEntry) {
@@ -179,9 +207,10 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
FullMemberViewHolder(@NonNull View itemView,
@Nullable RecipientClickListener recipientClickListener,
@Nullable RecipientLongClickListener recipientLongClickListener,
@Nullable AdminActionsListener adminActionsListener)
{
super(itemView, recipientClickListener, adminActionsListener);
super(itemView, recipientClickListener, recipientLongClickListener, adminActionsListener);
}
@Override
@@ -195,14 +224,46 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
admin.setVisibility(fullMember.isAdmin() ? View.VISIBLE : View.INVISIBLE);
}
}
final static class NewGroupInviteeViewHolder extends ViewHolder {
private final View smsContact;
private final View smsWarning;
NewGroupInviteeViewHolder(@NonNull View itemView,
@Nullable RecipientClickListener recipientClickListener,
@Nullable RecipientLongClickListener recipientLongClickListener)
{
super(itemView, recipientClickListener, recipientLongClickListener, null);
smsContact = itemView.findViewById(R.id.sms_contact);
smsWarning = itemView.findViewById(R.id.sms_warning);
}
@Override
void bind(@NonNull GroupMemberEntry memberEntry) {
GroupMemberEntry.NewGroupCandidate newGroupCandidate = (GroupMemberEntry.NewGroupCandidate) memberEntry;
bindRecipient(newGroupCandidate.getMember());
bindRecipientClick(newGroupCandidate.getMember());
itemView.setSelected(false);
newGroupCandidate.isSelected().observe(this, itemView::setSelected);
int smsWarningVisibility = newGroupCandidate.getMember().isRegistered() ? View.GONE : View.VISIBLE;
smsContact.setVisibility(smsWarningVisibility);
smsWarning.setVisibility(smsWarningVisibility);
}
}
final static class OwnInvitePendingMemberViewHolder extends ViewHolder {
OwnInvitePendingMemberViewHolder(@NonNull View itemView,
@Nullable RecipientClickListener recipientClickListener,
@Nullable RecipientLongClickListener recipientLongClickListener,
@Nullable AdminActionsListener adminActionsListener)
{
super(itemView, recipientClickListener, adminActionsListener);
super(itemView, recipientClickListener, recipientLongClickListener, adminActionsListener);
}
@Override
@@ -231,7 +292,7 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
final static class UnknownPendingMemberCountViewHolder extends ViewHolder {
UnknownPendingMemberCountViewHolder(@NonNull View itemView, @Nullable AdminActionsListener adminActionsListener) {
super(itemView, null, adminActionsListener);
super(itemView, null, null, adminActionsListener);
}
@Override

View File

@@ -59,6 +59,10 @@ public final class GroupMemberListView extends RecyclerView {
membersAdapter.setRecipientClickListener(listener);
}
public void setRecipientLongClickListener(@Nullable RecipientLongClickListener listener) {
membersAdapter.setRecipientLongClickListener(listener);
}
public void setMembers(@NonNull List<? extends GroupMemberEntry> recipients) {
membersAdapter.updateData(recipients);
}

View File

@@ -0,0 +1,9 @@
package org.thoughtcrime.securesms.groups.ui;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.recipients.Recipient;
public interface RecipientLongClickListener {
boolean onLongClick(@NonNull Recipient recipient);
}

View File

@@ -0,0 +1,107 @@
package org.thoughtcrime.securesms.groups.ui.creategroup;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.ContactSelectionActivity;
import org.thoughtcrime.securesms.ContactSelectionListFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
import org.thoughtcrime.securesms.groups.ui.creategroup.details.AddGroupDetailsActivity;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.util.guava.Optional;
public class CreateGroupActivity extends ContactSelectionActivity {
private static final int MINIMUM_GROUP_SIZE = 1;
private static final short REQUEST_CODE_ADD_DETAILS = 17275;
private View next;
public static Intent newIntent(@NonNull Context context) {
Intent intent = new Intent(context, CreateGroupActivity.class);
intent.putExtra(ContactSelectionListFragment.MULTI_SELECT, true);
intent.putExtra(ContactSelectionListFragment.REFRESHABLE, false);
intent.putExtra(ContactSelectionActivity.EXTRA_LAYOUT_RES_ID, R.layout.create_group_activity);
int displayMode = TextSecurePreferences.isSmsEnabled(context) ? ContactsCursorLoader.DisplayMode.FLAG_SMS | ContactsCursorLoader.DisplayMode.FLAG_PUSH
: ContactsCursorLoader.DisplayMode.FLAG_PUSH;
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
return intent;
}
@Override
public void onCreate(Bundle bundle, boolean ready) {
super.onCreate(bundle, ready);
assert getSupportActionBar() != null;
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
next = findViewById(R.id.next);
disableNext();
next.setOnClickListener(v -> handleNextPressed());
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == REQUEST_CODE_ADD_DETAILS && resultCode == RESULT_OK) {
finish();
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
@Override
public void onContactSelected(Optional<RecipientId> recipientId, String number) {
if (contactsFragment.getSelectedContactsCount() >= MINIMUM_GROUP_SIZE) {
enableNext();
}
}
@Override
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {
if (contactsFragment.getSelectedContactsCount() < MINIMUM_GROUP_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() {
RecipientId[] ids = Stream.of(contactsFragment.getSelectedContacts())
.map(selectedContact -> selectedContact.getOrCreateRecipientId(this))
.toArray(RecipientId[]::new);
startActivityForResult(AddGroupDetailsActivity.newIntent(this, ids), REQUEST_CODE_ADD_DETAILS);
}
}

View File

@@ -0,0 +1,78 @@
package org.thoughtcrime.securesms.groups.ui.creategroup.details;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.navigation.NavGraph;
import androidx.navigation.Navigation;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
public class AddGroupDetailsActivity extends PassphraseRequiredActionBarActivity implements AddGroupDetailsFragment.Callback {
private static final String EXTRA_RECIPIENTS = "recipient_ids";
private final DynamicTheme theme = new DynamicNoActionBarTheme();
public static Intent newIntent(@NonNull Context context, @NonNull RecipientId[] recipients) {
Intent intent = new Intent(context, AddGroupDetailsActivity.class);
intent.putExtra(EXTRA_RECIPIENTS, recipients);
return intent;
}
@Override
protected void onCreate(@Nullable Bundle bundle, boolean ready) {
theme.onCreate(this);
setContentView(R.layout.add_group_details_activity);
if (bundle == null) {
Parcelable[] parcelables = getIntent().getParcelableArrayExtra(EXTRA_RECIPIENTS);
RecipientId[] ids = new RecipientId[parcelables.length];
System.arraycopy(parcelables, 0, ids, 0, parcelables.length);
AddGroupDetailsFragmentArgs arguments = new AddGroupDetailsFragmentArgs.Builder(ids).build();
NavGraph graph = Navigation.findNavController(this, R.id.nav_host_fragment).getGraph();
Navigation.findNavController(this, R.id.nav_host_fragment).setGraph(graph, arguments.toBundle());
}
}
@Override
protected void onResume() {
super.onResume();
theme.onResume(this);
}
@Override
public void onGroupCreated(@NonNull RecipientId recipientId, long threadId) {
Intent intent = ConversationActivity.buildIntent(this,
recipientId,
threadId,
ThreadDatabase.DistributionTypes.DEFAULT,
-1);
startActivity(intent);
setResult(RESULT_OK);
finish();
}
@Override
public void onNavigationButtonPressed() {
setResult(RESULT_CANCELED);
finish();
}
}

View File

@@ -0,0 +1,287 @@
package org.thoughtcrime.securesms.groups.ui.creategroup.details;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.os.Bundle;
import android.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
import com.dd.CircularProgressButton;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity;
import org.thoughtcrime.securesms.mediasend.AvatarSelectionBottomSheetDialogFragment;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
import java.util.Objects;
public class AddGroupDetailsFragment extends Fragment {
private static final int AVATAR_PLACEHOLDER_INSET_DP = 18;
private static final short REQUEST_CODE_AVATAR = 27621;
private static final String ARG_RECIPIENT_IDS = "recipient_ids";
private CircularProgressButton create;
private Callback callback;
private AddGroupDetailsViewModel viewModel;
private Drawable avatarPlaceholder;
private EditText name;
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
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof Callback) {
callback = (Callback) context;
} else {
throw new ClassCastException("Parent context should implement AddGroupDetailsFragment.Callback");
}
}
public static Fragment create(@NonNull RecipientId[] recipientIds) {
AddGroupDetailsFragment fragment = new AddGroupDetailsFragment();
Bundle arguments = new Bundle();
arguments.putParcelableArray(ARG_RECIPIENT_IDS, recipientIds);
fragment.setArguments(arguments);
return fragment;
}
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState)
{
return inflater.inflate(R.layout.add_group_details_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
create = view.findViewById(R.id.create);
name = view.findViewById(R.id.group_name);
toolbar = view.findViewById(R.id.toolbar);
setCreateEnabled(false, false);
GroupMemberListView members = view.findViewById(R.id.member_list);
ImageView avatar = view.findViewById(R.id.group_avatar);
View mmsWarning = view.findViewById(R.id.mms_warning);
avatarPlaceholder = VectorDrawableCompat.create(getResources(), R.drawable.ic_camera_outline_32_ultramarine, requireActivity().getTheme());
if (savedInstanceState == null) {
avatar.setImageDrawable(new InsetDrawable(avatarPlaceholder, ViewUtil.dpToPx(AVATAR_PLACEHOLDER_INSET_DP)));
}
initializeViewModel();
avatar.setOnClickListener(v -> AvatarSelectionBottomSheetDialogFragment.create(false, true, REQUEST_CODE_AVATAR, true)
.show(getChildFragmentManager(), "BOTTOM"));
members.setRecipientLongClickListener(this::handleRecipientLongClick);
members.setRecipientClickListener(this::handleRecipientClick);
name.addTextChangedListener(new AfterTextChanged(editable -> viewModel.setName(editable.toString())));
toolbar.setNavigationOnClickListener(unused -> callback.onNavigationButtonPressed());
create.setOnClickListener(v -> handleCreateClicked());
viewModel.getMembers().observe(getViewLifecycleOwner(), members::setMembers);
viewModel.getCanSubmitForm().observe(getViewLifecycleOwner(), isFormValid -> setCreateEnabled(isFormValid, true));
viewModel.getIsMms().observe(getViewLifecycleOwner(), isMms -> mmsWarning.setVisibility(isMms ? View.VISIBLE : View.GONE));
viewModel.getAvatar().observe(getViewLifecycleOwner(), avatarBytes -> {
if (avatarBytes == null) {
avatar.setImageDrawable(new InsetDrawable(avatarPlaceholder, ViewUtil.dpToPx(AVATAR_PLACEHOLDER_INSET_DP)));
} else {
GlideApp.with(this)
.load(avatarBytes)
.circleCrop()
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.into(avatar);
}
});
}
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == REQUEST_CODE_AVATAR && resultCode == Activity.RESULT_OK && data != null) {
final Media result = data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA);
final DecryptableStreamUriLoader.DecryptableUri decryptableUri = new DecryptableStreamUriLoader.DecryptableUri(result.getUri());
GlideApp.with(this)
.asBitmap()
.load(decryptableUri)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.centerCrop()
.override(AvatarHelper.AVATAR_DIMENSIONS, AvatarHelper.AVATAR_DIMENSIONS)
.into(new CustomTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, Transition<? super Bitmap> transition) {
viewModel.setAvatar(Objects.requireNonNull(BitmapUtil.toByteArray(resource)));
}
@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
}
});
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
private void initializeViewModel() {
AddGroupDetailsFragmentArgs args = AddGroupDetailsFragmentArgs.fromBundle(requireArguments());
AddGroupDetailsRepository repository = new AddGroupDetailsRepository(requireContext());
AddGroupDetailsViewModel.Factory factory = new AddGroupDetailsViewModel.Factory(args.getRecipientIds(), repository);
viewModel = ViewModelProviders.of(this, factory).get(AddGroupDetailsViewModel.class);
viewModel.getGroupCreateResult().observe(getViewLifecycleOwner(), this::handleGroupCreateResult);
}
private void handleCreateClicked() {
create.setClickable(false);
create.setIndeterminateProgressMode(true);
create.setProgress(50);
viewModel.create();
}
private void handleRecipientClick(@NonNull Recipient recipient) {
if (actionMode == null) {
return;
}
int size = viewModel.toggleSelected(recipient);
if (size == 0) {
actionMode.finish();
}
}
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) {
groupCreateResult.consume(this::handleGroupCreateResultSuccess, this::handleGroupCreateResultError);
}
private void handleGroupCreateResultSuccess(@NonNull GroupCreateResult.Success success) {
callback.onGroupCreated(success.getGroupRecipient().getId(), success.getThreadId());
}
private void handleGroupCreateResultError(@NonNull GroupCreateResult.Error error) {
switch (error.getErrorType()) {
case ERROR_IO:
case ERROR_BUSY:
toast(R.string.AddGroupDetailsFragment__try_again_later);
break;
case ERROR_FAILED:
toast(R.string.AddGroupDetailsFragment__group_creation_failed);
break;
case ERROR_INVALID_NAME:
name.setError(getString(R.string.AddGroupDetailsFragment__this_field_is_required));
break;
case ERROR_INVALID_MEMBER_COUNT:
toast(R.string.AddGroupDetailsFragment__groups_require_at_least_two_members);
break;
default:
throw new IllegalStateException("Unexpected error: " + error.getErrorType().name());
}
}
private void toast(@StringRes int toastStringId) {
Toast.makeText(requireContext(), toastStringId, Toast.LENGTH_SHORT)
.show();
}
private void setCreateEnabled(boolean isEnabled, boolean animate) {
if (create.isEnabled() == isEnabled) {
return;
}
create.setEnabled(isEnabled);
create.animate()
.setDuration(animate ? 300 : 0)
.alpha(isEnabled ? 1f : 0.5f);
}
public interface Callback {
void onGroupCreated(@NonNull RecipientId recipientId, long threadId);
void onNavigationButtonPressed();
}
}

View File

@@ -0,0 +1,67 @@
package org.thoughtcrime.securesms.groups.ui.creategroup.details;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Consumer;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
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.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
final class AddGroupDetailsRepository {
private final Context context;
AddGroupDetailsRepository(@NonNull Context context) {
this.context = context;
}
void resolveMembers(@NonNull RecipientId[] recipientIds, Consumer<List<GroupMemberEntry.NewGroupCandidate>> consumer) {
SignalExecutors.BOUNDED.execute(() -> {
List<GroupMemberEntry.NewGroupCandidate> members = new ArrayList<>(recipientIds.length);
for (RecipientId id : recipientIds) {
members.add(new GroupMemberEntry.NewGroupCandidate(Recipient.resolved(id)));
}
consumer.accept(members);
});
}
void createPushGroup(@NonNull Set<RecipientId> members,
@Nullable byte[] avatar,
@Nullable String name,
boolean mms,
Consumer<GroupCreateResult> resultConsumer)
{
SignalExecutors.BOUNDED.execute(() -> {
Set<Recipient> recipients = new HashSet<>(Stream.of(members).map(Recipient::resolved).toList());
try {
GroupManager.GroupActionResult result = GroupManager.createGroup(context, recipients, avatar, name, mms);
resultConsumer.accept(GroupCreateResult.success(result));
} catch (GroupChangeBusyException e) {
resultConsumer.accept(GroupCreateResult.error(GroupCreateResult.Error.Type.ERROR_BUSY));
} catch (GroupChangeFailedException e) {
resultConsumer.accept(GroupCreateResult.error(GroupCreateResult.Error.Type.ERROR_FAILED));
} catch (IOException e) {
resultConsumer.accept(GroupCreateResult.error(GroupCreateResult.Error.Type.ERROR_IO));
}
});
}
}

View File

@@ -0,0 +1,165 @@
package org.thoughtcrime.securesms.groups.ui.creategroup.details;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
public final class AddGroupDetailsViewModel extends ViewModel {
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 MutableLiveData<String> name = new MutableLiveData<>("");
private final MutableLiveData<byte[]> avatar = new MutableLiveData<>();
private final LiveData<Boolean> isMms;
private final SingleLiveEvent<GroupCreateResult> groupCreateResult = new SingleLiveEvent<>();
private final LiveData<Boolean> canSubmitForm = Transformations.map(name, name -> !TextUtils.isEmpty(name));
private final AddGroupDetailsRepository repository;
AddGroupDetailsViewModel(@NonNull RecipientId[] recipientIds,
@NonNull AddGroupDetailsRepository repository)
{
this.repository = repository;
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);
isMms = Transformations.map(members, this::isAnyForcedSms);
repository.resolveMembers(recipientIds, initialMembers::postValue);
}
@NonNull LiveData<List<GroupMemberEntry.NewGroupCandidate>> getMembers() {
return members;
}
@NonNull LiveData<Boolean> getCanSubmitForm() {
return canSubmitForm;
}
@NonNull LiveData<GroupCreateResult> getGroupCreateResult() {
return groupCreateResult;
}
@NonNull LiveData<byte[]> getAvatar() {
return avatar;
}
@NonNull LiveData<Boolean> getIsMms() {
return isMms;
}
void setAvatar(@NonNull byte[] avatar) {
this.avatar.setValue(avatar);
}
void setName(@NonNull String name) {
this.name.setValue(name);
}
int toggleSelected(@NonNull Recipient recipient) {
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();
deleted.addAll(selected);
this.deleted.setValue(deleted);
}
void create() {
List<GroupMemberEntry.NewGroupCandidate> members = Objects.requireNonNull(this.members.getValue());
Set<RecipientId> memberIds = Stream.of(members).map(member -> member.getMember().getId()).collect(Collectors.toSet());
byte[] avatarBytes = avatar.getValue();
String groupName = name.getValue();
boolean isGroupMms = isMms.getValue() == Boolean.TRUE;
if (TextUtils.isEmpty(groupName)) {
groupCreateResult.postValue(GroupCreateResult.error(GroupCreateResult.Error.Type.ERROR_INVALID_NAME));
return;
}
if (memberIds.isEmpty()) {
groupCreateResult.postValue(GroupCreateResult.error(GroupCreateResult.Error.Type.ERROR_INVALID_MEMBER_COUNT));
return;
}
repository.createPushGroup(memberIds,
avatarBytes,
groupName,
isGroupMms,
groupCreateResult::postValue);
}
private static @NonNull List<GroupMemberEntry.NewGroupCandidate> filterDeletedMembers(@NonNull List<GroupMemberEntry.NewGroupCandidate> members, @NonNull Set<RecipientId> deleted) {
return Stream.of(members)
.filterNot(member -> deleted.contains(member.getMember().getId()))
.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) {
return Stream.of(members)
.anyMatch(member -> !member.getMember().isRegistered());
}
static final class Factory implements ViewModelProvider.Factory {
private final RecipientId[] recipientIds;
private final AddGroupDetailsRepository repository;
Factory(@NonNull RecipientId[] recipientIds, @NonNull AddGroupDetailsRepository repository) {
this.recipientIds = recipientIds;
this.repository = repository;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
return Objects.requireNonNull(modelClass.cast(new AddGroupDetailsViewModel(recipientIds, repository)));
}
}
}

View File

@@ -0,0 +1,77 @@
package org.thoughtcrime.securesms.groups.ui.creategroup.details;
import androidx.annotation.NonNull;
import androidx.core.util.Consumer;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.recipients.Recipient;
abstract class GroupCreateResult {
static GroupCreateResult success(@NonNull GroupManager.GroupActionResult result) {
return new GroupCreateResult.Success(result.getThreadId(), result.getGroupRecipient());
}
static GroupCreateResult error(@NonNull GroupCreateResult.Error.Type errorType) {
return new GroupCreateResult.Error(errorType);
}
private GroupCreateResult() {
}
static final class Success extends GroupCreateResult {
private final long threadId;
private final Recipient groupRecipient;
private Success(long threadId, @NonNull Recipient groupRecipient) {
this.threadId = threadId;
this.groupRecipient = groupRecipient;
}
long getThreadId() {
return threadId;
}
@NonNull Recipient getGroupRecipient() {
return groupRecipient;
}
@Override
void consume(@NonNull Consumer<Success> successConsumer,
@NonNull Consumer<Error> errorConsumer)
{
successConsumer.accept(this);
}
}
static final class Error extends GroupCreateResult {
private final Error.Type errorType;
private Error(Error.Type errorType) {
this.errorType = errorType;
}
@Override
void consume(@NonNull Consumer<Success> successConsumer,
@NonNull Consumer<Error> errorConsumer)
{
errorConsumer.accept(this);
}
public Type getErrorType() {
return errorType;
}
enum Type {
ERROR_IO,
ERROR_BUSY,
ERROR_FAILED,
ERROR_INVALID_NAME,
ERROR_INVALID_MEMBER_COUNT
}
}
abstract void consume(@NonNull Consumer<Success> successConsumer,
@NonNull Consumer<Error> errorConsumer);
}