From 1290d0ead90667c064bab7138a40c1b906b780b4 Mon Sep 17 00:00:00 2001 From: Alan Evans Date: Fri, 3 Apr 2020 16:24:25 -0300 Subject: [PATCH] Add pending member activity. --- app/src/main/AndroidManifest.xml | 4 + .../conversation/ConversationActivity.java | 15 ++ .../securesms/groups/GroupProtoUtil.java | 37 +++++ .../securesms/groups/ui/GroupMemberEntry.java | 38 ++++- .../groups/ui/GroupMemberListAdapter.java | 53 ++++++- .../PendingMemberInvitesActivity.java | 60 ++++++++ .../PendingMemberInvitesFragment.java | 71 ++++++++++ .../PendingMemberInvitesViewModel.java | 85 ++++++++++++ .../PendingMemberRepository.java | 131 ++++++++++++++++++ .../group_pending_member_invites_activity.xml | 32 +++++ .../group_pending_member_invites_fragment.xml | 121 ++++++++++++++++ .../layout/selected_recipient_list_item.xml | 1 + .../conversation_push_group_v2_options.xml | 16 +++ app/src/main/res/values/attrs.xml | 3 + app/src/main/res/values/strings.xml | 15 ++ app/src/main/res/values/text_styles.xml | 9 ++ app/src/main/res/values/themes.xml | 10 ++ 17 files changed, 699 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/groups/GroupProtoUtil.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/groups/ui/pendingmemberinvites/PendingMemberInvitesActivity.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/groups/ui/pendingmemberinvites/PendingMemberInvitesFragment.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/groups/ui/pendingmemberinvites/PendingMemberInvitesViewModel.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/groups/ui/pendingmemberinvites/PendingMemberRepository.java create mode 100644 app/src/main/res/layout/group_pending_member_invites_activity.xml create mode 100644 app/src/main/res/layout/group_pending_member_invites_fragment.xml create mode 100644 app/src/main/res/menu/conversation_push_group_v2_options.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f6dbdfedf..dccf47a9c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -251,6 +251,10 @@ android:windowSoftInputMode="stateVisible" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> + + 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; diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupProtoUtil.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupProtoUtil.java new file mode 100644 index 000000000..e5af13b66 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupProtoUtil.java @@ -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); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberEntry.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberEntry.java index e82c23d72..fd0ec948d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberEntry.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberEntry.java @@ -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; + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberListAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberListAdapter.java index f3631aa46..52ec5c370 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberListAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberListAdapter.java @@ -18,7 +18,9 @@ import java.util.Collection; final class GroupMemberListAdapter extends RecyclerView.Adapter { - 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 data = new ArrayList<>(); @@ -35,6 +37,14 @@ final class GroupMemberListAdapter extends RecyclerView.Adapter { + 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); + }); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/pendingmemberinvites/PendingMemberInvitesViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/pendingmemberinvites/PendingMemberInvitesViewModel.java new file mode 100644 index 000000000..1790544e5 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/pendingmemberinvites/PendingMemberInvitesViewModel.java @@ -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> whoYouInvited = new MutableLiveData<>(); + private final MutableLiveData> 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> getWhoYouInvited() { + return whoYouInvited; + } + + public LiveData> getWhoOthersInvited() { + return whoOthersInvited; + } + + private void setInvitees(List byYou, List byOthers) { + whoYouInvited.postValue(byYou); + whoOthersInvited.postValue(byOthers); + } + + private void setMembers(PendingMemberRepository.InviteeResult inviteeResult) { + List byMe = new ArrayList<>(inviteeResult.getByMe().size()); + List 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 create(@NonNull Class modelClass) { + //noinspection unchecked + return (T) new PendingMemberInvitesViewModel(context, groupId, new PendingMemberRepository(context.getApplicationContext())); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/pendingmemberinvites/PendingMemberRepository.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/pendingmemberinvites/PendingMemberRepository.java new file mode 100644 index 000000000..741ab8924 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/pendingmemberinvites/PendingMemberRepository.java @@ -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 onInviteesLoaded) { + executor.execute(() -> { + GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); + GroupDatabase.V2GroupProperties v2GroupProperties = groupDatabase.getGroup(groupId).get().requireV2GroupProperties(); + DecryptedGroup decryptedGroup = v2GroupProperties.getDecryptedGroup(); + List pendingMembersList = decryptedGroup.getPendingMembersList(); + List byMe = new ArrayList<>(pendingMembersList.size()); + List 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 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 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 byMe; + private final List byOthers; + + private InviteeResult(List byMe, + List byOthers) + { + this.byMe = byMe; + this.byOthers = byOthers; + } + + public List getByMe() { + return byMe; + } + + public List 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 uuidCipherTexts; + + private MultiplePendingMembersInvitedByAnother(@NonNull Recipient inviter, @NonNull ArrayList uuidCipherTexts) { + this.inviter = inviter; + this.uuidCipherTexts = uuidCipherTexts; + } + + public Recipient getInviter() { + return inviter; + } + + public ArrayList getUuidCipherTexts() { + return uuidCipherTexts; + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/group_pending_member_invites_activity.xml b/app/src/main/res/layout/group_pending_member_invites_activity.xml new file mode 100644 index 000000000..2183c3031 --- /dev/null +++ b/app/src/main/res/layout/group_pending_member_invites_activity.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/group_pending_member_invites_fragment.xml b/app/src/main/res/layout/group_pending_member_invites_fragment.xml new file mode 100644 index 000000000..0ab7b72d9 --- /dev/null +++ b/app/src/main/res/layout/group_pending_member_invites_fragment.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/selected_recipient_list_item.xml b/app/src/main/res/layout/selected_recipient_list_item.xml index 423d93def..38e2d048f 100644 --- a/app/src/main/res/layout/selected_recipient_list_item.xml +++ b/app/src/main/res/layout/selected_recipient_list_item.xml @@ -29,6 +29,7 @@ android:layout_alignParentEnd="true" android:layout_centerVertical="true" android:background="#00ffffff" + android:contentDescription="@string/GroupCreateActivity_remove_member_description" android:src="@drawable/ic_menu_remove_holo_light" /> \ No newline at end of file diff --git a/app/src/main/res/menu/conversation_push_group_v2_options.xml b/app/src/main/res/menu/conversation_push_group_v2_options.xml new file mode 100644 index 000000000..0197b90f5 --- /dev/null +++ b/app/src/main/res/menu/conversation_push_group_v2_options.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index c8f6e923d..1dd04ce93 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -271,6 +271,9 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8799c79ba..7ffed6045 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -432,6 +432,7 @@ Couldn\'t add %1$s because they\'re not a Signal user. Loading group details… You\'re already in the group. + Remove member Share your profile name and photo with this group? @@ -441,6 +442,19 @@ You + + Pending group invites + People you invited + You have no pending invites. + Invites by other group members + No pending invites by other group members. + 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. + + + %1$s invited 1 person + %1$s invited %2$d people + + Group avatar Avatar @@ -1674,6 +1688,7 @@ All media Conversation settings Add to home screen + Pending members Expand popup diff --git a/app/src/main/res/values/text_styles.xml b/app/src/main/res/values/text_styles.xml index ab7719cd2..2fb5f8ce2 100644 --- a/app/src/main/res/values/text_styles.xml +++ b/app/src/main/res/values/text_styles.xml @@ -97,6 +97,15 @@ bold + + + + + +