diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/GroupsV1MigrationSuggestionsReminder.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/GroupsV1MigrationSuggestionsReminder.java index d3c7f4dc5..cfad166bd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/GroupsV1MigrationSuggestionsReminder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/GroupsV1MigrationSuggestionsReminder.java @@ -16,7 +16,7 @@ public class GroupsV1MigrationSuggestionsReminder extends Reminder { public GroupsV1MigrationSuggestionsReminder(@NonNull Context context, @NonNull List 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)); + addAction(new Action(context.getResources().getString(R.string.GroupsV1MigrationSuggestionsReminder_no_thanks), R.id.reminder_action_gv1_suggestion_no_thanks)); } @Override 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 a97ef7323..29edb0149 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -1782,8 +1782,8 @@ public class ConversationActivity extends PassphraseRequiredActivity 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()); + } else if (actionId == R.id.reminder_action_gv1_suggestion_no_thanks) { + groupViewModel.onSuggestedMembersBannerDismissed(recipient.get().requireGroupId(), gv1MigrationSuggestions); } }); reminderView.get().setOnDismissListener(() -> { 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 4fc7484aa..9b4d4daeb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationGroupViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationGroupViewModel.java @@ -31,13 +31,11 @@ import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.AsynchronousCallback; import org.thoughtcrime.securesms.util.FeatureFlags; -import org.thoughtcrime.securesms.util.SetUtil; import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; import java.io.IOException; import java.util.Collections; import java.util.List; -import java.util.Set; import java.util.concurrent.TimeUnit; final class ConversationGroupViewModel extends ViewModel { @@ -83,10 +81,10 @@ final class ConversationGroupViewModel extends ViewModel { liveRecipient.setValue(recipient); } - void onSuggestedMembersBannerDismissed(@NonNull GroupId groupId) { + void onSuggestedMembersBannerDismissed(@NonNull GroupId groupId, @NonNull List suggestions) { SignalExecutors.BOUNDED.execute(() -> { if (groupId.isV2()) { - DatabaseFactory.getGroupDatabase(ApplicationDependencies.getApplication()).clearFormerV1Members(groupId.requireV2()); + DatabaseFactory.getGroupDatabase(ApplicationDependencies.getApplication()).removeUnmigratedV1Members(groupId.requireV2(), suggestions); liveRecipient.postValue(liveRecipient.getValue()); } }); @@ -177,9 +175,9 @@ final class ConversationGroupViewModel extends ViewModel { return Collections.emptyList(); } - Set difference = SetUtil.difference(record.getFormerV1Members(), record.getMembers()); - - return Stream.of(Recipient.resolvedList(difference)) + return Stream.of(record.getUnmigratedV1Members()) + .filterNot(m -> record.getMembers().contains(m)) + .map(Recipient::resolved) .filter(GroupsV1MigrationUtil::isAutoMigratable) .map(Recipient::getId) .toList(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index 549bb5a59..ea1fcd9e8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -15,8 +15,10 @@ import com.google.protobuf.InvalidProtocolBufferException; import org.signal.core.util.logging.Log; import org.signal.storageservice.protos.groups.AccessControl; +import org.signal.storageservice.protos.groups.GroupChange; import org.signal.storageservice.protos.groups.Member; import org.signal.storageservice.protos.groups.local.DecryptedGroup; +import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; import org.signal.storageservice.protos.groups.local.DecryptedMember; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.groups.GroupMasterKey; @@ -33,6 +35,7 @@ import org.thoughtcrime.securesms.util.SqlUtil; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; +import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.util.UuidUtil; @@ -47,6 +50,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.UUID; @Trace @@ -54,22 +58,22 @@ public final class GroupDatabase extends Database { private static final String TAG = Log.tag(GroupDatabase.class); - static final String TABLE_NAME = "groups"; - private static final String ID = "_id"; - static final String GROUP_ID = "group_id"; - static final String RECIPIENT_ID = "recipient_id"; - private static final String TITLE = "title"; - static final String MEMBERS = "members"; - private static final String AVATAR_ID = "avatar_id"; - private static final String AVATAR_KEY = "avatar_key"; - private static final String AVATAR_CONTENT_TYPE = "avatar_content_type"; - private static final String AVATAR_RELAY = "avatar_relay"; - private static final String AVATAR_DIGEST = "avatar_digest"; - private static final String TIMESTAMP = "timestamp"; - static final String ACTIVE = "active"; - static final String MMS = "mms"; - private static final String EXPECTED_V2_ID = "expected_v2_id"; - private static final String FORMER_V1_MEMBERS = "former_v1_members"; + static final String TABLE_NAME = "groups"; + private static final String ID = "_id"; + static final String GROUP_ID = "group_id"; + static final String RECIPIENT_ID = "recipient_id"; + private static final String TITLE = "title"; + static final String MEMBERS = "members"; + private static final String AVATAR_ID = "avatar_id"; + private static final String AVATAR_KEY = "avatar_key"; + private static final String AVATAR_CONTENT_TYPE = "avatar_content_type"; + private static final String AVATAR_RELAY = "avatar_relay"; + private static final String AVATAR_DIGEST = "avatar_digest"; + private static final String TIMESTAMP = "timestamp"; + static final String ACTIVE = "active"; + static final String MMS = "mms"; + private static final String EXPECTED_V2_ID = "expected_v2_id"; + private static final String UNMIGRATED_V1_MEMBERS = "former_v1_members"; /* V2 Group columns */ @@ -80,24 +84,24 @@ public final class GroupDatabase extends Database { /** Serialized {@link DecryptedGroup} protobuf */ private static final String V2_DECRYPTED_GROUP = "decrypted_group"; - public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " + - GROUP_ID + " TEXT, " + - RECIPIENT_ID + " INTEGER, " + - TITLE + " TEXT, " + - MEMBERS + " TEXT, " + - AVATAR_ID + " INTEGER, " + - AVATAR_KEY + " BLOB, " + - AVATAR_CONTENT_TYPE + " TEXT, " + - AVATAR_RELAY + " TEXT, " + - TIMESTAMP + " INTEGER, " + - ACTIVE + " INTEGER DEFAULT 1, " + - AVATAR_DIGEST + " BLOB, " + - MMS + " INTEGER DEFAULT 0, " + - V2_MASTER_KEY + " BLOB, " + - V2_REVISION + " BLOB, " + - V2_DECRYPTED_GROUP + " BLOB, " + - EXPECTED_V2_ID + " TEXT DEFAULT NULL, " + - FORMER_V1_MEMBERS + " TEXT DEFAULT NULL);"; + public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " + + GROUP_ID + " TEXT, " + + RECIPIENT_ID + " INTEGER, " + + TITLE + " TEXT, " + + MEMBERS + " TEXT, " + + AVATAR_ID + " INTEGER, " + + AVATAR_KEY + " BLOB, " + + AVATAR_CONTENT_TYPE + " TEXT, " + + AVATAR_RELAY + " TEXT, " + + TIMESTAMP + " INTEGER, " + + ACTIVE + " INTEGER DEFAULT 1, " + + AVATAR_DIGEST + " BLOB, " + + MMS + " INTEGER DEFAULT 0, " + + V2_MASTER_KEY + " BLOB, " + + V2_REVISION + " BLOB, " + + V2_DECRYPTED_GROUP + " BLOB, " + + EXPECTED_V2_ID + " TEXT DEFAULT NULL, " + + UNMIGRATED_V1_MEMBERS + " TEXT DEFAULT NULL);"; public static final String[] CREATE_INDEXS = { "CREATE UNIQUE INDEX IF NOT EXISTS group_id_index ON " + TABLE_NAME + " (" + GROUP_ID + ");", @@ -106,7 +110,7 @@ public final class GroupDatabase extends Database { }; private static final String[] GROUP_PROJECTION = { - GROUP_ID, RECIPIENT_ID, TITLE, MEMBERS, FORMER_V1_MEMBERS, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST, + GROUP_ID, RECIPIENT_ID, TITLE, MEMBERS, UNMIGRATED_V1_MEMBERS, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST, TIMESTAMP, ACTIVE, MMS, V2_MASTER_KEY, V2_REVISION, V2_DECRYPTED_GROUP }; @@ -162,9 +166,23 @@ public final class GroupDatabase extends Database { } } - public void clearFormerV1Members(@NonNull GroupId.V2 id) { + /** + * Removes the specified members from the list of 'unmigrated V1 members' -- the list of members + * that were either dropped or had to be invited when migrating the group from V1->V2. + */ + public void removeUnmigratedV1Members(@NonNull GroupId.V2 id, @NonNull List toRemove) { + Optional group = getGroup(id); + + if (!group.isPresent()) { + Log.w(TAG, "Couldn't find the group!", new Throwable()); + return; + } + + List newUnmigrated = group.get().getUnmigratedV1Members(); + newUnmigrated.removeAll(toRemove); + ContentValues values = new ContentValues(); - values.putNull(FORMER_V1_MEMBERS); + values.put(UNMIGRATED_V1_MEMBERS, newUnmigrated.isEmpty() ? null : RecipientId.toSerializedList(newUnmigrated)); databaseHelper.getWritableDatabase().update(TABLE_NAME, values, GROUP_ID + " = ?", SqlUtil.buildArgs(id)); @@ -505,16 +523,15 @@ public final class GroupDatabase extends Database { contentValues.put(V2_MASTER_KEY, groupMasterKey.serialize()); contentValues.putNull(EXPECTED_V2_ID); - List newMembers = Stream.of(DecryptedGroupUtil.membersToUuidList(decryptedGroup.getMembersList())).map(u -> RecipientId.from(u, null)).toList(); - List pendingMembers = Stream.of(DecryptedGroupUtil.pendingToUuidList(decryptedGroup.getPendingMembersList())).map(u -> RecipientId.from(u, null)).toList(); + List newMembers = uuidsToRecipientIds(DecryptedGroupUtil.membersToUuidList(decryptedGroup.getMembersList())); + List pendingMembers = uuidsToRecipientIds(DecryptedGroupUtil.pendingToUuidList(decryptedGroup.getPendingMembersList())); newMembers.addAll(pendingMembers); - List droppedMembers = new ArrayList<>(SetUtil.difference(record.getMembers(), newMembers)); + List droppedMembers = new ArrayList<>(SetUtil.difference(record.getMembers(), newMembers)); + List unmigratedMembers = Util.concatenatedList(pendingMembers, droppedMembers); - if (droppedMembers.size() > 0) { - contentValues.put(FORMER_V1_MEMBERS, RecipientId.toSerializedList(record.getMembers())); - } + contentValues.put(UNMIGRATED_V1_MEMBERS, unmigratedMembers.isEmpty() ? null : RecipientId.toSerializedList(unmigratedMembers)); int updated = db.update(TABLE_NAME, contentValues, GROUP_ID + " = ?", SqlUtil.buildArgs(groupIdV1.toString())); @@ -543,10 +560,31 @@ public final class GroupDatabase extends Database { } public void update(@NonNull GroupId.V2 groupId, @NonNull DecryptedGroup decryptedGroup) { - RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context); - RecipientId groupRecipientId = recipientDatabase.getOrInsertFromGroupId(groupId); - String title = decryptedGroup.getTitle(); - ContentValues contentValues = new ContentValues(); + RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context); + RecipientId groupRecipientId = recipientDatabase.getOrInsertFromGroupId(groupId); + Optional existingGroup = getGroup(groupId); + String title = decryptedGroup.getTitle(); + ContentValues contentValues = new ContentValues(); + + if (existingGroup.isPresent() && existingGroup.get().getUnmigratedV1Members().size() > 0 && existingGroup.get().isV2Group()) { + Set unmigratedV1Members = new HashSet<>(existingGroup.get().getUnmigratedV1Members()); + + DecryptedGroupChange change = GroupChangeReconstruct.reconstructGroupChange(existingGroup.get().requireV2GroupProperties().getDecryptedGroup(), decryptedGroup); + + List addedMembers = uuidsToRecipientIds(DecryptedGroupUtil.membersToUuidList(change.getNewMembersList())); + List removedMembers = uuidsToRecipientIds(DecryptedGroupUtil.removedMembersUuidList(change)); + List addedInvites = uuidsToRecipientIds(DecryptedGroupUtil.pendingToUuidList(change.getNewPendingMembersList())); + List removedInvites = uuidsToRecipientIds(DecryptedGroupUtil.removedPendingMembersUuidList(change)); + List acceptedInvites = uuidsToRecipientIds(DecryptedGroupUtil.membersToUuidList(change.getPromotePendingMembersList())); + + unmigratedV1Members.removeAll(addedMembers); + unmigratedV1Members.removeAll(removedMembers); + unmigratedV1Members.removeAll(addedInvites); + unmigratedV1Members.removeAll(removedInvites); + unmigratedV1Members.removeAll(acceptedInvites); + + contentValues.put(UNMIGRATED_V1_MEMBERS, unmigratedV1Members.isEmpty() ? null : RecipientId.toSerializedList(unmigratedV1Members)); + } contentValues.put(TITLE, title); contentValues.put(V2_REVISION, decryptedGroup.getRevision()); @@ -688,16 +726,11 @@ public final class GroupDatabase extends Database { } } - @WorkerThread - public boolean isPendingMember(@NonNull GroupId.Push groupId, @NonNull Recipient recipient) { - return getGroup(groupId).transform(g -> g.isPendingMember(recipient)).or(false); - } - private static String serializeV2GroupMembers(@NonNull DecryptedGroup decryptedGroup) { - List groupMembers = new ArrayList<>(decryptedGroup.getMembersCount()); + private static List uuidsToRecipientIds(@NonNull List uuids) { + List groupMembers = new ArrayList<>(uuids.size()); - for (DecryptedMember member : decryptedGroup.getMembersList()) { - UUID uuid = UuidUtil.fromByteString(member.getUuid()); + for (UUID uuid : uuids) { if (UuidUtil.UNKNOWN_UUID.equals(uuid)) { Log.w(TAG, "Seen unknown UUID in members list"); } else { @@ -707,7 +740,14 @@ public final class GroupDatabase extends Database { Collections.sort(groupMembers); - return RecipientId.toSerializedList(groupMembers); + return groupMembers; + } + + private static String serializeV2GroupMembers(@NonNull DecryptedGroup decryptedGroup) { + List uuids = DecryptedGroupUtil.membersToUuidList(decryptedGroup.getMembersList()); + List recipientIds = uuidsToRecipientIds(uuids); + + return RecipientId.toSerializedList(recipientIds); } public @NonNull List getAllGroupV2Ids() { @@ -772,7 +812,7 @@ public final class GroupDatabase extends Database { RecipientId.from(CursorUtil.requireString(cursor, RECIPIENT_ID)), CursorUtil.requireString(cursor, TITLE), CursorUtil.requireString(cursor, MEMBERS), - CursorUtil.requireString(cursor, FORMER_V1_MEMBERS), + CursorUtil.requireString(cursor, UNMIGRATED_V1_MEMBERS), CursorUtil.requireLong(cursor, AVATAR_ID), CursorUtil.requireBlob(cursor, AVATAR_KEY), CursorUtil.requireString(cursor, AVATAR_CONTENT_TYPE), @@ -798,7 +838,7 @@ public final class GroupDatabase extends Database { private final RecipientId recipientId; private final String title; private final List members; - private final List formerV1Members; + private final List unmigratedV1Members; private final long avatarId; private final byte[] avatarKey; private final byte[] avatarDigest; @@ -812,7 +852,7 @@ public final class GroupDatabase extends Database { @NonNull RecipientId recipientId, String title, String members, - String formerV1Members, + @Nullable String unmigratedV1Members, long avatarId, byte[] avatarKey, String avatarContentType, @@ -853,10 +893,10 @@ public final class GroupDatabase extends Database { this.members = Collections.emptyList(); } - if (!TextUtils.isEmpty(formerV1Members)) { - this.formerV1Members = RecipientId.fromSerializedList(formerV1Members); + if (!TextUtils.isEmpty(unmigratedV1Members)) { + this.unmigratedV1Members = RecipientId.fromSerializedList(unmigratedV1Members); } else { - this.formerV1Members = Collections.emptyList(); + this.unmigratedV1Members = Collections.emptyList(); } } @@ -876,8 +916,9 @@ public final class GroupDatabase extends Database { return members; } - public @NonNull List getFormerV1Members() { - return formerV1Members; + /** V1 members that were lost during the V1->V2 migration */ + public @NonNull List getUnmigratedV1Members() { + return unmigratedV1Members; } public boolean hasAvatar() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index e42c7b188..d744550bf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -164,8 +164,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int GV1_MIGRATION_LAST_SEEN = 82; private static final int VIEWED_RECEIPTS = 83; private static final int CLEAN_UP_GV1_IDS = 84; + private static final int GV1_MIGRATION_REFACTOR = 85; - private static final int DATABASE_VERSION = 84; + private static final int DATABASE_VERSION = 85; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -1234,6 +1235,15 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { } } + if (oldVersion < GV1_MIGRATION_REFACTOR) { + ContentValues values = new ContentValues(1); + values.putNull("former_v1_members"); + + int count = db.update("groups", values, "former_v1_members NOT NULL", null); + + Log.i(TAG, "Cleared former_v1_members for " + count + " rows"); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/migration/GroupsV1MigrationSuggestionsDialog.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/migration/GroupsV1MigrationSuggestionsDialog.java index e3f8a6690..2e99de1ab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/migration/GroupsV1MigrationSuggestionsDialog.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/migration/GroupsV1MigrationSuggestionsDialog.java @@ -75,15 +75,15 @@ public final class GroupsV1MigrationSuggestionsDialog { 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); + Log.i(TAG, "Successfully added members! Removing these dropped members from the list."); + DatabaseFactory.getGroupDatabase(fragmentActivity).removeUnmigratedV1Members(groupId, suggestions); 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); + Log.w(TAG, "Permanent failure! Removing these dropped members from the list.", e); + DatabaseFactory.getGroupDatabase(fragmentActivity).removeUnmigratedV1Members(groupId, suggestions); return Result.IMPOSSIBLE; } }, result -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientId.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientId.java index 71e4f573b..a674fba44 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientId.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientId.java @@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.UUID; import java.util.regex.Pattern; @@ -94,7 +95,7 @@ public class RecipientId implements Parcelable, Comparable { id = in.readLong(); } - public static @NonNull String toSerializedList(@NonNull List ids) { + public static @NonNull String toSerializedList(@NonNull Collection ids) { return Util.join(Stream.of(ids).map(RecipientId::serialize).toList(), String.valueOf(DELIMITER)); } diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml index 2d024ebd5..b84376281 100644 --- a/app/src/main/res/values/ids.xml +++ b/app/src/main/res/values/ids.xml @@ -9,7 +9,7 @@ - + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4ca1e8c59..22e703393 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -613,7 +613,7 @@ Add member Add members - Not now + No thanks