Add the ability to add suggested members after a GV1 migration.

This commit is contained in:
Greyson Parrelli 2020-11-09 08:30:58 -05:00 committed by Cody Henthorne
parent c4c32d80b2
commit d307db8a95
8 changed files with 244 additions and 16 deletions

View file

@ -0,0 +1,26 @@
package org.thoughtcrime.securesms.components.reminder;
import android.content.Context;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.recipients.RecipientId;
import java.util.List;
/**
* Shows a reminder to add anyone that might have been missed in GV1->GV2 migration.
*/
public class GroupsV1MigrationSuggestionsReminder extends Reminder {
public GroupsV1MigrationSuggestionsReminder(@NonNull Context context, @NonNull List<RecipientId> suggestions) {
super(null, context.getResources().getQuantityString(R.plurals.GroupsV1MigrationSuggestionsReminder_members_couldnt_be_added_to_the_new_group, suggestions.size(), suggestions.size()));
addAction(new Action(context.getResources().getQuantityString(R.plurals.GroupsV1MigrationSuggestionsReminder_add_members, suggestions.size()), R.id.reminder_action_gv1_suggestion_add_members));
addAction(new Action(context.getResources().getString(R.string.GroupsV1MigrationSuggestionsReminder_not_now), R.id.reminder_action_gv1_suggestion_not_now));
}
@Override
public boolean isDismissable() {
return false;
}
}

View file

