mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-08 20:28:33 +00:00
Allow pending member invite cancelation.
This commit is contained in:
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;
|
||||
}
|
||||
}
|
@ -25,14 +25,14 @@
|
||||
android:textColor="?title_text_color_primary"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/recipient_avatar"
|
||||
app:layout_constraintEnd_toStartOf="@+id/popupMenu"
|
||||
app:layout_constraintEnd_toStartOf="@+id/popupMenuProgressContainer"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toEndOf="@+id/recipient_avatar"
|
||||
app:layout_constraintTop_toTopOf="@+id/recipient_avatar"
|
||||
tools:text="@tools:sample/full_names" />
|
||||
|
||||
<org.thoughtcrime.securesms.groups.ui.PopupMenuView
|
||||
android:id="@+id/popupMenu"
|
||||
<FrameLayout
|
||||
android:id="@+id/popupMenuProgressContainer"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
@ -40,6 +40,27 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible" />
|
||||
tools:visibility="visible">
|
||||
|
||||
<org.thoughtcrime.securesms.groups.ui.PopupMenuView
|
||||
android:id="@+id/popupMenu"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/menuBusyProgress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
8
app/src/main/res/menu/others_invite_pending_menu.xml
Normal file
8
app/src/main/res/menu/others_invite_pending_menu.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/cancel_invites"
|
||||
android:title="@string/PendingMembersActivity_cancel_invites" />
|
||||
|
||||
</menu>
|
8
app/src/main/res/menu/own_invite_pending_menu.xml
Normal file
8
app/src/main/res/menu/own_invite_pending_menu.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/cancel_invite"
|
||||
android:title="@string/PendingMembersActivity_cancel_invite" />
|
||||
|
||||
</menu>
|
@ -456,11 +456,29 @@
|
||||
<string name="PendingMembersActivity_no_pending_invites_by_other_group_members">No pending invites by other group members.</string>
|
||||
<string name="PendingMembersActivity_missing_detail_explanation">Details of people invited by other group members are not shown. If invitees choose to join, their information will be shared with the group at that time. They will not see any messages in the group until they join.</string>
|
||||
|
||||
<string name="PendingMembersActivity_cancel_invite">Cancel invite</string>
|
||||
<string name="PendingMembersActivity_cancel_invites">Cancel invites</string>
|
||||
<plurals name="PendingMembersActivity_cancel_invites">
|
||||
<item quantity="one">Cancel invite</item>
|
||||
<item quantity="other">Cancel %1$d invites</item>
|
||||
</plurals>
|
||||
<plurals name="PendingMembersActivity_error_canceling_invite">
|
||||
<item quantity="one">Error canceling invite</item>
|
||||
<item quantity="other">Error canceling invites</item>
|
||||
</plurals>
|
||||
|
||||
<plurals name="GroupMemberList_invited">
|
||||
<item quantity="one">%1$s invited 1 person</item>
|
||||
<item quantity="other">%1$s invited %2$d people</item>
|
||||
</plurals>
|
||||
|
||||
<!-- GV2 Invite cancellation confirmation -->
|
||||
<string name="GroupManagement_cancel_own_single_invite">Do you want to cancel the invite you sent to %1$s?</string>
|
||||
<plurals name="GroupManagement_cancel_others_invites">
|
||||
<item quantity="one">Do you want to cancel the invite sent by %1$s?</item>
|
||||
<item quantity="other">Do you want to cancel %2$d invites sent by %1$s?</item>
|
||||
</plurals>
|
||||
|
||||
<!-- CropImageActivity -->
|
||||
<string name="CropImageActivity_group_avatar">Group avatar</string>
|
||||
<string name="CropImageActivity_profile_avatar">Avatar</string>
|
||||
|
@ -192,12 +192,13 @@ public final class GroupsV2Operations {
|
||||
.setPresentation(ByteString.copyFrom(presentation.serialize())));
|
||||
}
|
||||
|
||||
public GroupChange.Actions.Builder createRemoveInvitationChange(final Set<byte[]> uuidCipherTextsFromInvitesToRemove) {
|
||||
public GroupChange.Actions.Builder createRemoveInvitationChange(final Set<UuidCiphertext> uuidCipherTextsFromInvitesToRemove) {
|
||||
GroupChange.Actions.Builder builder = GroupChange.Actions
|
||||
.newBuilder();
|
||||
|
||||
for (byte[] uuidCipherText: uuidCipherTextsFromInvitesToRemove) {
|
||||
builder.addDeletePendingMembers(GroupChange.Actions.DeletePendingMemberAction.newBuilder().setDeletedUserId(ByteString.copyFrom(uuidCipherText)));
|
||||
for (UuidCiphertext uuidCipherText: uuidCipherTextsFromInvitesToRemove) {
|
||||
builder.addDeletePendingMembers(GroupChange.Actions.DeletePendingMemberAction.newBuilder()
|
||||
.setDeletedUserId(ByteString.copyFrom(uuidCipherText.serialize())));
|
||||
}
|
||||
|
||||
return builder;
|
||||
|
Loading…
x
Reference in New Issue
Block a user