diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/PendingGroupJoinRequestsReminder.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/PendingGroupJoinRequestsReminder.java new file mode 100644 index 0000000000..582072a5ab --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/PendingGroupJoinRequestsReminder.java @@ -0,0 +1,44 @@ +package org.thoughtcrime.securesms.components.reminder; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.database.GroupDatabase; +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.util.PlayStoreUtil; + +import java.util.List; + +/** + * Shown to admins when there are pending group join requests. + */ +public final class PendingGroupJoinRequestsReminder extends Reminder { + + private PendingGroupJoinRequestsReminder(@Nullable CharSequence title, + @NonNull CharSequence text) + { + super(title, text); + } + + public static Reminder create(@NonNull Context context, int count) { + String message = context.getResources().getQuantityString(R.plurals.PendingGroupJoinRequestsReminder_d_pending_member_requests, count, count); + Reminder reminder = new PendingGroupJoinRequestsReminder(null, message); + + reminder.addAction(new Action(context.getString(R.string.PendingGroupJoinRequestsReminder_view), R.id.reminder_action_review_join_requests)); + + return reminder; + } + + @Override + public boolean isDismissable() { + return true; + } + + @Override + public @NonNull Importance getImportance() { + return Importance.NORMAL; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/Reminder.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/Reminder.java index df3c946fcf..c829cf01cc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/Reminder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/Reminder.java @@ -74,7 +74,7 @@ public abstract class Reminder { NORMAL, ERROR, TERMINAL } - public final class Action { + public static final class Action { private final CharSequence title; private final int actionId; diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 68d8931edd..65f6396c51 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -115,6 +115,7 @@ import org.thoughtcrime.securesms.components.identity.UnverifiedBannerView; import org.thoughtcrime.securesms.components.location.SignalPlace; import org.thoughtcrime.securesms.components.mention.MentionAnnotation; import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder; +import org.thoughtcrime.securesms.components.reminder.PendingGroupJoinRequestsReminder; import org.thoughtcrime.securesms.components.reminder.Reminder; import org.thoughtcrime.securesms.components.reminder.ReminderView; import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder; @@ -162,6 +163,7 @@ import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason; import org.thoughtcrime.securesms.groups.ui.GroupChangeResult; import org.thoughtcrime.securesms.groups.ui.GroupErrors; import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog; +import org.thoughtcrime.securesms.groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity; import org.thoughtcrime.securesms.groups.ui.managegroup.ManageGroupActivity; import org.thoughtcrime.securesms.insights.InsightsLauncher; import org.thoughtcrime.securesms.invites.InviteReminderModel; @@ -444,6 +446,7 @@ public class ConversationActivity extends PassphraseRequiredActivity initializeGroupViewModel(); initializeMentionsViewModel(); initializeEnabledCheck(); + initializePendingRequestsBanner(); initializeSecurity(recipient.get().isRegistered(), isDefaultSms).addListener(new AssertedSuccessListener() { @Override public void onSuccess(Boolean result) { @@ -1525,6 +1528,11 @@ public class ConversationActivity extends PassphraseRequiredActivity }); } + private void initializePendingRequestsBanner() { + groupViewModel.getActionableRequestingMembers() + .observe(this, actionablePendingGroupRequests -> updateReminders()); + } + private ListenableFuture initializeDraftFromDatabase() { SettableFuture future = new SettableFuture<>(); @@ -1678,7 +1686,8 @@ public class ConversationActivity extends PassphraseRequiredActivity } protected void updateReminders() { - Optional inviteReminder = inviteReminderModel.getReminder(); + Optional inviteReminder = inviteReminderModel.getReminder(); + Integer actionableRequestingMembers = groupViewModel.getActionableRequestingMembers().getValue(); if (UnauthorizedReminder.isEligible(this)) { reminderView.get().showReminder(new UnauthorizedReminder(this)); @@ -1696,6 +1705,13 @@ public class ConversationActivity extends PassphraseRequiredActivity reminderView.get().setOnActionClickListener(this::handleReminderAction); reminderView.get().setOnDismissListener(() -> inviteReminderModel.dismissReminder()); reminderView.get().showReminder(inviteReminder.get()); + } else if (actionableRequestingMembers != null && actionableRequestingMembers > 0 && FeatureFlags.groupsV2manageGroupLinks()) { + reminderView.get().showReminder(PendingGroupJoinRequestsReminder.create(this, actionableRequestingMembers)); + reminderView.get().setOnActionClickListener(id -> { + if (id == R.id.reminder_action_review_join_requests) { + startActivity(ManagePendingAndRequestingMembersActivity.newIntent(this, getRecipient().getGroupId().get().requireV2())); + } + }); } else if (reminderView.resolved()) { reminderView.get().hide(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationGroupViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationGroupViewModel.java index 18cc7d1799..81aa8537bc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationGroupViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationGroupViewModel.java @@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.groups.GroupManager; import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.AsynchronousCallback; +import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; @@ -30,20 +31,29 @@ final class ConversationGroupViewModel extends ViewModel { private final MutableLiveData liveRecipient; private final LiveData groupActiveState; private final LiveData selfMembershipLevel; + private final LiveData actionableRequestingMembers; private ConversationGroupViewModel() { this.liveRecipient = new MutableLiveData<>(); LiveData groupRecord = LiveDataUtil.mapAsync(liveRecipient, ConversationGroupViewModel::getGroupRecordForRecipient); - this.groupActiveState = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToGroupActiveState)); - this.selfMembershipLevel = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToSelfMembershipLevel)); + this.groupActiveState = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToGroupActiveState)); + this.selfMembershipLevel = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToSelfMembershipLevel)); + this.actionableRequestingMembers = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToActionableRequestingMemberCount)); } void onRecipientChange(Recipient recipient) { liveRecipient.setValue(recipient); } + /** + * The number of pending group join requests that can be actioned by this client. + */ + LiveData getActionableRequestingMembers() { + return actionableRequestingMembers; + } + LiveData getGroupActiveState() { return groupActiveState; } @@ -62,6 +72,20 @@ final class ConversationGroupViewModel extends ViewModel { } } + private static int mapToActionableRequestingMemberCount(@Nullable GroupRecord record) { + if (record != null && + FeatureFlags.groupsV2manageGroupLinks() && + record.isV2Group() && + record.memberLevel(Recipient.self()) == GroupDatabase.MemberLevel.ADMINISTRATOR) + { + return record.requireV2GroupProperties() + .getDecryptedGroup() + .getRequestingMembersCount(); + } else { + return 0; + } + } + private static GroupActiveState mapToGroupActiveState(@Nullable GroupRecord record) { if (record == null) { return null; diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml index 41e5186a12..61c80ab5a1 100644 --- a/app/src/main/res/values/ids.xml +++ b/app/src/main/res/values/ids.xml @@ -7,4 +7,5 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a0eeea16ab..43e21f6fae 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -435,6 +435,13 @@ This version of Signal has expired. Update now to send and receive messages. Update now + + + %d pending member request. + %d pending member requests. + + View + Share with Multiple attachments are only supported for images and videos