@ -69,7 +69,6 @@ import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.SearchView; import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.core.content.pm.ShortcutInfoCompat; import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.content.pm.ShortcutManagerCompat; import androidx.core.content.pm.ShortcutManagerCompat;
import androidx.core.graphics.drawable.DrawableCompat; import androidx.core.graphics.drawable.DrawableCompat;
@ -117,6 +116,7 @@ import org.thoughtcrime.securesms.components.identity.UnverifiedBannerView;
import org.thoughtcrime.securesms.components.location.SignalPlace; import org.thoughtcrime.securesms.components.location.SignalPlace;
import org.thoughtcrime.securesms.components.mention.MentionAnnotation; import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder; import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder;
import org.thoughtcrime.securesms.components.reminder.GroupsV1MigrationSuggestionsReminder;
import org.thoughtcrime.securesms.components.reminder.PendingGroupJoinRequestsReminder; import org.thoughtcrime.securesms.components.reminder.PendingGroupJoinRequestsReminder;
import org.thoughtcrime.securesms.components.reminder.Reminder; import org.thoughtcrime.securesms.components.reminder.Reminder;
import org.thoughtcrime.securesms.components.reminder.ReminderView; import org.thoughtcrime.securesms.components.reminder.ReminderView;
@ -167,6 +167,7 @@ import org.thoughtcrime.securesms.groups.ui.GroupErrors;
import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog; import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog;
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity; import org.thoughtcrime.securesms.groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity;
import org.thoughtcrime.securesms.groups.ui.managegroup.ManageGroupActivity; import org.thoughtcrime.securesms.groups.ui.managegroup.ManageGroupActivity;
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationSuggestionsDialog;
import org.thoughtcrime.securesms.insights.InsightsLauncher; import org.thoughtcrime.securesms.insights.InsightsLauncher;
import org.thoughtcrime.securesms.invites.InviteReminderModel; import org.thoughtcrime.securesms.invites.InviteReminderModel;
import org.thoughtcrime.securesms.invites.InviteReminderRepository; import org.thoughtcrime.securesms.invites.InviteReminderRepository;
@ -212,7 +213,6 @@ import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.GroupShareProfileView; import org.thoughtcrime.securesms.profiles.GroupShareProfileView;
import org.thoughtcrime.securesms.profiles.spoofing.ReviewBannerView; import org.thoughtcrime.securesms.profiles.spoofing.ReviewBannerView;
import org.thoughtcrime.securesms.profiles.spoofing.ReviewCardDialogFragment; import org.thoughtcrime.securesms.profiles.spoofing.ReviewCardDialogFragment;
import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil;
import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment; import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment;
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment; import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment;
@ -457,6 +457,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
initializeMentionsViewModel(); initializeMentionsViewModel();
initializeEnabledCheck(); initializeEnabledCheck();
initializePendingRequestsBanner(); initializePendingRequestsBanner();
initializeGroupV1MigrationSuggestionsBanner();
initializeSecurity(recipient.get().isRegistered(), isDefaultSms).addListener(new AssertedSuccessListener<Boolean>() { initializeSecurity(recipient.get().isRegistered(), isDefaultSms).addListener(new AssertedSuccessListener<Boolean>() {
@Override @Override
public void onSuccess(Boolean result) { public void onSuccess(Boolean result) {
@ -1547,6 +1548,11 @@ public class ConversationActivity extends PassphraseRequiredActivity
.observe(this, actionablePendingGroupRequests -> updateReminders()); .observe(this, actionablePendingGroupRequests -> updateReminders());
} }
private void initializeGroupV1MigrationSuggestionsBanner() {
groupViewModel.getGroupV1MigrationSuggestions()
.observe(this, s -> updateReminders());
}
private ListenableFuture<Boolean> initializeDraftFromDatabase() { private ListenableFuture<Boolean> initializeDraftFromDatabase() {
SettableFuture<Boolean> future = new SettableFuture<>(); SettableFuture<Boolean> future = new SettableFuture<>();
@ -1702,6 +1708,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
protected void updateReminders() { protected void updateReminders() {
Optional<Reminder> inviteReminder = inviteReminderModel.getReminder(); Optional<Reminder> inviteReminder = inviteReminderModel.getReminder();
Integer actionableRequestingMembers = groupViewModel.getActionableRequestingMembers().getValue(); Integer actionableRequestingMembers = groupViewModel.getActionableRequestingMembers().getValue();
List<RecipientId> gv1MigrationSuggestions = groupViewModel.getGroupV1MigrationSuggestions().getValue();
if (UnauthorizedReminder.isEligible(this)) { if (UnauthorizedReminder.isEligible(this)) {
reminderView.get().showReminder(new UnauthorizedReminder(this)); reminderView.get().showReminder(new UnauthorizedReminder(this));
@ -1726,25 +1733,32 @@ public class ConversationActivity extends PassphraseRequiredActivity
startActivity(ManagePendingAndRequestingMembersActivity.newIntent(this, getRecipient().getGroupId().get().requireV2())); startActivity(ManagePendingAndRequestingMembersActivity.newIntent(this, getRecipient().getGroupId().get().requireV2()));
} }
}); });
} else if (gv1MigrationSuggestions != null && gv1MigrationSuggestions.size() > 0 && recipient.get().isPushV2Group()) {
reminderView.get().showReminder(new GroupsV1MigrationSuggestionsReminder(this, gv1MigrationSuggestions));
reminderView.get().setOnActionClickListener(actionId -> {
if (actionId == R.id.reminder_action_gv1_suggestion_add_members) {
GroupsV1MigrationSuggestionsDialog.show(this, recipient.get().requireGroupId().requireV2(), gv1MigrationSuggestions);
} else if (actionId == R.id.reminder_action_gv1_suggestion_not_now) {
groupViewModel.onSuggestedMembersBannerDismissed(recipient.get().requireGroupId());
}
});
reminderView.get().setOnDismissListener(() -> {
});
} else if (reminderView.resolved()) { } else if (reminderView.resolved()) {
reminderView.get().hide(); reminderView.get().hide();
} }
} }
private void handleReminderAction(@IdRes int reminderActionId) { private void handleReminderAction(@IdRes int reminderActionId) {
switch (reminderActionId) { if (reminderActionId == R.id.reminder_action_invite) {
case R.id.reminder_action_invite: handleInviteLink();
handleInviteLink(); reminderView.get().requestDismiss();
reminderView.get().requestDismiss(); } else if (reminderActionId == R.id.reminder_action_view_insights) {
break; InsightsLauncher.showInsightsDashboard(getSupportFragmentManager());
case R.id.reminder_action_view_insights: } else if (reminderActionId == R.id.reminder_action_update_now) {
InsightsLauncher.showInsightsDashboard(getSupportFragmentManager()); PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(this);
break; } else {
case R.id.reminder_action_update_now: throw new IllegalArgumentException("Unknown ID: " + reminderActionId);
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(this);
break;
default:
throw new IllegalArgumentException("Unknown ID: " + reminderActionId);
} }
} }

View file

