mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-30 23:36:32 +00:00
Allow pending member invite cancelation.
This commit is contained in:
committed by
Greyson Parrelli
parent
1d63970a25
commit
68d29d9a0f
@@ -7,10 +7,14 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.zkgroup.VerificationFailedException;
|
||||
import org.signal.zkgroup.groups.UuidCiphertext;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
|
||||
import org.whispersystems.signalservice.api.util.InvalidNumberException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
@@ -54,6 +58,15 @@ public final class GroupManager {
|
||||
return V1GroupManager.leaveGroup(context, groupId.requireV1());
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static void cancelInvites(@NonNull Context context,
|
||||
@NonNull GroupId.V2 groupId,
|
||||
@NonNull Collection<UuidCiphertext> uuidCipherTexts)
|
||||
throws InvalidGroupStateException, VerificationFailedException, IOException
|
||||
{
|
||||
throw new AssertionError("NYI"); // TODO: GV2 allow invite cancellation
|
||||
}
|
||||
|
||||
public static class GroupActionResult {
|
||||
private final Recipient groupRecipient;
|
||||
private final long threadId;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.thoughtcrime.securesms.groups.ui;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public interface AdminActionsListener {
|
||||
|
||||
void onCancelInvite(@NonNull GroupMemberEntry.PendingMember pendingMember);
|
||||
|
||||
void onCancelAllInvites(@NonNull GroupMemberEntry.UnknownPendingMemberCount pendingMembers);
|
||||
}
|
||||
@@ -2,12 +2,18 @@ package org.thoughtcrime.securesms.groups.ui;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import org.signal.zkgroup.groups.UuidCiphertext;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public abstract class GroupMemberEntry {
|
||||
|
||||
private @Nullable Runnable onClick;
|
||||
private final DefaultValueLiveData<Boolean> busy = new DefaultValueLiveData<>(false);
|
||||
@Nullable private Runnable onClick;
|
||||
|
||||
private GroupMemberEntry() {
|
||||
}
|
||||
@@ -20,6 +26,14 @@ public abstract class GroupMemberEntry {
|
||||
return onClick;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getBusy() {
|
||||
return busy;
|
||||
}
|
||||
|
||||
public void setBusy(boolean busy) {
|
||||
this.busy.postValue(busy);
|
||||
}
|
||||
|
||||
public final static class FullMember extends GroupMemberEntry {
|
||||
|
||||
private final Recipient member;
|
||||
@@ -34,30 +48,40 @@ public abstract class GroupMemberEntry {
|
||||
}
|
||||
|
||||
public final static class PendingMember extends GroupMemberEntry {
|
||||
private final Recipient invitee;
|
||||
private final byte[] inviteeCipherText;
|
||||
private final Recipient invitee;
|
||||
private final UuidCiphertext inviteeCipherText;
|
||||
private final boolean cancellable;
|
||||
|
||||
public PendingMember(@NonNull Recipient invitee, @NonNull byte[] inviteeCipherText) {
|
||||
public PendingMember(@NonNull Recipient invitee, @NonNull UuidCiphertext inviteeCipherText, boolean cancellable) {
|
||||
this.invitee = invitee;
|
||||
this.inviteeCipherText = inviteeCipherText;
|
||||
this.cancellable = cancellable;
|
||||
}
|
||||
|
||||
public Recipient getInvitee() {
|
||||
return invitee;
|
||||
}
|
||||
|
||||
public byte[] getInviteeCipherText() {
|
||||
public UuidCiphertext getInviteeCipherText() {
|
||||
return inviteeCipherText;
|
||||
}
|
||||
|
||||
public boolean isCancellable() {
|
||||
return cancellable;
|
||||
}
|
||||
}
|
||||
|
||||
public final static class UnknownPendingMemberCount extends GroupMemberEntry {
|
||||
private Recipient inviter;
|
||||
private int inviteCount;
|
||||
private final Recipient inviter;
|
||||
private final Collection<UuidCiphertext> ciphertexts;
|
||||
private final boolean cancellable;
|
||||
|
||||
public UnknownPendingMemberCount(@NonNull Recipient inviter, int inviteCount) {
|
||||
public UnknownPendingMemberCount(@NonNull Recipient inviter,
|
||||
@NonNull Collection<UuidCiphertext> ciphertexts,
|
||||
boolean cancellable) {
|
||||
this.inviter = inviter;
|
||||
this.inviteCount = inviteCount;
|
||||
this.ciphertexts = ciphertexts;
|
||||
this.cancellable = cancellable;
|
||||
}
|
||||
|
||||
public Recipient getInviter() {
|
||||
@@ -65,7 +89,15 @@ public abstract class GroupMemberEntry {
|
||||
}
|
||||
|
||||
public int getInviteCount() {
|
||||
return inviteCount;
|
||||
return ciphertexts.size();
|
||||
}
|
||||
|
||||
public Collection<UuidCiphertext> getCiphertexts() {
|
||||
return ciphertexts;
|
||||
}
|
||||
|
||||
public boolean isCancellable() {
|
||||
return cancellable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,19 +4,22 @@ import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
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 java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
final class GroupMemberListAdapter extends RecyclerView.Adapter<GroupMemberListAdapter.ViewHolder> {
|
||||
final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberListAdapter.ViewHolder> {
|
||||
|
||||
private static final int FULL_MEMBER = 0;
|
||||
private static final int OWN_INVITE_PENDING = 1;
|
||||
@@ -24,6 +27,8 @@ final class GroupMemberListAdapter extends RecyclerView.Adapter<GroupMemberListA
|
||||
|
||||
private final ArrayList<GroupMemberEntry> data = new ArrayList<>();
|
||||
|
||||
@Nullable private AdminActionsListener adminActionsListener;
|
||||
|
||||
void updateData(@NonNull Collection<? extends GroupMemberEntry> recipients) {
|
||||
data.clear();
|
||||
data.addAll(recipients);
|
||||
@@ -36,20 +41,24 @@ final class GroupMemberListAdapter extends RecyclerView.Adapter<GroupMemberListA
|
||||
case FULL_MEMBER:
|
||||
return new FullMemberViewHolder(LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.group_recipient_list_item,
|
||||
parent, false));
|
||||
parent, false), adminActionsListener);
|
||||
case OWN_INVITE_PENDING:
|
||||
return new OwnInvitePendingMemberViewHolder(LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.group_recipient_list_item,
|
||||
parent, false));
|
||||
parent, false), adminActionsListener);
|
||||
case OTHER_INVITE_PENDING_COUNT:
|
||||
return new UnknownPendingMemberCountViewHolder(LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.group_recipient_list_item,
|
||||
parent, false));
|
||||
parent, false), adminActionsListener);
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
void setAdminActionsListener(@Nullable AdminActionsListener adminActionsListener) {
|
||||
this.adminActionsListener = adminActionsListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
holder.bind(data.get(position));
|
||||
@@ -75,20 +84,26 @@ final class GroupMemberListAdapter extends RecyclerView.Adapter<GroupMemberListA
|
||||
return data.size();
|
||||
}
|
||||
|
||||
static abstract class ViewHolder extends RecyclerView.ViewHolder {
|
||||
static abstract class ViewHolder extends LifecycleViewHolder {
|
||||
|
||||
final Context context;
|
||||
private final AvatarImageView avatar;
|
||||
private final TextView recipient;
|
||||
final PopupMenuView popupMenu;
|
||||
final Context context;
|
||||
private final AvatarImageView avatar;
|
||||
private final TextView recipient;
|
||||
final PopupMenuView popupMenu;
|
||||
final View popupMenuContainer;
|
||||
final ProgressBar busyProgress;
|
||||
@Nullable final AdminActionsListener adminActionsListener;
|
||||
|
||||
ViewHolder(@NonNull View itemView) {
|
||||
ViewHolder(@NonNull View itemView, @Nullable AdminActionsListener adminActionsListener) {
|
||||
super(itemView);
|
||||
|
||||
context = itemView.getContext();
|
||||
avatar = itemView.findViewById(R.id.recipient_avatar);
|
||||
recipient = itemView.findViewById(R.id.recipient_name);
|
||||
popupMenu = itemView.findViewById(R.id.popupMenu);
|
||||
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.adminActionsListener = adminActionsListener;
|
||||
}
|
||||
|
||||
void bindRecipient(@NonNull Recipient recipient) {
|
||||
@@ -103,18 +118,36 @@ final class GroupMemberListAdapter extends RecyclerView.Adapter<GroupMemberListA
|
||||
}
|
||||
|
||||
void bind(@NonNull GroupMemberEntry memberEntry) {
|
||||
busyProgress.setVisibility(View.GONE);
|
||||
hideMenu();
|
||||
|
||||
Runnable onClick = memberEntry.getOnClick();
|
||||
View.OnClickListener onClickListener = v -> { if (onClick != null) onClick.run(); };
|
||||
|
||||
this.avatar.setOnClickListener(onClickListener);
|
||||
this.recipient.setOnClickListener(onClickListener);
|
||||
avatar.setOnClickListener(onClickListener);
|
||||
recipient.setOnClickListener(onClickListener);
|
||||
|
||||
memberEntry.getBusy().observe(this, busy -> {
|
||||
busyProgress.setVisibility(busy ? View.VISIBLE : View.GONE);
|
||||
popupMenu.setVisibility(busy ? View.GONE : View.VISIBLE);
|
||||
});
|
||||
}
|
||||
|
||||
void hideMenu() {
|
||||
popupMenuContainer.setVisibility(View.GONE);
|
||||
popupMenu.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
void showMenu() {
|
||||
popupMenuContainer.setVisibility(View.VISIBLE);
|
||||
popupMenu.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
final static class FullMemberViewHolder extends ViewHolder {
|
||||
|
||||
FullMemberViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
FullMemberViewHolder(@NonNull View itemView, @Nullable AdminActionsListener adminActionsListener) {
|
||||
super(itemView, adminActionsListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -129,8 +162,8 @@ final class GroupMemberListAdapter extends RecyclerView.Adapter<GroupMemberListA
|
||||
|
||||
final static class OwnInvitePendingMemberViewHolder extends ViewHolder {
|
||||
|
||||
OwnInvitePendingMemberViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
OwnInvitePendingMemberViewHolder(@NonNull View itemView, @Nullable AdminActionsListener adminActionsListener) {
|
||||
super(itemView, adminActionsListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -140,27 +173,59 @@ final class GroupMemberListAdapter extends RecyclerView.Adapter<GroupMemberListA
|
||||
GroupMemberEntry.PendingMember pendingMember = (GroupMemberEntry.PendingMember) memberEntry;
|
||||
|
||||
bindRecipient(pendingMember.getInvitee());
|
||||
|
||||
if (pendingMember.isCancellable() && adminActionsListener != null) {
|
||||
popupMenu.setMenu(R.menu.own_invite_pending_menu,
|
||||
item -> {
|
||||
if (item == R.id.cancel_invite) {
|
||||
adminActionsListener.onCancelInvite(pendingMember);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
showMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final static class UnknownPendingMemberCountViewHolder extends ViewHolder {
|
||||
|
||||
UnknownPendingMemberCountViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
UnknownPendingMemberCountViewHolder(@NonNull View itemView, @Nullable AdminActionsListener adminActionsListener) {
|
||||
super(itemView, adminActionsListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
void bind(@NonNull GroupMemberEntry memberEntry) {
|
||||
super.bind(memberEntry);
|
||||
GroupMemberEntry.UnknownPendingMemberCount pendingMemberCount = (GroupMemberEntry.UnknownPendingMemberCount) memberEntry;
|
||||
GroupMemberEntry.UnknownPendingMemberCount pendingMembers = (GroupMemberEntry.UnknownPendingMemberCount) memberEntry;
|
||||
|
||||
Recipient inviter = pendingMemberCount.getInviter();
|
||||
Recipient inviter = pendingMembers.getInviter();
|
||||
String displayName = inviter.getDisplayName(itemView.getContext());
|
||||
String displayText = context.getResources().getQuantityString(R.plurals.GroupMemberList_invited,
|
||||
pendingMemberCount.getInviteCount(),
|
||||
displayName, pendingMemberCount.getInviteCount());
|
||||
pendingMembers.getInviteCount(),
|
||||
displayName, pendingMembers.getInviteCount());
|
||||
|
||||
bindImageAndText(inviter, displayText);
|
||||
|
||||
if (pendingMembers.isCancellable() && adminActionsListener != null) {
|
||||
popupMenu.setMenu(R.menu.others_invite_pending_menu,
|
||||
item -> {
|
||||
if (item.getItemId() == R.id.cancel_invites) {
|
||||
item.setTitle(context.getResources().getQuantityString(R.plurals.PendingMembersActivity_cancel_invites, pendingMembers.getInviteCount(),
|
||||
pendingMembers.getInviteCount()));
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
item -> {
|
||||
if (item == R.id.cancel_invites) {
|
||||
adminActionsListener.onCancelAllInvites(pendingMembers);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
showMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,10 @@ public final class GroupMemberListView extends RecyclerView {
|
||||
}
|
||||
}
|
||||
|
||||
public void setAdminActionsListener(@Nullable AdminActionsListener adminActionsListener) {
|
||||
membersAdapter.setAdminActionsListener(adminActionsListener);
|
||||
}
|
||||
|
||||
public void setMembers(@NonNull Collection<? extends GroupMemberEntry> recipients) {
|
||||
membersAdapter.updateData(recipients);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package org.thoughtcrime.securesms.groups.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
@@ -15,8 +17,9 @@ import org.thoughtcrime.securesms.R;
|
||||
|
||||
public final class PopupMenuView extends View {
|
||||
|
||||
private @MenuRes int menu;
|
||||
private @Nullable ItemClick callback;
|
||||
@MenuRes private int menu;
|
||||
@Nullable private PrepareOptionsMenuItem prepareOptionsMenuItemCallback;
|
||||
@Nullable private ItemClick callback;
|
||||
|
||||
public PopupMenuView(Context context) {
|
||||
super(context);
|
||||
@@ -43,6 +46,16 @@ public final class PopupMenuView extends View {
|
||||
|
||||
inflater.inflate(menu, popup.getMenu());
|
||||
|
||||
if (prepareOptionsMenuItemCallback != null) {
|
||||
Menu menu = popup.getMenu();
|
||||
for (int i = menu.size() - 1; i >= 0; i--) {
|
||||
MenuItem item = menu.getItem(i);
|
||||
if (!prepareOptionsMenuItemCallback.onPrepareOptionsMenuItem(item)) {
|
||||
menu.removeItem(item.getItemId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
popup.setOnMenuItemClickListener(item -> callback.onItemClick(item.getItemId()));
|
||||
popup.show();
|
||||
}
|
||||
@@ -50,8 +63,25 @@ public final class PopupMenuView extends View {
|
||||
}
|
||||
|
||||
public void setMenu(@MenuRes int menu, @NonNull ItemClick callback) {
|
||||
this.menu = menu;
|
||||
this.callback = callback;
|
||||
this.menu = menu;
|
||||
this.prepareOptionsMenuItemCallback = null;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public void setMenu(@MenuRes int menu, @NonNull PrepareOptionsMenuItem prepareOptionsMenuItem, @NonNull ItemClick callback) {
|
||||
this.menu = menu;
|
||||
this.prepareOptionsMenuItemCallback = prepareOptionsMenuItem;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public interface PrepareOptionsMenuItem {
|
||||
|
||||
/**
|
||||
* Chance to change the {@link MenuItem} after inflation.
|
||||
*
|
||||
* @return true to keep the {@link MenuItem}. false to remove the {@link MenuItem}.
|
||||
*/
|
||||
boolean onPrepareOptionsMenuItem(@NonNull MenuItem menuItem);
|
||||
}
|
||||
|
||||
public interface ItemClick {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.thoughtcrime.securesms.groups.ui.pendingmemberinvites;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
final class InviteCancelConfirmationDialog {
|
||||
|
||||
private InviteCancelConfirmationDialog() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms that you want to cancel an invite that you sent.
|
||||
*/
|
||||
static AlertDialog showOwnInviteCancelConfirmationDialog(@NonNull Context context,
|
||||
@NonNull Recipient invitee,
|
||||
@NonNull Runnable onCancel)
|
||||
{
|
||||
return new AlertDialog.Builder(context)
|
||||
.setMessage(context.getString(R.string.GroupManagement_cancel_own_single_invite,
|
||||
invitee.getDisplayName(context)))
|
||||
.setPositiveButton(R.string.yes, (dialog, which) -> onCancel.run())
|
||||
.setNegativeButton(R.string.no, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms that you want to cancel a number of invites that another member sent.
|
||||
*/
|
||||
static AlertDialog showOthersInviteCancelConfirmationDialog(@NonNull Context context,
|
||||
@NonNull Recipient inviter,
|
||||
int numberOfInvitations,
|
||||
@NonNull Runnable onCancel)
|
||||
{
|
||||
return new AlertDialog.Builder(context)
|
||||
.setMessage(context.getResources().getQuantityString(R.plurals.GroupManagement_cancel_others_invites,
|
||||
numberOfInvitations,
|
||||
inviter.getDisplayName(context),
|
||||
numberOfInvitations))
|
||||
.setPositiveButton(R.string.yes, (dialog, which) -> onCancel.run())
|
||||
.setNegativeButton(R.string.no, null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.ui.AdminActionsListener;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
|
||||
|
||||
import java.util.Objects;
|
||||
@@ -45,6 +47,32 @@ public class PendingMemberInvitesFragment extends Fragment {
|
||||
youInvitedEmptyState = view.findViewById(R.id.no_pending_from_you);
|
||||
othersInvitedEmptyState = view.findViewById(R.id.no_pending_from_others);
|
||||
|
||||
youInvited.setAdminActionsListener(new AdminActionsListener() {
|
||||
|
||||
@Override
|
||||
public void onCancelInvite(@NonNull GroupMemberEntry.PendingMember pendingMember) {
|
||||
viewModel.cancelInviteFor(pendingMember);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancelAllInvites(@NonNull GroupMemberEntry.UnknownPendingMemberCount pendingMembers) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
});
|
||||
|
||||
othersInvited.setAdminActionsListener(new AdminActionsListener() {
|
||||
|
||||
@Override
|
||||
public void onCancelInvite(@NonNull GroupMemberEntry.PendingMember pendingMember) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancelAllInvites(@NonNull GroupMemberEntry.UnknownPendingMemberCount pendingMembers) {
|
||||
viewModel.cancelInvitesFor(pendingMembers);
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
package org.thoughtcrime.securesms.groups.ui.pendingmemberinvites;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.signal.zkgroup.groups.UuidCiphertext;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class PendingMemberInvitesViewModel extends ViewModel {
|
||||
|
||||
private static final String TAG = Log.tag(PendingMemberInvitesViewModel.class);
|
||||
private final Context context;
|
||||
private final PendingMemberRepository pendingMemberRepository;
|
||||
private final DefaultValueLiveData<List<GroupMemberEntry.PendingMember>> whoYouInvited = new DefaultValueLiveData<>(Collections.emptyList());
|
||||
private final DefaultValueLiveData<List<GroupMemberEntry.UnknownPendingMemberCount>> whoOthersInvited = new DefaultValueLiveData<>(Collections.emptyList());
|
||||
|
||||
private final Context context;
|
||||
private final GroupId groupId;
|
||||
private final PendingMemberRepository pendingMemberRepository;
|
||||
private final MutableLiveData<List<GroupMemberEntry.PendingMember>> whoYouInvited = new MutableLiveData<>();
|
||||
private final MutableLiveData<List<GroupMemberEntry.UnknownPendingMemberCount>> whoOthersInvited = new MutableLiveData<>();
|
||||
|
||||
PendingMemberInvitesViewModel(@NonNull Context context,
|
||||
@NonNull GroupId.V2 groupId,
|
||||
@NonNull PendingMemberRepository pendingMemberRepository)
|
||||
private PendingMemberInvitesViewModel(@NonNull Context context,
|
||||
@NonNull PendingMemberRepository pendingMemberRepository)
|
||||
{
|
||||
this.context = context;
|
||||
this.groupId = groupId;
|
||||
this.pendingMemberRepository = pendingMemberRepository;
|
||||
|
||||
pendingMemberRepository.getInvitees(groupId, this::setMembers);
|
||||
pendingMemberRepository.getInvitees(this::setMembers);
|
||||
}
|
||||
|
||||
public LiveData<List<GroupMemberEntry.PendingMember>> getWhoYouInvited() {
|
||||
@@ -55,17 +55,86 @@ public class PendingMemberInvitesViewModel extends ViewModel {
|
||||
|
||||
for (PendingMemberRepository.SinglePendingMemberInvitedByYou pendingMember : inviteeResult.getByMe()) {
|
||||
byMe.add(new GroupMemberEntry.PendingMember(pendingMember.getInvitee(),
|
||||
pendingMember.getInviteeCipherText()));
|
||||
pendingMember.getInviteeCipherText(),
|
||||
true));
|
||||
}
|
||||
|
||||
for (PendingMemberRepository.MultiplePendingMembersInvitedByAnother pendingMembers : inviteeResult.getByOthers()) {
|
||||
byOthers.add(new GroupMemberEntry.UnknownPendingMemberCount(pendingMembers.getInviter(),
|
||||
pendingMembers.getUuidCipherTexts().size()));
|
||||
pendingMembers.getUuidCipherTexts(),
|
||||
inviteeResult.isCanCancelOthersInvites()));
|
||||
}
|
||||
|
||||
setInvitees(byMe, byOthers);
|
||||
}
|
||||
|
||||
void cancelInviteFor(@NonNull GroupMemberEntry.PendingMember pendingMember) {
|
||||
UuidCiphertext inviteeCipherText = pendingMember.getInviteeCipherText();
|
||||
|
||||
InviteCancelConfirmationDialog.showOwnInviteCancelConfirmationDialog(context, pendingMember.getInvitee(), () ->
|
||||
SimpleTask.run(
|
||||
() -> {
|
||||
pendingMember.setBusy(true);
|
||||
try {
|
||||
return pendingMemberRepository.cancelInvites(Collections.singleton(inviteeCipherText));
|
||||
} finally {
|
||||
pendingMember.setBusy(false);
|
||||
}
|
||||
},
|
||||
result -> {
|
||||
if (result) {
|
||||
ArrayList<GroupMemberEntry.PendingMember> newList = new ArrayList<>(whoYouInvited.getValue());
|
||||
Iterator<GroupMemberEntry.PendingMember> iterator = newList.iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
if (iterator.next().getInviteeCipherText().equals(inviteeCipherText)) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
whoYouInvited.setValue(newList);
|
||||
} else {
|
||||
toastErrorCanceling(1);
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
void cancelInvitesFor(@NonNull GroupMemberEntry.UnknownPendingMemberCount pendingMembers) {
|
||||
InviteCancelConfirmationDialog.showOthersInviteCancelConfirmationDialog(context, pendingMembers.getInviter(), pendingMembers.getInviteCount(),
|
||||
() -> SimpleTask.run(
|
||||
() -> {
|
||||
pendingMembers.setBusy(true);
|
||||
try {
|
||||
return pendingMemberRepository.cancelInvites(pendingMembers.getCiphertexts());
|
||||
} finally {
|
||||
pendingMembers.setBusy(false);
|
||||
}
|
||||
},
|
||||
result -> {
|
||||
if (result) {
|
||||
ArrayList<GroupMemberEntry.UnknownPendingMemberCount> newList = new ArrayList<>(whoOthersInvited.getValue());
|
||||
Iterator<GroupMemberEntry.UnknownPendingMemberCount> iterator = newList.iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
if (iterator.next().getInviter().equals(pendingMembers.getInviter())) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
whoOthersInvited.setValue(newList);
|
||||
} else {
|
||||
toastErrorCanceling(pendingMembers.getInviteCount());
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
private void toastErrorCanceling(int quantity) {
|
||||
Toast.makeText(context, context.getResources().getQuantityText(R.plurals.PendingMembersActivity_error_canceling_invite, quantity), Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
}
|
||||
|
||||
public static class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
private final Context context;
|
||||
@@ -79,7 +148,7 @@ public class PendingMemberInvitesViewModel extends ViewModel {
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection unchecked
|
||||
return (T) new PendingMemberInvitesViewModel(context, groupId, new PendingMemberRepository(context.getApplicationContext()));
|
||||
return (T) new PendingMemberInvitesViewModel(context, new PendingMemberRepository(context.getApplicationContext(), groupId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.groups.ui.pendingmemberinvites;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
@@ -10,29 +11,45 @@ import com.google.protobuf.ByteString;
|
||||
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.VerificationFailedException;
|
||||
import org.signal.zkgroup.groups.UuidCiphertext;
|
||||
import org.signal.zkgroup.util.UUIDUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||
import org.thoughtcrime.securesms.groups.GroupProtoUtil;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* Repository for modifying the pending members on a single group.
|
||||
*/
|
||||
final class PendingMemberRepository {
|
||||
|
||||
private final Context context;
|
||||
private final Executor executor;
|
||||
private static final String TAG = Log.tag(PendingMemberRepository.class);
|
||||
|
||||
PendingMemberRepository(@NonNull Context context) {
|
||||
private final Context context;
|
||||
private final GroupId.V2 groupId;
|
||||
private final Executor executor;
|
||||
|
||||
PendingMemberRepository(@NonNull Context context, @NonNull GroupId.V2 groupId) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.executor = SignalExecutors.BOUNDED;
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
public void getInvitees(GroupId.V2 groupId, @NonNull Consumer<InviteeResult> onInviteesLoaded) {
|
||||
public void getInvitees(@NonNull Consumer<InviteeResult> onInviteesLoaded) {
|
||||
executor.execute(() -> {
|
||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
GroupDatabase.V2GroupProperties v2GroupProperties = groupDatabase.getGroup(groupId).get().requireV2GroupProperties();
|
||||
@@ -41,6 +58,7 @@ final class PendingMemberRepository {
|
||||
List<SinglePendingMemberInvitedByYou> byMe = new ArrayList<>(pendingMembersList.size());
|
||||
List<MultiplePendingMembersInvitedByAnother> byOthers = new ArrayList<>(pendingMembersList.size());
|
||||
ByteString self = ByteString.copyFrom(UUIDUtil.serialize(Recipient.self().getUuid().get()));
|
||||
boolean selfIsAdmin = v2GroupProperties.isAdmin(Recipient.self());
|
||||
|
||||
Stream.of(pendingMembersList)
|
||||
.groupBy(DecryptedPendingMember::getAddedByUuid)
|
||||
@@ -51,17 +69,25 @@ final class PendingMemberRepository {
|
||||
|
||||
if (self.equals(inviterUuid)) {
|
||||
for (DecryptedPendingMember pendingMember : invitedMembers) {
|
||||
Recipient invitee = GroupProtoUtil.pendingMemberToRecipient(context, pendingMember);
|
||||
byte[] uuidCipherText = pendingMember.getUuidCipherText().toByteArray();
|
||||
try {
|
||||
Recipient invitee = GroupProtoUtil.pendingMemberToRecipient(context, pendingMember);
|
||||
UuidCiphertext uuidCipherText = new UuidCiphertext(pendingMember.getUuidCipherText().toByteArray());
|
||||
|
||||
byMe.add(new SinglePendingMemberInvitedByYou(invitee, uuidCipherText));
|
||||
byMe.add(new SinglePendingMemberInvitedByYou(invitee, uuidCipherText));
|
||||
} catch (InvalidInputException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Recipient inviter = GroupProtoUtil.uuidByteStringToRecipient(context, inviterUuid);
|
||||
Recipient inviter = GroupProtoUtil.uuidByteStringToRecipient(context, inviterUuid);
|
||||
ArrayList<UuidCiphertext> uuidCipherTexts = new ArrayList<>(invitedMembers.size());
|
||||
|
||||
ArrayList<byte[]> uuidCipherTexts = new ArrayList<>(invitedMembers.size());
|
||||
for (DecryptedPendingMember pendingMember : invitedMembers) {
|
||||
uuidCipherTexts.add(pendingMember.getUuidCipherText().toByteArray());
|
||||
try {
|
||||
uuidCipherTexts.add(new UuidCiphertext(pendingMember.getUuidCipherText().toByteArray()));
|
||||
} catch (InvalidInputException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
byOthers.add(new MultiplePendingMembersInvitedByAnother(inviter, uuidCipherTexts));
|
||||
@@ -69,19 +95,33 @@ final class PendingMemberRepository {
|
||||
}
|
||||
);
|
||||
|
||||
onInviteesLoaded.accept(new InviteeResult(byMe, byOthers));
|
||||
onInviteesLoaded.accept(new InviteeResult(byMe, byOthers, selfIsAdmin));
|
||||
});
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
boolean cancelInvites(@NonNull Collection<UuidCiphertext> uuidCipherTexts) {
|
||||
try {
|
||||
GroupManager.cancelInvites(context, groupId, uuidCipherTexts);
|
||||
return true;
|
||||
} catch (InvalidGroupStateException | VerificationFailedException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class InviteeResult {
|
||||
private final List<SinglePendingMemberInvitedByYou> byMe;
|
||||
private final List<MultiplePendingMembersInvitedByAnother> byOthers;
|
||||
private final boolean canCancelOthersInvites;
|
||||
|
||||
private InviteeResult(List<SinglePendingMemberInvitedByYou> byMe,
|
||||
List<MultiplePendingMembersInvitedByAnother> byOthers)
|
||||
List<MultiplePendingMembersInvitedByAnother> byOthers,
|
||||
boolean canCancelOthersInvites)
|
||||
{
|
||||
this.byMe = byMe;
|
||||
this.byOthers = byOthers;
|
||||
this.byMe = byMe;
|
||||
this.byOthers = byOthers;
|
||||
this.canCancelOthersInvites = canCancelOthersInvites;
|
||||
}
|
||||
|
||||
public List<SinglePendingMemberInvitedByYou> getByMe() {
|
||||
@@ -91,13 +131,17 @@ final class PendingMemberRepository {
|
||||
public List<MultiplePendingMembersInvitedByAnother> getByOthers() {
|
||||
return byOthers;
|
||||
}
|
||||
|
||||
public boolean isCanCancelOthersInvites() {
|
||||
return canCancelOthersInvites;
|
||||
}
|
||||
}
|
||||
|
||||
public final static class SinglePendingMemberInvitedByYou {
|
||||
private final Recipient invitee;
|
||||
private final byte[] inviteeCipherText;
|
||||
private final Recipient invitee;
|
||||
private final UuidCiphertext inviteeCipherText;
|
||||
|
||||
private SinglePendingMemberInvitedByYou(@NonNull Recipient invitee, @NonNull byte[] inviteeCipherText) {
|
||||
private SinglePendingMemberInvitedByYou(@NonNull Recipient invitee, @NonNull UuidCiphertext inviteeCipherText) {
|
||||
this.invitee = invitee;
|
||||
this.inviteeCipherText = inviteeCipherText;
|
||||
}
|
||||
@@ -106,16 +150,16 @@ final class PendingMemberRepository {
|
||||
return invitee;
|
||||
}
|
||||
|
||||
public byte[] getInviteeCipherText() {
|
||||
public UuidCiphertext getInviteeCipherText() {
|
||||
return inviteeCipherText;
|
||||
}
|
||||
}
|
||||
|
||||
public final static class MultiplePendingMembersInvitedByAnother {
|
||||
private final Recipient inviter;
|
||||
private final ArrayList<byte[]> uuidCipherTexts;
|
||||
private final Recipient inviter;
|
||||
private final Collection<UuidCiphertext> uuidCipherTexts;
|
||||
|
||||
private MultiplePendingMembersInvitedByAnother(@NonNull Recipient inviter, @NonNull ArrayList<byte[]> uuidCipherTexts) {
|
||||
private MultiplePendingMembersInvitedByAnother(@NonNull Recipient inviter, @NonNull Collection<UuidCiphertext> uuidCipherTexts) {
|
||||
this.inviter = inviter;
|
||||
this.uuidCipherTexts = uuidCipherTexts;
|
||||
}
|
||||
@@ -124,7 +168,7 @@ final class PendingMemberRepository {
|
||||
return inviter;
|
||||
}
|
||||
|
||||
public ArrayList<byte[]> getUuidCipherTexts() {
|
||||
public Collection<UuidCiphertext> getUuidCipherTexts() {
|
||||
return uuidCipherTexts;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public abstract class LifecycleRecyclerAdapter<VH extends LifecycleViewHolder> extends RecyclerView.Adapter<VH> {
|
||||
|
||||
@Override
|
||||
public void onViewAttachedToWindow(@NonNull VH holder) {
|
||||
super.onViewAttachedToWindow(holder);
|
||||
holder.onAttachedToWindow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(@NonNull VH holder) {
|
||||
super.onViewDetachedFromWindow(holder);
|
||||
holder.onDetachedFromWindow();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LifecycleRegistry;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public abstract class LifecycleViewHolder extends RecyclerView.ViewHolder implements LifecycleOwner {
|
||||
|
||||
private final LifecycleRegistry lifecycleRegistry;
|
||||
|
||||
public LifecycleViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
lifecycleRegistry = new LifecycleRegistry(this);
|
||||
}
|
||||
|
||||
void onAttachedToWindow() {
|
||||
lifecycleRegistry.setCurrentState(Lifecycle.State.RESUMED);
|
||||
}
|
||||
|
||||
void onDetachedFromWindow() {
|
||||
lifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Lifecycle getLifecycle() {
|
||||
return lifecycleRegistry;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user