Add pending member activity.

This commit is contained in:
Alan Evans
2020-04-03 16:24:25 -03:00
committed by Greyson Parrelli
parent ef0f26b64c
commit 1290d0ead9
17 changed files with 699 additions and 2 deletions

View File

@@ -150,6 +150,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog;
import org.thoughtcrime.securesms.groups.ui.pendingmemberinvites.PendingMemberInvitesActivity;
import org.thoughtcrime.securesms.insights.InsightsLauncher;
import org.thoughtcrime.securesms.invites.InviteReminderModel;
import org.thoughtcrime.securesms.invites.InviteReminderRepository;
@@ -730,6 +731,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} else {
menu.findItem(R.id.menu_distribution_conversation).setChecked(true);
}
} else if (isActiveV2Group()) {
inflater.inflate(R.menu.conversation_push_group_v2_options, menu);
} else if (isActiveGroup()) {
inflater.inflate(R.menu.conversation_push_group_options, menu);
}
@@ -835,6 +838,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
case R.id.menu_distribution_broadcast: handleDistributionBroadcastEnabled(item); return true;
case R.id.menu_distribution_conversation: handleDistributionConversationEnabled(item); return true;
case R.id.menu_edit_group: handleEditPushGroup(); return true;
case R.id.menu_pending_members: handlePendingMembers(); return true;
case R.id.menu_leave: handleLeavePushGroup(); return true;
case R.id.menu_invite: handleInviteLink(); return true;
case R.id.menu_mute_notifications: handleMuteNotifications(); return true;
@@ -1130,6 +1134,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
startActivityForResult(intent, GROUP_EDIT);
}
private void handlePendingMembers() {
startActivity(PendingMemberInvitesActivity.newIntent(ConversationActivity.this, recipient.get().requireGroupId().requireV2()));
}
private void handleDistributionBroadcastEnabled(MenuItem item) {
distributionType = ThreadDatabase.DistributionTypes.BROADCAST;
item.setChecked(true);
@@ -2108,6 +2116,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return record.isPresent() && record.get().isActive();
}
private boolean isActiveV2Group() {
if (!isGroupConversation()) return false;
Optional<GroupRecord> record = DatabaseFactory.getGroupDatabase(this).getGroup(getRecipient().getId());
return record.isPresent() && record.get().isActive() && record.get().isV2Group();
}
@SuppressWarnings("SimplifiableIfStatement")
private boolean isSelfConversation() {
if (!TextSecurePreferences.isPushRegistered(this)) return false;

View File

@@ -0,0 +1,37 @@
package org.thoughtcrime.securesms.groups;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import com.google.protobuf.ByteString;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.signal.zkgroup.util.UUIDUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import java.util.UUID;
public final class GroupProtoUtil {
private GroupProtoUtil() {
}
@WorkerThread
public static Recipient pendingMemberToRecipient(@NonNull Context context, @NonNull DecryptedPendingMember pendingMember) {
return uuidByteStringToRecipient(context, pendingMember.getUuid());
}
@WorkerThread
public static Recipient uuidByteStringToRecipient(@NonNull Context context, @NonNull ByteString uuidByteString) {
UUID uuid = UUIDUtil.deserialize(uuidByteString.toByteArray());
if (uuid.equals(GroupsV2Operations.UNKNOWN_UUID)) {
return Recipient.UNKNOWN;
}
return Recipient.externalPush(context, uuid, null);
}
}

View File

@@ -20,7 +20,7 @@ public abstract class GroupMemberEntry {
return onClick;
}
public static class FullMember extends GroupMemberEntry {
public final static class FullMember extends GroupMemberEntry {
private final Recipient member;
@@ -32,4 +32,40 @@ public abstract class GroupMemberEntry {
return member;
}
}
public final static class PendingMember extends GroupMemberEntry {
private final Recipient invitee;
private final byte[] inviteeCipherText;
public PendingMember(@NonNull Recipient invitee, @NonNull byte[] inviteeCipherText) {
this.invitee = invitee;
this.inviteeCipherText = inviteeCipherText;
}
public Recipient getInvitee() {
return invitee;
}
public byte[] getInviteeCipherText() {
return inviteeCipherText;
}
}
public final static class UnknownPendingMemberCount extends GroupMemberEntry {
private Recipient inviter;
private int inviteCount;
public UnknownPendingMemberCount(@NonNull Recipient inviter, int inviteCount) {
this.inviter = inviter;
this.inviteCount = inviteCount;
}
public Recipient getInviter() {
return inviter;
}
public int getInviteCount() {
return inviteCount;
}
}
}

View File

@@ -18,7 +18,9 @@ import java.util.Collection;
final class GroupMemberListAdapter extends RecyclerView.Adapter<GroupMemberListAdapter.ViewHolder> {
private static final int FULL_MEMBER = 0;
private static final int FULL_MEMBER = 0;
private static final int OWN_INVITE_PENDING = 1;
private static final int OTHER_INVITE_PENDING_COUNT = 2;
private final ArrayList<GroupMemberEntry> data = new ArrayList<>();
@@ -35,6 +37,14 @@ final class GroupMemberListAdapter extends RecyclerView.Adapter<GroupMemberListA
return new FullMemberViewHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.group_recipient_list_item,
parent, false));
case OWN_INVITE_PENDING:
return new OwnInvitePendingMemberViewHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.group_recipient_list_item,
parent, false));
case OTHER_INVITE_PENDING_COUNT:
return new UnknownPendingMemberCountViewHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.group_recipient_list_item,
parent, false));
default:
throw new AssertionError();
}
@@ -51,6 +61,10 @@ final class GroupMemberListAdapter extends RecyclerView.Adapter<GroupMemberListA
if (groupMemberEntry instanceof GroupMemberEntry.FullMember) {
return FULL_MEMBER;
} else if (groupMemberEntry instanceof GroupMemberEntry.PendingMember) {
return OWN_INVITE_PENDING;
} else if (groupMemberEntry instanceof GroupMemberEntry.UnknownPendingMemberCount) {
return OTHER_INVITE_PENDING_COUNT;
}
throw new AssertionError();
@@ -112,4 +126,41 @@ final class GroupMemberListAdapter extends RecyclerView.Adapter<GroupMemberListA
bindRecipient(fullMember.getMember());
}
}
final static class OwnInvitePendingMemberViewHolder extends ViewHolder {
OwnInvitePendingMemberViewHolder(@NonNull View itemView) {
super(itemView);
}
@Override
void bind(@NonNull GroupMemberEntry memberEntry) {
super.bind(memberEntry);
GroupMemberEntry.PendingMember pendingMember = (GroupMemberEntry.PendingMember) memberEntry;
bindRecipient(pendingMember.getInvitee());
}
}
final static class UnknownPendingMemberCountViewHolder extends ViewHolder {
UnknownPendingMemberCountViewHolder(@NonNull View itemView) {
super(itemView);
}
@Override
void bind(@NonNull GroupMemberEntry memberEntry) {
super.bind(memberEntry);
GroupMemberEntry.UnknownPendingMemberCount pendingMemberCount = (GroupMemberEntry.UnknownPendingMemberCount) memberEntry;
Recipient inviter = pendingMemberCount.getInviter();
String displayName = inviter.getDisplayName(itemView.getContext());
String displayText = context.getResources().getQuantityString(R.plurals.GroupMemberList_invited,
pendingMemberCount.getInviteCount(),
displayName, pendingMemberCount.getInviteCount());
bindImageAndText(inviter, displayText);
}
}
}