@ -4,6 +4,7 @@ import android.app.Application;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations; import androidx.lifecycle.Transformations;
@ -15,6 +16,7 @@ import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord; import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupChangeBusyException; import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
import org.thoughtcrime.securesms.groups.GroupChangeFailedException; import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
@ -24,14 +26,17 @@ import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
import org.thoughtcrime.securesms.profiles.spoofing.ReviewRecipient; import org.thoughtcrime.securesms.profiles.spoofing.ReviewRecipient;
import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil; import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.AsynchronousCallback; import org.thoughtcrime.securesms.util.AsynchronousCallback;
import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import java.io.IOException; import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Set;
final class ConversationGroupViewModel extends ViewModel { final class ConversationGroupViewModel extends ViewModel {
@ -40,6 +45,7 @@ final class ConversationGroupViewModel extends ViewModel {
private final LiveData<GroupDatabase.MemberLevel> selfMembershipLevel; private final LiveData<GroupDatabase.MemberLevel> selfMembershipLevel;
private final LiveData<Integer> actionableRequestingMembers; private final LiveData<Integer> actionableRequestingMembers;
private final LiveData<ReviewState> reviewState; private final LiveData<ReviewState> reviewState;
private final LiveData<List<RecipientId>> gv1MigrationSuggestions;
private ConversationGroupViewModel() { private ConversationGroupViewModel() {
this.liveRecipient = new MutableLiveData<>(); this.liveRecipient = new MutableLiveData<>();
@ -58,6 +64,7 @@ final class ConversationGroupViewModel extends ViewModel {
this.groupActiveState = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToGroupActiveState)); this.groupActiveState = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToGroupActiveState));
this.selfMembershipLevel = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToSelfMembershipLevel)); this.selfMembershipLevel = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToSelfMembershipLevel));
this.actionableRequestingMembers = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToActionableRequestingMemberCount)); this.actionableRequestingMembers = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToActionableRequestingMemberCount));
this.gv1MigrationSuggestions = Transformations.distinctUntilChanged(LiveDataUtil.mapAsync(groupRecord, ConversationGroupViewModel::mapToGroupV1MigrationSuggestions));
this.reviewState = LiveDataUtil.combineLatest(groupRecord, this.reviewState = LiveDataUtil.combineLatest(groupRecord,
duplicates, duplicates,
(record, dups) -> dups.isEmpty() (record, dups) -> dups.isEmpty()
@ -70,6 +77,15 @@ final class ConversationGroupViewModel extends ViewModel {
liveRecipient.setValue(recipient); liveRecipient.setValue(recipient);
} }
void onSuggestedMembersBannerDismissed(@NonNull GroupId groupId) {
SignalExecutors.BOUNDED.execute(() -> {
if (groupId.isV2()) {
DatabaseFactory.getGroupDatabase(ApplicationDependencies.getApplication()).clearFormerV1Members(groupId.requireV2());
liveRecipient.postValue(liveRecipient.getValue());
}
});
}
/** /**
* The number of pending group join requests that can be actioned by this client. * The number of pending group join requests that can be actioned by this client.
*/ */
@ -89,6 +105,10 @@ final class ConversationGroupViewModel extends ViewModel {
return reviewState; return reviewState;
} }
@NonNull LiveData<List<RecipientId>> getGroupV1MigrationSuggestions() {
return gv1MigrationSuggestions;
}
private static @Nullable GroupRecord getGroupRecordForRecipient(@Nullable Recipient recipient) { private static @Nullable GroupRecord getGroupRecordForRecipient(@Nullable Recipient recipient) {
if (recipient != null && recipient.isGroup()) { if (recipient != null && recipient.isGroup()) {
Application context = ApplicationDependencies.getApplication(); Application context = ApplicationDependencies.getApplication();
@ -127,6 +147,24 @@ final class ConversationGroupViewModel extends ViewModel {
return record.memberLevel(Recipient.self()); return record.memberLevel(Recipient.self());
} }
@WorkerThread
private static List<RecipientId> mapToGroupV1MigrationSuggestions(@Nullable GroupRecord record) {
if (record == null) {
return Collections.emptyList();
}
Set<RecipientId> difference = SetUtil.difference(record.getFormerV1Members(), record.getMembers());
return Stream.of(Recipient.resolvedList(difference))
.filter(r -> r.hasUuid() &&
r.getGroupsV1MigrationCapability() == Recipient.Capability.SUPPORTED &&
r.getGroupsV2Capability() == Recipient.Capability.SUPPORTED &&
r.getProfileKey() != null &&
r.getRegistered() == RecipientDatabase.RegisteredState.REGISTERED)
.map(Recipient::getId)
.toList();
}
public static void onCancelJoinRequest(@NonNull Recipient recipient, public static void onCancelJoinRequest(@NonNull Recipient recipient,
@NonNull AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason> callback) @NonNull AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason> callback)
{ {

View file

@ -37,6 +37,7 @@ import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.Closeable; import java.io.Closeable;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
@ -162,6 +163,8 @@ public final class GroupDatabase extends Database {
values.putNull(FORMER_V1_MEMBERS); values.putNull(FORMER_V1_MEMBERS);
databaseHelper.getWritableDatabase().update(TABLE_NAME, values, GROUP_ID + " = ?", SqlUtil.buildArgs(id)); databaseHelper.getWritableDatabase().update(TABLE_NAME, values, GROUP_ID + " = ?", SqlUtil.buildArgs(id));
Recipient.live(Recipient.externalGroupExact(context, id).getId()).refresh();
} }
Optional<GroupRecord> getGroup(Cursor cursor) { Optional<GroupRecord> getGroup(Cursor cursor) {

View file

@ -0,0 +1,109 @@
package org.thoughtcrime.securesms.groups.ui.migration;
import android.content.DialogInterface;
import android.content.DialogInterface.OnDismissListener;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.FragmentActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupInsufficientRightsException;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.groups.GroupNotAMemberException;
import org.thoughtcrime.securesms.groups.MembershipNotSuitableForV2Exception;
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import java.io.IOException;
import java.util.List;
/**
* Shows a list of members that got lost when migrating from a V1->V2 group, giving you the chance
* to add them back.
*/
public final class GroupsV1MigrationSuggestionsDialog {
private static final String TAG = Log.tag(GroupsV1MigrationSuggestionsDialog.class);
private final FragmentActivity fragmentActivity;
private final GroupId.V2 groupId;
private final List<RecipientId> suggestions;
public static void show(@NonNull FragmentActivity activity,
@NonNull GroupId.V2 groupId,
@NonNull List<RecipientId> suggestions)
{
new GroupsV1MigrationSuggestionsDialog(activity, groupId, suggestions).display();
}
private GroupsV1MigrationSuggestionsDialog(@NonNull FragmentActivity activity,
@NonNull GroupId.V2 groupId,
@NonNull List<RecipientId> suggestions)
{
this.fragmentActivity = activity;
this.groupId = groupId;
this.suggestions = suggestions;
}
private void display() {
AlertDialog dialog = new AlertDialog.Builder(fragmentActivity)
.setTitle(fragmentActivity.getResources().getQuantityString(R.plurals.GroupsV1MigrationSuggestionsDialog_add_members_question, suggestions.size()))
.setMessage(fragmentActivity.getResources().getQuantityString(R.plurals.GroupsV1MigrationSuggestionsDialog_these_members_couldnt_be_automatically_added, suggestions.size()))
.setView(R.layout.dialog_group_members)
.setPositiveButton(fragmentActivity.getResources().getQuantityString(R.plurals.GroupsV1MigrationSuggestionsDialog_add_members, suggestions.size()), (d, i) -> onAddClicked(d))
.setNegativeButton(android.R.string.cancel, (d, i) -> d.dismiss())
.show();
GroupMemberListView memberListView = dialog.findViewById(R.id.list_members);
SimpleTask.run(() -> Recipient.resolvedList(suggestions),
memberListView::setDisplayOnlyMembers);
}
private void onAddClicked(@NonNull DialogInterface rootDialog) {
SimpleProgressDialog.DismissibleDialog progressDialog = SimpleProgressDialog.showDelayed(fragmentActivity, 300, 0);
SimpleTask.run(SignalExecutors.UNBOUNDED, () -> {
try {
GroupManager.addMembers(fragmentActivity, groupId.requirePush(), suggestions);
Log.i(TAG, "Successfully added members! Clearing former members.");
DatabaseFactory.getGroupDatabase(fragmentActivity).clearFormerV1Members(groupId);
return Result.SUCCESS;
} catch (IOException | GroupChangeBusyException e) {
Log.w(TAG, "Temporary failure.", e);
return Result.NETWORK_ERROR;
} catch (GroupNotAMemberException | GroupInsufficientRightsException | MembershipNotSuitableForV2Exception | GroupChangeFailedException e) {
Log.w(TAG, "Permanent failure! Clearing former members.", e);
DatabaseFactory.getGroupDatabase(fragmentActivity).clearFormerV1Members(groupId);
return Result.IMPOSSIBLE;
}
}, result -> {
progressDialog.dismiss();
rootDialog.dismiss();
switch (result) {
case NETWORK_ERROR:
Toast.makeText(fragmentActivity, fragmentActivity.getResources().getQuantityText(R.plurals.GroupsV1MigrationSuggestionsDialog_failed_to_add_members_try_again_later, suggestions.size()), Toast.LENGTH_SHORT).show();
break;
case IMPOSSIBLE:
Toast.makeText(fragmentActivity, fragmentActivity.getResources().getQuantityText(R.plurals.GroupsV1MigrationSuggestionsDialog_cannot_add_members, suggestions.size()), Toast.LENGTH_SHORT).show();
break;
}
});
}
private enum Result {
SUCCESS, NETWORK_ERROR, IMPOSSIBLE
}
}

View file

@ -18,12 +18,14 @@ import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
public class SignalExecutors { public final class SignalExecutors {
public static final ExecutorService UNBOUNDED = Executors.newCachedThreadPool(new NumberedThreadFactory("signal-unbounded")); public static final ExecutorService UNBOUNDED = Executors.newCachedThreadPool(new NumberedThreadFactory("signal-unbounded"));
public static final ExecutorService BOUNDED = Executors.newFixedThreadPool(getIdealThreadCount(), new NumberedThreadFactory("signal-bounded")); public static final ExecutorService BOUNDED = Executors.newFixedThreadPool(getIdealThreadCount(), new NumberedThreadFactory("signal-bounded"));
public static final ExecutorService SERIAL = Executors.newSingleThreadExecutor(new NumberedThreadFactory("signal-serial")); public static final ExecutorService SERIAL = Executors.newSingleThreadExecutor(new NumberedThreadFactory("signal-serial"));
private SignalExecutors() {}
public static ExecutorService newCachedSingleThreadExecutor(final String name) { public static ExecutorService newCachedSingleThreadExecutor(final String name) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 15, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), r -> new Thread(r, name)); ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 15, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), r -> new Thread(r, name));
executor.allowCoreThreadTimeOut(true); executor.allowCoreThreadTimeOut(true);

View file

@ -8,4 +8,7 @@
<item name="reminder_action_invite" type="id" /> <item name="reminder_action_invite" type="id" />
<item name="reminder_action_update_now" type="id" /> <item name="reminder_action_update_now" type="id" />
<item name="reminder_action_review_join_requests" type="id" /> <item name="reminder_action_review_join_requests" type="id" />
<item name="reminder_action_gv1_suggestion_not_now" type="id" />
<item name="reminder_action_gv1_suggestion_add_members" type="id" />
</resources> </resources>

View file

@ -556,6 +556,39 @@
<item quantity="other">These members are not capable of joining New Groups, and have been removed from the group:</item> <item quantity="other">These members are not capable of joining New Groups, and have been removed from the group:</item>
</plurals> </plurals>
<!-- GroupsV1MigrationSuggestionsReminder -->
<plurals name="GroupsV1MigrationSuggestionsReminder_members_couldnt_be_added_to_the_new_group">
<item quantity="one">%1$d member couldn\'t be re-added to the New Group. Do you want to add them now?</item>
<item quantity="other">%1$d members couldn\'t be re-added to the New Group. Do you want to add them now?</item>
</plurals>
<plurals name="GroupsV1MigrationSuggestionsReminder_add_members">
<item quantity="one">Add member</item>
<item quantity="other">Add members</item>
</plurals>
<string name="GroupsV1MigrationSuggestionsReminder_not_now">Not now</string>
<!-- GroupsV1MigrationSuggestionsDialog -->
<plurals name="GroupsV1MigrationSuggestionsDialog_add_members_question">
<item quantity="one">Add member?</item>
<item quantity="other">Add members?</item>
</plurals>
<plurals name="GroupsV1MigrationSuggestionsDialog_these_members_couldnt_be_automatically_added">
<item quantity="one">This member couldn\'t be automatically added to the New Group when it was upgraded:</item>
<item quantity="other">These members couldn\'t be automatically added to the New Group when it was upgraded:</item>
</plurals>
<plurals name="GroupsV1MigrationSuggestionsDialog_add_members">
<item quantity="one">Add member</item>
<item quantity="other">Add members</item>
</plurals>
<plurals name="GroupsV1MigrationSuggestionsDialog_failed_to_add_members_try_again_later">
<item quantity="one">Failed to add member. Try again later.</item>
<item quantity="other">Failed to add members. Try again later.</item>
</plurals>
<plurals name="GroupsV1MigrationSuggestionsDialog_cannot_add_members">
<item quantity="one">Cannot add member.</item>
<item quantity="other">Cannot add members.</item>
</plurals>
<!-- LeaveGroupDialog --> <!-- LeaveGroupDialog -->
<string name="LeaveGroupDialog_leave_group">Leave group?</string> <string name="LeaveGroupDialog_leave_group">Leave group?</string>
<string name="LeaveGroupDialog_you_will_no_longer_be_able_to_send_or_receive_messages_in_this_group">You will no longer be able to send or receive messages in this group.</string> <string name="LeaveGroupDialog_you_will_no_longer_be_able_to_send_or_receive_messages_in_this_group">You will no longer be able to send or receive messages in this group.</string>