Allow pending member invite cancelation.

This commit is contained in:
Alan Evans
2020-04-08 12:56:57 -03:00
committed by Greyson Parrelli
parent 1d63970a25
commit 68d29d9a0f
17 changed files with 538 additions and 87 deletions

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -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);
}

View File

@@ -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 {

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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));
}
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}