View File

@@ -0,0 +1,60 @@
package org.thoughtcrime.securesms.groups.ui.pendingmemberinvites;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
public class PendingMemberInvitesActivity extends PassphraseRequiredActionBarActivity {
private static final String GROUP_ID = "GROUP_ID";
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
public static Intent newIntent(@NonNull Context context, @NonNull GroupId.V2 groupId) {
Intent intent = new Intent(context, PendingMemberInvitesActivity.class);
intent.putExtra(GROUP_ID, groupId.toString());
return intent;
}
@Override
protected void onPreCreate() {
dynamicTheme.onCreate(this);
}
@Override
protected void onCreate(Bundle savedInstanceState, boolean ready) {
super.onCreate(savedInstanceState, ready);
setContentView(R.layout.group_pending_member_invites_activity);
if (savedInstanceState == null) {
getSupportFragmentManager().beginTransaction()
.replace(R.id.container, PendingMemberInvitesFragment.newInstance(GroupId.parse(getIntent().getStringExtra(GROUP_ID)).requireV2()))
.commitNow();
}
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
@Override
public void onResume() {
super.onResume();
dynamicTheme.onResume(this);
}
@Override
public boolean onSupportNavigateUp() {
onBackPressed();
return true;
}
}

View File

@@ -0,0 +1,71 @@
package org.thoughtcrime.securesms.groups.ui.pendingmemberinvites;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
import java.util.Objects;
public class PendingMemberInvitesFragment extends Fragment {
private static final String GROUP_ID = "GROUP_ID";
private PendingMemberInvitesViewModel viewModel;
private GroupMemberListView youInvited;
private GroupMemberListView othersInvited;
private View youInvitedEmptyState;
private View othersInvitedEmptyState;
public static PendingMemberInvitesFragment newInstance(@NonNull GroupId.V2 groupId) {
PendingMemberInvitesFragment fragment = new PendingMemberInvitesFragment();
Bundle args = new Bundle();
args.putString(GROUP_ID, groupId.toString());
fragment.setArguments(args);
return fragment;
}
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.group_pending_member_invites_fragment, container, false);
youInvited = view.findViewById(R.id.members_you_invited);
othersInvited = view.findViewById(R.id.members_others_invited);
youInvitedEmptyState = view.findViewById(R.id.no_pending_from_you);
othersInvitedEmptyState = view.findViewById(R.id.no_pending_from_others);
return view;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
GroupId.V2 groupId = GroupId.parse(Objects.requireNonNull(requireArguments().getString(GROUP_ID))).requireV2();
PendingMemberInvitesViewModel.Factory factory = new PendingMemberInvitesViewModel.Factory(requireContext(), groupId);
viewModel = ViewModelProviders.of(requireActivity(), factory).get(PendingMemberInvitesViewModel.class);
viewModel.getWhoYouInvited().observe(getViewLifecycleOwner(), invitees -> {
youInvited.setMembers(invitees);
youInvitedEmptyState.setVisibility(invitees.isEmpty() ? View.VISIBLE : View.GONE);
});
viewModel.getWhoOthersInvited().observe(getViewLifecycleOwner(), invitees -> {
othersInvited.setMembers(invitees);
othersInvitedEmptyState.setVisibility(invitees.isEmpty() ? View.VISIBLE : View.GONE);
});
}
}

