diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java index cde0b80224..56f5ca8599 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java @@ -54,22 +54,12 @@ public final class GroupManager { } @WorkerThread - public static @NonNull GroupActionResult createGroupV1(@NonNull Context context, - @NonNull Set members, - @Nullable byte[] avatar, - @Nullable String name, - boolean mms) - { - return GroupManagerV1.createGroup(context, getMemberIds(members), avatar, name, mms); - } - - @WorkerThread - public static GroupActionResult updateGroup(@NonNull Context context, - @NonNull GroupId groupId, - @Nullable byte[] avatar, - boolean avatarChanged, - @NonNull String name, - boolean nameChanged) + public static GroupActionResult updateGroupDetails(@NonNull Context context, + @NonNull GroupId groupId, + @Nullable byte[] avatar, + boolean avatarChanged, + @NonNull String name, + boolean nameChanged) throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException { if (groupId.isV2()) { @@ -80,23 +70,15 @@ public final class GroupManager { List members = DatabaseFactory.getGroupDatabase(context) .getGroupMembers(groupId, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF); - return updateGroup(context, groupId.requireV1(), new HashSet<>(members), avatar, name); + Set recipientIds = getMemberIds(new HashSet<>(members)); + + return GroupManagerV1.updateGroup(context, groupId.requireV1(), recipientIds, avatar, name, 0); } } - public static @Nullable GroupActionResult updateGroup(@NonNull Context context, - @NonNull GroupId.V1 groupId, - @NonNull Set members, - @Nullable byte[] avatar, - @Nullable String name) - { - Set addresses = getMemberIds(members); - - return GroupManagerV1.updateGroup(context, groupId, addresses, avatar, name); - } - private static Set getMemberIds(Collection recipients) { - final Set results = new HashSet<>(); + Set results = new HashSet<>(recipients.size()); + for (Recipient recipient : recipients) { results.add(recipient.getId()); } @@ -250,41 +232,59 @@ public final class GroupManager { } } - public static void addMembers(@NonNull Context context, - @NonNull GroupId.Push groupId, - @NonNull Collection newMembers) + @WorkerThread + public static @NonNull GroupActionResult addMembers(@NonNull Context context, + @NonNull GroupId.Push groupId, + @NonNull Collection newMembers) throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException, MembershipNotSuitableForV2Exception { if (groupId.isV2()) { try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) { - editor.addMembers(newMembers); + return editor.addMembers(newMembers); } } else { - GroupDatabase.GroupRecord groupRecord = DatabaseFactory.getGroupDatabase(context).requireGroup(groupId); - List members = groupRecord.getMembers(); - byte[] avatar = groupRecord.hasAvatar() ? Util.readFully(AvatarHelper.getAvatar(context, groupRecord.getRecipientId())) : null; - Set addresses = new HashSet<>(members); + GroupDatabase.GroupRecord groupRecord = DatabaseFactory.getGroupDatabase(context).requireGroup(groupId); + List members = groupRecord.getMembers(); + byte[] avatar = groupRecord.hasAvatar() ? Util.readFully(AvatarHelper.getAvatar(context, groupRecord.getRecipientId())) : null; + Set recipientIds = new HashSet<>(members); + int originalSize = recipientIds.size(); - addresses.addAll(newMembers); - GroupManagerV1.updateGroup(context, groupId, addresses, avatar, groupRecord.getTitle()); + recipientIds.addAll(newMembers); + return GroupManagerV1.updateGroup(context, groupId, recipientIds, avatar, groupRecord.getTitle(), recipientIds.size() - originalSize); } } public static class GroupActionResult { - private final Recipient groupRecipient; - private final long threadId; + private final Recipient groupRecipient; + private final long threadId; + private final int addedMemberCount; + private final List invitedMembers; - public GroupActionResult(Recipient groupRecipient, long threadId) { - this.groupRecipient = groupRecipient; - this.threadId = threadId; + public GroupActionResult(@NonNull Recipient groupRecipient, + long threadId, + int addedMemberCount, + @NonNull List invitedMembers) + { + this.groupRecipient = groupRecipient; + this.threadId = threadId; + this.addedMemberCount = addedMemberCount; + this.invitedMembers = invitedMembers; } - public Recipient getGroupRecipient() { + public @NonNull Recipient getGroupRecipient() { return groupRecipient; } public long getThreadId() { return threadId; } + + public int getAddedMemberCount() { + return addedMemberCount; + } + + public @NonNull List getInvitedMembers() { + return invitedMembers; + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV1.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV1.java index bb215bb488..6e7f3a1d48 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV1.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV1.java @@ -71,11 +71,11 @@ final class GroupManagerV1 { } groupDatabase.onAvatarUpdated(groupIdV1, avatarBytes != null); DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient.getId(), true); - return sendGroupUpdate(context, groupIdV1, memberIds, name, avatarBytes); + return sendGroupUpdate(context, groupIdV1, memberIds, name, avatarBytes, memberIds.size() - 1); } else { groupDatabase.create(groupId.requireMms(), memberIds); long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION); - return new GroupActionResult(groupRecipient, threadId); + return new GroupActionResult(groupRecipient, threadId, memberIds.size() - 1, Collections.emptyList()); } } @@ -83,7 +83,8 @@ final class GroupManagerV1 { @NonNull GroupId groupId, @NonNull Set memberAddresses, @Nullable byte[] avatarBytes, - @Nullable String name) + @Nullable String name, + int newMemberCount) { final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); final RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId); @@ -102,19 +103,20 @@ final class GroupManagerV1 { } catch (IOException e) { Log.w(TAG, "Failed to save avatar!", e); } - return sendGroupUpdate(context, groupIdV1, memberAddresses, name, avatarBytes); + return sendGroupUpdate(context, groupIdV1, memberAddresses, name, avatarBytes, newMemberCount); } else { Recipient groupRecipient = Recipient.resolved(groupRecipientId); long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); - return new GroupActionResult(groupRecipient, threadId); + return new GroupActionResult(groupRecipient, threadId, newMemberCount, Collections.emptyList()); } } - private static GroupActionResult sendGroupUpdate(@NonNull Context context, - @NonNull GroupId.V1 groupId, - @NonNull Set members, - @Nullable String groupName, - @Nullable byte[] avatar) + private static GroupActionResult sendGroupUpdate(@NonNull Context context, + @NonNull GroupId.V1 groupId, + @NonNull Set members, + @Nullable String groupName, + @Nullable byte[] avatar, + int newMemberCount) { Attachment avatarAttachment = null; RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId); @@ -144,7 +146,7 @@ final class GroupManagerV1 { OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0, false, null, Collections.emptyList(), Collections.emptyList()); long threadId = MessageSender.send(context, outgoingMessage, -1, false, null); - return new GroupActionResult(groupRecipient, threadId); + return new GroupActionResult(groupRecipient, threadId, newMemberCount, Collections.emptyList()); } @WorkerThread diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java index 2d89aae4de..9c8ba2ed1c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java @@ -156,7 +156,12 @@ final class GroupManagerV2 { groupDatabase.onAvatarUpdated(groupId, avatar != null); DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient.getId(), true); - return sendGroupUpdate(masterKey, decryptedGroup, null, null); + RecipientAndThread recipientAndThread = sendGroupUpdate(masterKey, decryptedGroup, null, null); + + return new GroupManager.GroupActionResult(recipientAndThread.groupRecipient, + recipientAndThread.threadId, + decryptedGroup.getMembersCount() - 1, + getPendingMemberRecipientIds(decryptedGroup.getPendingMembersList())); } catch (VerificationFailedException | InvalidGroupStateException e) { throw new GroupChangeFailedException(e); } @@ -378,7 +383,7 @@ final class GroupManagerV2 { Recipient groupRecipient = Recipient.externalGroup(context, groupId); long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); - return new GroupManager.GroupActionResult(groupRecipient, threadId); + return new GroupManager.GroupActionResult(groupRecipient, threadId, 0, Collections.emptyList()); } } } @@ -429,7 +434,11 @@ final class GroupManagerV2 { GroupChange signedGroupChange = commitToServer(changeActions); groupDatabase.update(groupId, decryptedGroupState); - return sendGroupUpdate(groupMasterKey, decryptedGroupState, decryptedChange, signedGroupChange); + RecipientAndThread recipientAndThread = sendGroupUpdate(groupMasterKey, decryptedGroupState, decryptedChange, signedGroupChange); + int newMembersCount = decryptedChange.getNewMembersCount(); + List newPendingMembers = getPendingMemberRecipientIds(decryptedChange.getNewPendingMembersList()); + + return new GroupManager.GroupActionResult(recipientAndThread.groupRecipient, recipientAndThread.threadId, newMembersCount, newPendingMembers); } private GroupChange commitToServer(GroupChange.Actions change) @@ -494,10 +503,10 @@ final class GroupManagerV2 { } } - private @NonNull GroupManager.GroupActionResult sendGroupUpdate(@NonNull GroupMasterKey masterKey, - @NonNull DecryptedGroup decryptedGroup, - @Nullable DecryptedGroupChange plainGroupChange, - @Nullable GroupChange signedGroupChange) + private @NonNull RecipientAndThread sendGroupUpdate(@NonNull GroupMasterKey masterKey, + @NonNull DecryptedGroup decryptedGroup, + @Nullable DecryptedGroupChange plainGroupChange, + @Nullable GroupChange signedGroupChange) { GroupId.V2 groupId = GroupId.v2(masterKey); Recipient groupRecipient = Recipient.externalGroup(context, groupId); @@ -514,13 +523,19 @@ final class GroupManagerV2 { if (plainGroupChange != null && DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(plainGroupChange)) { ApplicationDependencies.getJobManager().add(PushGroupSilentUpdateSendJob.create(context, groupId, decryptedGroup, outgoingMessage)); - return new GroupManager.GroupActionResult(groupRecipient, -1); + return new RecipientAndThread(groupRecipient, -1); } else { long threadId = MessageSender.send(context, outgoingMessage, -1, false, null); - return new GroupManager.GroupActionResult(groupRecipient, threadId); + return new RecipientAndThread(groupRecipient, threadId); } } + private static @NonNull List getPendingMemberRecipientIds(@NonNull List newPendingMembersList) { + return Stream.of(DecryptedGroupUtil.pendingToUuidList(newPendingMembersList)) + .map(uuid-> RecipientId.from(uuid,null)) + .toList(); + } + private static @NonNull AccessControl.AccessRequired rightsToAccessControl(@NonNull GroupAccessControl rights) { switch (rights){ case ALL_MEMBERS: @@ -531,4 +546,14 @@ final class GroupManagerV2 { throw new AssertionError(); } } + + static class RecipientAndThread { + private final Recipient groupRecipient; + private final long threadId; + + RecipientAndThread(@NonNull Recipient groupRecipient, long threadId) { + this.groupRecipient = groupRecipient; + this.threadId = threadId; + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/AddMembersResultCallback.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/AddMembersResultCallback.java index d5bc85133d..75531f9e81 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/AddMembersResultCallback.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/AddMembersResultCallback.java @@ -1,5 +1,11 @@ package org.thoughtcrime.securesms.groups.ui; +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.recipients.RecipientId; + +import java.util.List; + public interface AddMembersResultCallback { - void onMembersAdded(int numberOfMembersAdded); + void onMembersAdded(int numberOfMembersAdded, @NonNull List invitedMembers); } 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 0c72a591a4..57d12069fe 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 @@ -111,10 +111,17 @@ public abstract class GroupMemberEntry { private final UuidCiphertext inviteeCipherText; private final boolean cancellable; - public PendingMember(@NonNull Recipient invitee, @NonNull UuidCiphertext inviteeCipherText, boolean cancellable) { + public PendingMember(@NonNull Recipient invitee, @Nullable UuidCiphertext inviteeCipherText, boolean cancellable) { this.invitee = invitee; this.inviteeCipherText = inviteeCipherText; this.cancellable = cancellable; + if (cancellable && inviteeCipherText == null) { + throw new IllegalArgumentException("inviteeCipherText must be supplied to enable cancellation"); + } + } + + public PendingMember(@NonNull Recipient invitee) { + this(invitee, null, false); } public Recipient getInvitee() { @@ -122,6 +129,9 @@ public abstract class GroupMemberEntry { } public UuidCiphertext getInviteeCipherText() { + if (!cancellable) { + throw new UnsupportedOperationException(); + } return inviteeCipherText; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsActivity.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsActivity.java index 2e448b115d..0d602d930b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsActivity.java @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.groups.ui.creategroup.details; +import android.app.Dialog; import android.content.Context; import android.content.Intent; import android.os.Bundle; @@ -14,10 +15,14 @@ import org.thoughtcrime.securesms.PassphraseRequiredActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.conversation.ConversationActivity; import org.thoughtcrime.securesms.database.ThreadDatabase; +import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupInviteSentDialog; +import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; import org.thoughtcrime.securesms.util.DynamicTheme; +import java.util.List; + public class AddGroupDetailsActivity extends PassphraseRequiredActivity implements AddGroupDetailsFragment.Callback { private static final String EXTRA_RECIPIENTS = "recipient_ids"; @@ -58,7 +63,19 @@ public class AddGroupDetailsActivity extends PassphraseRequiredActivity implemen } @Override - public void onGroupCreated(@NonNull RecipientId recipientId, long threadId) { + public void onGroupCreated(@NonNull RecipientId recipientId, + long threadId, + @NonNull List invitedMembers) + { + Dialog dialog = GroupInviteSentDialog.showInvitesSent(this, invitedMembers); + if (dialog != null) { + dialog.setOnDismissListener((d) -> goToConversation(recipientId, threadId)); + } else { + goToConversation(recipientId, threadId); + } + } + + void goToConversation(@NonNull RecipientId recipientId, long threadId) { Intent intent = ConversationActivity.buildIntent(this, recipientId, threadId, diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsFragment.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsFragment.java index 64a4cfd00e..ca23a60767 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsFragment.java @@ -20,7 +20,6 @@ import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.Toolbar; -import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProviders; import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; @@ -45,6 +44,7 @@ import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.text.AfterTextChanged; +import java.util.List; import java.util.Objects; public class AddGroupDetailsFragment extends LoggingFragment { @@ -202,7 +202,7 @@ public class AddGroupDetailsFragment extends LoggingFragment { } private void handleGroupCreateResultSuccess(@NonNull GroupCreateResult.Success success) { - callback.onGroupCreated(success.getGroupRecipient().getId(), success.getThreadId()); + callback.onGroupCreated(success.getGroupRecipient().getId(), success.getThreadId(), success.getInvitedMembers()); } private void handleGroupCreateResultError(@NonNull GroupCreateResult.Error error) { @@ -252,7 +252,7 @@ public class AddGroupDetailsFragment extends LoggingFragment { } public interface Callback { - void onGroupCreated(@NonNull RecipientId recipientId, long threadId); + void onGroupCreated(@NonNull RecipientId recipientId, long threadId, @NonNull List invitedMembers); void onNavigationButtonPressed(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/GroupCreateResult.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/GroupCreateResult.java index d95b147523..3e9df30643 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/GroupCreateResult.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/GroupCreateResult.java @@ -1,15 +1,19 @@ package org.thoughtcrime.securesms.groups.ui.creategroup.details; import androidx.annotation.NonNull; +import androidx.annotation.WorkerThread; import androidx.core.util.Consumer; import org.thoughtcrime.securesms.groups.GroupManager; import org.thoughtcrime.securesms.recipients.Recipient; +import java.util.List; + abstract class GroupCreateResult { + @WorkerThread static GroupCreateResult success(@NonNull GroupManager.GroupActionResult result) { - return new GroupCreateResult.Success(result.getThreadId(), result.getGroupRecipient()); + return new GroupCreateResult.Success(result.getThreadId(), result.getGroupRecipient(), result.getAddedMemberCount(), Recipient.resolvedList(result.getInvitedMembers())); } static GroupCreateResult error(@NonNull GroupCreateResult.Error.Type errorType) { @@ -20,12 +24,20 @@ abstract class GroupCreateResult { } static final class Success extends GroupCreateResult { - private final long threadId; - private final Recipient groupRecipient; + private final long threadId; + private final Recipient groupRecipient; + private final int addedMemberCount; + private final List invitedMembers; - private Success(long threadId, @NonNull Recipient groupRecipient) { - this.threadId = threadId; - this.groupRecipient = groupRecipient; + private Success(long threadId, + @NonNull Recipient groupRecipient, + int addedMemberCount, + @NonNull List invitedMembers) + { + this.threadId = threadId; + this.groupRecipient = groupRecipient; + this.addedMemberCount = addedMemberCount; + this.invitedMembers = invitedMembers; } long getThreadId() { @@ -36,6 +48,14 @@ abstract class GroupCreateResult { return groupRecipient; } + int getAddedMemberCount() { + return addedMemberCount; + } + + List getInvitedMembers() { + return invitedMembers; + } + @Override void consume(@NonNull Consumer successConsumer, @NonNull Consumer errorConsumer) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupFragment.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupFragment.java index 3a7a5d0467..57a0feb7d0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupFragment.java @@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto80dp; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.ui.GroupMemberListView; import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog; +import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupInviteSentDialog; import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupRightsDialog; import org.thoughtcrime.securesms.groups.ui.pendingmemberinvites.PendingMemberInvitesActivity; import org.thoughtcrime.securesms.logging.Log; @@ -317,6 +318,7 @@ public class ManageGroupFragment extends LoggingFragment { } viewModel.getSnackbarEvents().observe(getViewLifecycleOwner(), this::handleSnackbarEvent); + viewModel.getInvitedDialogEvents().observe(getViewLifecycleOwner(), this::handleInvitedDialogEvent); viewModel.getCanLeaveGroup().observe(getViewLifecycleOwner(), canLeave -> leaveGroup.setVisibility(canLeave ? View.VISIBLE : View.GONE)); viewModel.getCanBlockGroup().observe(getViewLifecycleOwner(), canBlock -> { @@ -364,6 +366,10 @@ public class ManageGroupFragment extends LoggingFragment { Snackbar.make(requireView(), buildSnackbarString(snackbarEvent), Snackbar.LENGTH_SHORT).setTextColor(Color.WHITE).show(); } + private void handleInvitedDialogEvent(@NonNull ManageGroupViewModel.InvitedDialogEvent invitedDialogEvent) { + GroupInviteSentDialog.showInvitesSent(requireContext(), invitedDialogEvent.getNewInvitedMembers()); + } + private @NonNull String buildSnackbarString(@NonNull ManageGroupViewModel.SnackbarEvent snackbarEvent) { return getResources().getQuantityString(R.plurals.ManageGroupActivity_added, snackbarEvent.getNumberOfMembersAdded(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupRepository.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupRepository.java index 0d57e778a3..caa52d1959 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupRepository.java @@ -146,8 +146,8 @@ final class ManageGroupRepository { void addMembers(@NonNull List selected, @NonNull AddMembersResultCallback addMembersResultCallback, @NonNull GroupChangeErrorCallback error) { SignalExecutors.UNBOUNDED.execute(() -> { try { - GroupManager.addMembers(context, groupId.requirePush(), selected); - addMembersResultCallback.onMembersAdded(selected.size()); + GroupManager.GroupActionResult groupActionResult = GroupManager.addMembers(context, groupId.requirePush(), selected); + addMembersResultCallback.onMembersAdded(groupActionResult.getAddedMemberCount(), groupActionResult.getInvitedMembers()); } catch (GroupInsufficientRightsException | GroupNotAMemberException e) { Log.w(TAG, e); error.onError(GroupChangeFailureReason.NO_RIGHTS); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupViewModel.java index aeb236274d..58cd6096f6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupViewModel.java @@ -25,8 +25,6 @@ import org.thoughtcrime.securesms.database.MediaDatabase; import org.thoughtcrime.securesms.database.loaders.MediaLoader; import org.thoughtcrime.securesms.database.loaders.ThreadMediaLoader; import org.thoughtcrime.securesms.groups.GroupAccessControl; -import org.thoughtcrime.securesms.groups.GroupChangeBusyException; -import org.thoughtcrime.securesms.groups.GroupChangeFailedException; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.LiveGroup; import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason; @@ -43,7 +41,6 @@ import org.thoughtcrime.securesms.util.SingleLiveEvent; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; -import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -54,6 +51,7 @@ public class ManageGroupViewModel extends ViewModel { private final Context context; private final ManageGroupRepository manageGroupRepository; private final SingleLiveEvent snackbarEvents = new SingleLiveEvent<>(); + private final SingleLiveEvent invitedDialogEvents = new SingleLiveEvent<>(); private final LiveData title; private final LiveData isAdmin; private final LiveData canEditGroupAttributes; @@ -91,7 +89,7 @@ public class ManageGroupViewModel extends ViewModel { (state, hasEnoughMembers) -> state != CollapseState.OPEN && hasEnoughMembers); this.members = LiveDataUtil.combineLatest(liveGroup.getFullMembers(), memberListCollapseState, - this::filterMemberList); + ManageGroupViewModel::filterMemberList); this.pendingMemberCount = liveGroup.getPendingMemberCount(); this.memberCountSummary = liveGroup.getMembershipCountDescription(context.getResources()); this.fullMemberCountSummary = liveGroup.getFullMembershipCountDescription(context.getResources()); @@ -180,6 +178,10 @@ public class ManageGroupViewModel extends ViewModel { return snackbarEvents; } + SingleLiveEvent getInvitedDialogEvents() { + return invitedDialogEvents; + } + LiveData getCanCollapseMemberList() { return canCollapseMemberList; } @@ -220,7 +222,7 @@ public class ManageGroupViewModel extends ViewModel { } void onAddMembers(List selected) { - manageGroupRepository.addMembers(selected, this::showSuccessSnackbar, this::showErrorToast); + manageGroupRepository.addMembers(selected, this::showAddSuccess, this::showErrorToast); } void setMuteUntil(long muteUntil) { @@ -235,8 +237,8 @@ public class ManageGroupViewModel extends ViewModel { memberListCollapseState.setValue(CollapseState.OPEN); } - private @NonNull List filterMemberList(@NonNull List members, - @NonNull CollapseState collapseState) + private static @NonNull List filterMemberList(@NonNull List members, + @NonNull CollapseState collapseState) { if (collapseState == CollapseState.COLLAPSED && members.size() > MAX_COLLAPSED_MEMBERS) { return members.subList(0, MAX_COLLAPSED_MEMBERS); @@ -246,8 +248,14 @@ public class ManageGroupViewModel extends ViewModel { } @WorkerThread - private void showSuccessSnackbar(int numberOfMembersAdded) { - snackbarEvents.postValue(new SnackbarEvent(numberOfMembersAdded)); + private void showAddSuccess(int numberOfMembersAdded, @NonNull List newInvitedMembers) { + if (!newInvitedMembers.isEmpty()) { + invitedDialogEvents.postValue(new InvitedDialogEvent(Recipient.resolvedList(newInvitedMembers))); + } + + if (numberOfMembersAdded > 0) { + snackbarEvents.postValue(new SnackbarEvent(numberOfMembersAdded)); + } } @WorkerThread @@ -328,6 +336,19 @@ public class ManageGroupViewModel extends ViewModel { } } + static final class InvitedDialogEvent { + + private final List newInvitedMembers; + + private InvitedDialogEvent(@NonNull List newInvitedMembers) { + this.newInvitedMembers = newInvitedMembers; + } + + public @NonNull List getNewInvitedMembers() { + return newInvitedMembers; + } + } + private enum CollapseState { OPEN, COLLAPSED diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/dialogs/GroupInviteSentDialog.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/dialogs/GroupInviteSentDialog.java new file mode 100644 index 0000000000..b118f079ad --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/dialogs/GroupInviteSentDialog.java @@ -0,0 +1,57 @@ +package org.thoughtcrime.securesms.groups.ui.managegroup.dialogs; + +import android.app.Dialog; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry; +import org.thoughtcrime.securesms.groups.ui.GroupMemberListView; +import org.thoughtcrime.securesms.recipients.Recipient; + +import java.util.ArrayList; +import java.util.List; + +public final class GroupInviteSentDialog { + + private GroupInviteSentDialog() { + } + + public static @Nullable Dialog showInvitesSent(@NonNull Context context, @NonNull List recipients) { + int size = recipients.size(); + if (size == 0) { + return null; + } + + AlertDialog.Builder builder = new AlertDialog.Builder(context) + .setTitle(context.getResources().getQuantityString(R.plurals.GroupManagement_invitation_sent, size, size)) + // TODO: GV2 Need a URL for learn more + // .setNegativeButton(R.string.GroupManagement_learn_more, (dialog, which) -> { + // }) + .setPositiveButton(android.R.string.ok, null); + if (size == 1) { + builder.setMessage(context.getString(R.string.GroupManagement_invite_single_user, recipients.get(0).getDisplayName(context))); + } else { + builder.setMessage(R.string.GroupManagement_invite_multiple_users) + .setView(R.layout.dialog_multiple_group_invites_sent); + } + + Dialog dialog = builder.show(); + if (size > 1) { + GroupMemberListView invitees = dialog.findViewById(R.id.list_invitees); + + List pendingMembers = new ArrayList<>(recipients.size()); + for (Recipient r : recipients) { + pendingMembers.add(new GroupMemberEntry.PendingMember(r)); + } + + //noinspection ConstantConditions + invitees.setMembers(pendingMembers); + } + + return dialog; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditPushGroupProfileRepository.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditPushGroupProfileRepository.java index d02b1f4a97..30f1817b6f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditPushGroupProfileRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditPushGroupProfileRepository.java @@ -80,7 +80,7 @@ class EditPushGroupProfileRepository implements EditProfileRepository { { SimpleTask.run(() -> { try { - GroupManager.updateGroup(context, groupId, avatar, avatarChanged, displayName, displayNameChanged); + GroupManager.updateGroupDetails(context, groupId, avatar, avatarChanged, displayName, displayNameChanged); return UploadResult.SUCCESS; } catch (GroupChangeFailedException | GroupInsufficientRightsException | IOException | GroupNotAMemberException | GroupChangeBusyException e) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java index c70df20abb..4f900789f7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java @@ -47,6 +47,7 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.UuidUtil; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -125,6 +126,17 @@ public class Recipient { return live(id).resolve(); } + @WorkerThread + public static @NonNull List resolvedList(@NonNull Collection ids) { + List recipients = new ArrayList<>(ids.size()); + + for (RecipientId recipientId : ids) { + recipients.add(resolved(recipientId)); + } + + return recipients; + } + /** * Returns a fully-populated {@link Recipient} and associates it with the provided username. */ diff --git a/app/src/main/res/layout/dialog_multiple_group_invites_sent.xml b/app/src/main/res/layout/dialog_multiple_group_invites_sent.xml new file mode 100644 index 0000000000..90d6b5374e --- /dev/null +++ b/app/src/main/res/layout/dialog_multiple_group_invites_sent.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d11925632e..fa832ceed3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -472,6 +472,15 @@ @string/GroupManagement_access_level_only_admins + + + Invitation sent + %d invitations sent + + “%1$s” can’t be automatically added to this group by you.\n\nThey’ve been invited to join, and won’t see any group messages until they accept. + Learn more + These users can’t be automatically added to this group by you.\n\nThey’ve been invited to join the group, and won’t see any group messages until they accept. + Pending group invites People you invited