View File

@@ -0,0 +1,85 @@
package org.thoughtcrime.securesms.groups.ui.pendingmemberinvites;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
import org.thoughtcrime.securesms.logging.Log;
import java.util.ArrayList;
import java.util.List;
public class PendingMemberInvitesViewModel extends ViewModel {
private static final String TAG = Log.tag(PendingMemberInvitesViewModel.class);
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)
{
this.context = context;
this.groupId = groupId;
this.pendingMemberRepository = pendingMemberRepository;
pendingMemberRepository.getInvitees(groupId, this::setMembers);
}
public LiveData<List<GroupMemberEntry.PendingMember>> getWhoYouInvited() {
return whoYouInvited;
}
public LiveData<List<GroupMemberEntry.UnknownPendingMemberCount>> getWhoOthersInvited() {
return whoOthersInvited;
}
private void setInvitees(List<GroupMemberEntry.PendingMember> byYou, List<GroupMemberEntry.UnknownPendingMemberCount> byOthers) {
whoYouInvited.postValue(byYou);
whoOthersInvited.postValue(byOthers);
}
private void setMembers(PendingMemberRepository.InviteeResult inviteeResult) {
List<GroupMemberEntry.PendingMember> byMe = new ArrayList<>(inviteeResult.getByMe().size());
List<GroupMemberEntry.UnknownPendingMemberCount> byOthers = new ArrayList<>(inviteeResult.getByOthers().size());
for (PendingMemberRepository.SinglePendingMemberInvitedByYou pendingMember : inviteeResult.getByMe()) {
byMe.add(new GroupMemberEntry.PendingMember(pendingMember.getInvitee(),
pendingMember.getInviteeCipherText()));
}
for (PendingMemberRepository.MultiplePendingMembersInvitedByAnother pendingMembers : inviteeResult.getByOthers()) {
byOthers.add(new GroupMemberEntry.UnknownPendingMemberCount(pendingMembers.getInviter(),
pendingMembers.getUuidCipherTexts().size()));
}
setInvitees(byMe, byOthers);
}
public static class Factory implements ViewModelProvider.Factory {
private final Context context;
private final GroupId.V2 groupId;
public Factory(@NonNull Context context, @NonNull GroupId.V2 groupId) {
this.context = context;
this.groupId = groupId;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection unchecked
return (T) new PendingMemberInvitesViewModel(context, groupId, new PendingMemberRepository(context.getApplicationContext()));
}
}
}

View File

@@ -0,0 +1,131 @@
package org.thoughtcrime.securesms.groups.ui.pendingmemberinvites;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.core.util.Consumer;
import com.annimon.stream.Stream;
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.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.GroupProtoUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
final class PendingMemberRepository {
private final Context context;
private final Executor executor;
PendingMemberRepository(@NonNull Context context) {
this.context = context.getApplicationContext();
this.executor = SignalExecutors.BOUNDED;
}
public void getInvitees(GroupId.V2 groupId, @NonNull Consumer<InviteeResult> onInviteesLoaded) {
executor.execute(() -> {
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
GroupDatabase.V2GroupProperties v2GroupProperties = groupDatabase.getGroup(groupId).get().requireV2GroupProperties();
DecryptedGroup decryptedGroup = v2GroupProperties.getDecryptedGroup();
List<DecryptedPendingMember> pendingMembersList = decryptedGroup.getPendingMembersList();
List<SinglePendingMemberInvitedByYou> byMe = new ArrayList<>(pendingMembersList.size());
List<MultiplePendingMembersInvitedByAnother> byOthers = new ArrayList<>(pendingMembersList.size());
ByteString self = ByteString.copyFrom(UUIDUtil.serialize(Recipient.self().getUuid().get()));
Stream.of(pendingMembersList)
.groupBy(DecryptedPendingMember::getAddedByUuid)
.forEach(g ->
{
ByteString inviterUuid = g.getKey();
List<DecryptedPendingMember> invitedMembers = g.getValue();
if (self.equals(inviterUuid)) {
for (DecryptedPendingMember pendingMember : invitedMembers) {
Recipient invitee = GroupProtoUtil.pendingMemberToRecipient(context, pendingMember);
byte[] uuidCipherText = pendingMember.getUuidCipherText().toByteArray();
byMe.add(new SinglePendingMemberInvitedByYou(invitee, uuidCipherText));
}
} else {
Recipient inviter = GroupProtoUtil.uuidByteStringToRecipient(context, inviterUuid);
ArrayList<byte[]> uuidCipherTexts = new ArrayList<>(invitedMembers.size());
for (DecryptedPendingMember pendingMember : invitedMembers) {
uuidCipherTexts.add(pendingMember.getUuidCipherText().toByteArray());
}
byOthers.add(new MultiplePendingMembersInvitedByAnother(inviter, uuidCipherTexts));
}
}
);
onInviteesLoaded.accept(new InviteeResult(byMe, byOthers));
});
}
public static final class InviteeResult {
private final List<SinglePendingMemberInvitedByYou> byMe;
private final List<MultiplePendingMembersInvitedByAnother> byOthers;
private InviteeResult(List<SinglePendingMemberInvitedByYou> byMe,
List<MultiplePendingMembersInvitedByAnother> byOthers)
{
this.byMe = byMe;
this.byOthers = byOthers;
}
public List<SinglePendingMemberInvitedByYou> getByMe() {
return byMe;
}
public List<MultiplePendingMembersInvitedByAnother> getByOthers() {
return byOthers;
}
}
public final static class SinglePendingMemberInvitedByYou {
private final Recipient invitee;
private final byte[] inviteeCipherText;
private SinglePendingMemberInvitedByYou(@NonNull Recipient invitee, @NonNull byte[] inviteeCipherText) {
this.invitee = invitee;
this.inviteeCipherText = inviteeCipherText;
}
public Recipient getInvitee() {
return invitee;
}
public byte[] getInviteeCipherText() {
return inviteeCipherText;
}
}
public final static class MultiplePendingMembersInvitedByAnother {
private final Recipient inviter;
private final ArrayList<byte[]> uuidCipherTexts;
private MultiplePendingMembersInvitedByAnother(@NonNull Recipient inviter, @NonNull ArrayList<byte[]> uuidCipherTexts) {
this.inviter = inviter;
this.uuidCipherTexts = uuidCipherTexts;
}
public Recipient getInviter() {
return inviter;
}
public ArrayList<byte[]> getUuidCipherTexts() {
return uuidCipherTexts;
}
}
}