From 1999db97f25d7565ac3b040c0246ccef62be3e90 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 28 Sep 2022 14:09:44 -0400 Subject: [PATCH 01/78] Add support for system names on the ContactRecord. --- .../securesms/database/RecipientDatabase.kt | 23 +++++- .../securesms/jobs/JobManagerFactories.java | 2 + .../migrations/ApplicationMigrations.java | 7 +- .../StorageServiceSystemNameMigrationJob.java | 56 +++++++++++++++ .../storage/ContactRecordProcessor.java | 71 ++++++++++--------- .../securesms/storage/StorageSyncModels.java | 10 ++- .../storage/ContactRecordProcessorTest.kt | 16 +++++ .../storage/StorageSyncHelperTest.java | 3 +- .../api/storage/SignalContactRecord.java | 70 ++++++++++++------ .../src/main/proto/StorageService.proto | 2 + .../api/storage/SignalContactRecordTest.java | 3 +- 11 files changed, 197 insertions(+), 66 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/migrations/StorageServiceSystemNameMigrationJob.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt index 813166f894..6549198157 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt @@ -925,6 +925,21 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(recipientId) } + fun markAllSystemContactsNeedsSync() { + writableDatabase.withinTransaction { db -> + db + .select(ID) + .from(TABLE_NAME) + .where("$SYSTEM_CONTACT_URI NOT NULL") + .run() + .use { cursor -> + while (cursor.moveToNext()) { + rotateStorageId(RecipientId.from(cursor.requireLong(ID))) + } + } + } + } + fun applyStorageIdUpdates(storageIds: Map) { val db = writableDatabase db.beginTransaction() @@ -3667,8 +3682,9 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : private fun getValuesForStorageContact(contact: SignalContactRecord, isInsert: Boolean): ContentValues { return ContentValues().apply { - val profileName = ProfileName.fromParts(contact.givenName.orElse(null), contact.familyName.orElse(null)) - val username: String? = contact.username.orElse(null) + val profileName = ProfileName.fromParts(contact.profileGivenName.orElse(null), contact.profileFamilyName.orElse(null)) + val systemName = ProfileName.fromParts(contact.systemGivenName.orElse(null), contact.systemFamilyName.orElse(null)) + val username = contact.username.orElse(null) if (contact.serviceId.isValid) { put(SERVICE_ID, contact.serviceId.toString()) @@ -3682,6 +3698,9 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : put(PROFILE_GIVEN_NAME, profileName.givenName) put(PROFILE_FAMILY_NAME, profileName.familyName) put(PROFILE_JOINED_NAME, profileName.toString()) + put(SYSTEM_GIVEN_NAME, systemName.givenName) + put(SYSTEM_FAMILY_NAME, systemName.familyName) + put(SYSTEM_JOINED_NAME, systemName.toString()) put(PROFILE_KEY, contact.profileKey.map { source -> Base64.encodeBytes(source) }.orElse(null)) put(USERNAME, if (TextUtils.isEmpty(username)) null else username) put(PROFILE_SHARING, if (contact.isProfileSharingEnabled) "1" else "0") diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index b962c303d5..03394d37f2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -61,6 +61,7 @@ import org.thoughtcrime.securesms.migrations.StickerMyDailyLifeMigrationJob; import org.thoughtcrime.securesms.migrations.StorageCapabilityMigrationJob; import org.thoughtcrime.securesms.migrations.StorageServiceMigrationJob; +import org.thoughtcrime.securesms.migrations.StorageServiceSystemNameMigrationJob; import org.thoughtcrime.securesms.migrations.SyncDistributionListsMigrationJob; import org.thoughtcrime.securesms.migrations.TrimByLengthSettingsMigrationJob; import org.thoughtcrime.securesms.migrations.UserNotificationMigrationJob; @@ -221,6 +222,7 @@ public static Map getJobFactories(@NonNull Application appl put(StickerMyDailyLifeMigrationJob.KEY, new StickerMyDailyLifeMigrationJob.Factory()); put(StorageCapabilityMigrationJob.KEY, new StorageCapabilityMigrationJob.Factory()); put(StorageServiceMigrationJob.KEY, new StorageServiceMigrationJob.Factory()); + put(StorageServiceSystemNameMigrationJob.KEY, new StorageServiceSystemNameMigrationJob.Factory()); put(TrimByLengthSettingsMigrationJob.KEY, new TrimByLengthSettingsMigrationJob.Factory()); put(UserNotificationMigrationJob.KEY, new UserNotificationMigrationJob.Factory()); put(UuidMigrationJob.KEY, new UuidMigrationJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java index 481dcfc8e6..386c8bbf0a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -108,9 +108,10 @@ static final class Version { static final int REFRESH_PNI_REGISTRATION_ID = 64; static final int KBS_MIGRATION_2 = 65; static final int PNI_2 = 66; + static final int SYSTEM_NAME_SYNC = 67; } - public static final int CURRENT_VERSION = 66; + public static final int CURRENT_VERSION = 67; /** * This *must* be called after the {@link JobManager} has been instantiated, but *before* the call @@ -476,6 +477,10 @@ private static LinkedHashMap getMigrationJobs(@NonNull Co jobs.put(Version.PNI_2, new PniMigrationJob()); } + if (lastSeenVersion < Version.SYSTEM_NAME_SYNC) { + jobs.put(Version.SYSTEM_NAME_SYNC, new StorageServiceSystemNameMigrationJob()); + } + return jobs; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/StorageServiceSystemNameMigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/StorageServiceSystemNameMigrationJob.java new file mode 100644 index 0000000000..4ccf03c0aa --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/StorageServiceSystemNameMigrationJob.java @@ -0,0 +1,56 @@ +package org.thoughtcrime.securesms.migrations; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.database.SignalDatabase; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob; +import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob; +import org.thoughtcrime.securesms.storage.StorageSyncHelper; + +/** + * Added for when we started syncing contact names in storage service. + * Rotates the storageId of every system contact and then schedules a storage sync. + */ +public final class StorageServiceSystemNameMigrationJob extends MigrationJob { + + public static final String KEY = "StorageServiceSystemNameMigrationJob"; + + StorageServiceSystemNameMigrationJob() { + this(new Parameters.Builder().build()); + } + + private StorageServiceSystemNameMigrationJob(@NonNull Parameters parameters) { + super(parameters); + } + + @Override + public boolean isUiBlocking() { + return false; + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; + } + + @Override + public void performMigration() { + SignalDatabase.recipients().markAllSystemContactsNeedsSync(); + StorageSyncHelper.scheduleSyncForDataChange(); + } + + @Override + boolean shouldRetry(@NonNull Exception e) { + return false; + } + + public static class Factory implements Job.Factory { + @Override + public @NonNull StorageServiceSystemNameMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new StorageServiceSystemNameMigrationJob(parameters); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/ContactRecordProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/storage/ContactRecordProcessor.java index 95f04f6361..4e12945da7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/ContactRecordProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/ContactRecordProcessor.java @@ -1,7 +1,5 @@ package org.thoughtcrime.securesms.storage; -import android.content.Context; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -17,7 +15,6 @@ import org.whispersystems.signalservice.api.push.ACI; import org.whispersystems.signalservice.api.push.PNI; import org.whispersystems.signalservice.api.push.ServiceId; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.storage.SignalContactRecord; import org.whispersystems.signalservice.api.util.OptionalUtil; import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState; @@ -122,15 +119,15 @@ boolean isInvalid(@NonNull SignalContactRecord remote) { remote = remote.withoutPni(); } - String givenName; - String familyName; + String profileGivenName; + String profileFamilyName; - if (remote.getGivenName().isPresent() || remote.getFamilyName().isPresent()) { - givenName = remote.getGivenName().orElse(""); - familyName = remote.getFamilyName().orElse(""); + if (remote.getProfileGivenName().isPresent() || remote.getProfileFamilyName().isPresent()) { + profileGivenName = remote.getProfileGivenName().orElse(""); + profileFamilyName = remote.getProfileFamilyName().orElse(""); } else { - givenName = local.getGivenName().orElse(""); - familyName = local.getFamilyName().orElse(""); + profileGivenName = local.getProfileGivenName().orElse(""); + profileFamilyName = local.getProfileFamilyName().orElse(""); } IdentityState identityState; @@ -198,8 +195,10 @@ boolean isInvalid(@NonNull SignalContactRecord remote) { boolean hideStory = remote.shouldHideStory(); long unregisteredTimestamp = remote.getUnregisteredTimestamp(); boolean hidden = remote.isHidden(); - boolean matchesRemote = doParamsMatch(remote, unknownFields, serviceId, pni, e164, givenName, familyName, profileKey, username, identityState, identityKey, blocked, profileSharing, archived, forcedUnread, muteUntil, hideStory, unregisteredTimestamp, hidden); - boolean matchesLocal = doParamsMatch(local, unknownFields, serviceId, pni, e164, givenName, familyName, profileKey, username, identityState, identityKey, blocked, profileSharing, archived, forcedUnread, muteUntil, hideStory, unregisteredTimestamp, hidden); + String systemGivenName = SignalStore.account().isPrimaryDevice() ? local.getSystemGivenName().orElse("") : remote.getSystemGivenName().orElse(""); + String systemFamilyName = SignalStore.account().isPrimaryDevice() ? local.getSystemFamilyName().orElse("") : remote.getSystemFamilyName().orElse(""); + boolean matchesRemote = doParamsMatch(remote, unknownFields, serviceId, pni, e164, profileGivenName, profileFamilyName, systemGivenName, systemFamilyName, profileKey, username, identityState, identityKey, blocked, profileSharing, archived, forcedUnread, muteUntil, hideStory, unregisteredTimestamp, hidden); + boolean matchesLocal = doParamsMatch(local, unknownFields, serviceId, pni, e164, profileGivenName, profileFamilyName, systemGivenName, systemFamilyName, profileKey, username, identityState, identityKey, blocked, profileSharing, archived, forcedUnread, muteUntil, hideStory, unregisteredTimestamp, hidden); if (matchesRemote) { return remote; @@ -209,8 +208,10 @@ boolean isInvalid(@NonNull SignalContactRecord remote) { return new SignalContactRecord.Builder(keyGenerator.generate(), serviceId, unknownFields) .setE164(e164) .setPni(pni) - .setGivenName(givenName) - .setFamilyName(familyName) + .setProfileGivenName(profileGivenName) + .setProfileFamilyName(profileFamilyName) + .setSystemGivenName(systemGivenName) + .setSystemFamilyName(systemFamilyName) .setProfileKey(profileKey) .setUsername(username) .setIdentityState(identityState) @@ -254,8 +255,10 @@ private static boolean doParamsMatch(@NonNull SignalContactRecord contact, @NonNull ServiceId serviceId, @Nullable PNI pni, @Nullable String e164, - @NonNull String givenName, - @NonNull String familyName, + @NonNull String profileGivenName, + @NonNull String profileFamilyName, + @NonNull String systemGivenName, + @NonNull String systemFamilyName, @Nullable byte[] profileKey, @NonNull String username, @Nullable IdentityState identityState, @@ -269,23 +272,25 @@ private static boolean doParamsMatch(@NonNull SignalContactRecord contact, long unregisteredTimestamp, boolean hidden) { - return Arrays.equals(contact.serializeUnknownFields(), unknownFields) && - Objects.equals(contact.getServiceId(), serviceId) && - Objects.equals(contact.getPni().orElse(null), pni) && - Objects.equals(contact.getNumber().orElse(null), e164) && - Objects.equals(contact.getGivenName().orElse(""), givenName) && - Objects.equals(contact.getFamilyName().orElse(""), familyName) && - Arrays.equals(contact.getProfileKey().orElse(null), profileKey) && - Objects.equals(contact.getUsername().orElse(""), username) && - Objects.equals(contact.getIdentityState(), identityState) && - Arrays.equals(contact.getIdentityKey().orElse(null), identityKey) && - contact.isBlocked() == blocked && - contact.isProfileSharingEnabled() == profileSharing && - contact.isArchived() == archived && - contact.isForcedUnread() == forcedUnread && - contact.getMuteUntil() == muteUntil && - contact.shouldHideStory() == hideStory && - contact.getUnregisteredTimestamp() == unregisteredTimestamp && + return Arrays.equals(contact.serializeUnknownFields(), unknownFields) && + Objects.equals(contact.getServiceId(), serviceId) && + Objects.equals(contact.getPni().orElse(null), pni) && + Objects.equals(contact.getNumber().orElse(null), e164) && + Objects.equals(contact.getProfileGivenName().orElse(""), profileGivenName) && + Objects.equals(contact.getProfileFamilyName().orElse(""), profileFamilyName) && + Objects.equals(contact.getSystemGivenName().orElse(""), systemGivenName) && + Objects.equals(contact.getSystemFamilyName().orElse(""), systemFamilyName) && + Arrays.equals(contact.getProfileKey().orElse(null), profileKey) && + Objects.equals(contact.getUsername().orElse(""), username) && + Objects.equals(contact.getIdentityState(), identityState) && + Arrays.equals(contact.getIdentityKey().orElse(null), identityKey) && + contact.isBlocked() == blocked && + contact.isProfileSharingEnabled() == profileSharing && + contact.isArchived() == archived && + contact.isForcedUnread() == forcedUnread && + contact.getMuteUntil() == muteUntil && + contact.shouldHideStory() == hideStory && + contact.getUnregisteredTimestamp() == unregisteredTimestamp && contact.isHidden() == hidden; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.java b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.java index 711e116cb2..8100375ce0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.java @@ -11,15 +11,12 @@ import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.model.DistributionListId; -import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode; import org.thoughtcrime.securesms.database.model.DistributionListRecord; import org.thoughtcrime.securesms.database.model.RecipientRecord; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.subscription.Subscriber; -import org.thoughtcrime.securesms.util.FeatureFlags; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.storage.SignalAccountRecord; @@ -34,7 +31,6 @@ import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState; import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record; -import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -116,8 +112,10 @@ public static List localToRemotePinnedCo .setE164(recipient.getE164()) .setPni(recipient.getPni()) .setProfileKey(recipient.getProfileKey()) - .setGivenName(recipient.getProfileName().getGivenName()) - .setFamilyName(recipient.getProfileName().getFamilyName()) + .setProfileGivenName(recipient.getProfileName().getGivenName()) + .setProfileFamilyName(recipient.getProfileName().getFamilyName()) + .setSystemGivenName(recipient.getSystemProfileName().getGivenName()) + .setSystemFamilyName(recipient.getSystemProfileName().getFamilyName()) .setBlocked(recipient.isBlocked()) .setProfileSharingEnabled(recipient.isProfileSharing() || recipient.getSystemContactUri() != null) .setIdentityKey(recipient.getSyncExtras().getIdentityKey()) diff --git a/app/src/test/java/org/thoughtcrime/securesms/storage/ContactRecordProcessorTest.kt b/app/src/test/java/org/thoughtcrime/securesms/storage/ContactRecordProcessorTest.kt index 4183e3906d..471a1918da 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/storage/ContactRecordProcessorTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/storage/ContactRecordProcessorTest.kt @@ -3,17 +3,23 @@ package org.thoughtcrime.securesms.storage import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue +import org.junit.Before import org.junit.BeforeClass import org.junit.Rule import org.junit.Test import org.mockito.Mock import org.mockito.MockedStatic +import org.mockito.Mockito +import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` import org.mockito.internal.configuration.plugins.Plugins import org.mockito.internal.junit.JUnitRule import org.mockito.junit.MockitoRule import org.mockito.quality.Strictness import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.database.RecipientDatabase +import org.thoughtcrime.securesms.keyvalue.AccountValues +import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.testutil.EmptyLogger import org.thoughtcrime.securesms.util.FeatureFlags import org.whispersystems.signalservice.api.push.ACI @@ -35,6 +41,16 @@ class ContactRecordProcessorTest { @Mock lateinit var featureFlags: MockedStatic + @Mock + lateinit var signalStore: MockedStatic + + @Before + fun setup() { + val mockAccountValues = mock(AccountValues::class.java) + Mockito.lenient().`when`(mockAccountValues.isPrimaryDevice).thenReturn(true) + signalStore.`when` { SignalStore.account() }.thenReturn(mockAccountValues) + } + @Test fun `isInvalid, normal, false`() { // GIVEN diff --git a/app/src/test/java/org/thoughtcrime/securesms/storage/StorageSyncHelperTest.java b/app/src/test/java/org/thoughtcrime/securesms/storage/StorageSyncHelperTest.java index a422cacb8f..bbdfc1bf10 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/storage/StorageSyncHelperTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/storage/StorageSyncHelperTest.java @@ -14,7 +14,6 @@ import org.thoughtcrime.securesms.storage.StorageSyncHelper.IdDifferenceResult; import org.thoughtcrime.securesms.util.FeatureFlags; import org.whispersystems.signalservice.api.push.ServiceId; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.storage.SignalAccountRecord; import org.whispersystems.signalservice.api.storage.SignalContactRecord; import org.whispersystems.signalservice.api.storage.SignalGroupV1Record; @@ -178,7 +177,7 @@ private static SignalContactRecord.Builder contactBuilder(int key, { return new SignalContactRecord.Builder(byteArray(key), aci, null) .setE164(e164) - .setGivenName(profileName); + .setProfileGivenName(profileName); } private static StorageRecordUpdate update(E oldRecord, E newRecord) { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalContactRecord.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalContactRecord.java index 5ac79d8d8d..c78f7d720d 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalContactRecord.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalContactRecord.java @@ -28,8 +28,10 @@ public final class SignalContactRecord implements SignalRecord { private final ServiceId serviceId; private final Optional pni; private final Optional e164; - private final Optional givenName; - private final Optional familyName; + private final Optional profileGivenName; + private final Optional profileFamilyName; + private final Optional systemGivenName; + private final Optional systemFamilyName; private final Optional profileKey; private final Optional username; private final Optional identityKey; @@ -39,14 +41,16 @@ public SignalContactRecord(StorageId id, ContactRecord proto) { this.proto = proto; this.hasUnknownFields = ProtoUtil.hasUnknownFields(proto); - this.serviceId = ServiceId.parseOrUnknown(proto.getServiceId()); - this.pni = OptionalUtil.absentIfEmpty(proto.getServicePni()).map(PNI::parseOrNull); - this.e164 = OptionalUtil.absentIfEmpty(proto.getServiceE164()); - this.givenName = OptionalUtil.absentIfEmpty(proto.getGivenName()); - this.familyName = OptionalUtil.absentIfEmpty(proto.getFamilyName()); - this.profileKey = OptionalUtil.absentIfEmpty(proto.getProfileKey()); - this.username = OptionalUtil.absentIfEmpty(proto.getUsername()); - this.identityKey = OptionalUtil.absentIfEmpty(proto.getIdentityKey()); + this.serviceId = ServiceId.parseOrUnknown(proto.getServiceId()); + this.pni = OptionalUtil.absentIfEmpty(proto.getServicePni()).map(PNI::parseOrNull); + this.e164 = OptionalUtil.absentIfEmpty(proto.getServiceE164()); + this.profileGivenName = OptionalUtil.absentIfEmpty(proto.getGivenName()); + this.profileFamilyName = OptionalUtil.absentIfEmpty(proto.getFamilyName()); + this.systemGivenName = OptionalUtil.absentIfEmpty(proto.getSystemGivenName()); + this.systemFamilyName = OptionalUtil.absentIfEmpty(proto.getSystemFamilyName()); + this.profileKey = OptionalUtil.absentIfEmpty(proto.getProfileKey()); + this.username = OptionalUtil.absentIfEmpty(proto.getUsername()); + this.identityKey = OptionalUtil.absentIfEmpty(proto.getIdentityKey()); } @Override @@ -81,12 +85,20 @@ public String describeDiff(SignalRecord other) { diff.add("E164"); } - if (!Objects.equals(this.givenName, that.givenName)) { - diff.add("GivenName"); + if (!Objects.equals(this.profileGivenName, that.profileGivenName)) { + diff.add("ProfileGivenName"); } - if (!Objects.equals(this.familyName, that.familyName)) { - diff.add("FamilyName"); + if (!Objects.equals(this.profileFamilyName, that.profileFamilyName)) { + diff.add("ProfileFamilyName"); + } + + if (!Objects.equals(this.systemGivenName, that.systemGivenName)) { + diff.add("SystemGivenName"); + } + + if (!Objects.equals(this.systemFamilyName, that.systemFamilyName)) { + diff.add("SystemFamilyName"); } if (!OptionalUtil.byteArrayEquals(this.profileKey, that.profileKey)) { @@ -167,12 +179,20 @@ public Optional getNumber() { return e164; } - public Optional getGivenName() { - return givenName; + public Optional getProfileGivenName() { + return profileGivenName; + } + + public Optional getProfileFamilyName() { + return profileFamilyName; + } + + public Optional getSystemGivenName() { + return systemGivenName; } - public Optional getFamilyName() { - return familyName; + public Optional getSystemFamilyName() { + return systemFamilyName; } public Optional getProfileKey() { @@ -274,16 +294,26 @@ public Builder setPni(PNI pni) { return this; } - public Builder setGivenName(String givenName) { + public Builder setProfileGivenName(String givenName) { builder.setGivenName(givenName == null ? "" : givenName); return this; } - public Builder setFamilyName(String familyName) { + public Builder setProfileFamilyName(String familyName) { builder.setFamilyName(familyName == null ? "" : familyName); return this; } + public Builder setSystemGivenName(String givenName) { + builder.setSystemGivenName(givenName == null ? "" : givenName); + return this; + } + + public Builder setSystemFamilyName(String familyName) { + builder.setSystemFamilyName(familyName == null ? "" : familyName); + return this; + } + public Builder setProfileKey(byte[] profileKey) { builder.setProfileKey(profileKey == null ? ByteString.EMPTY : ByteString.copyFrom(profileKey)); return this; diff --git a/libsignal/service/src/main/proto/StorageService.proto b/libsignal/service/src/main/proto/StorageService.proto index 814d691380..1ad46d802d 100644 --- a/libsignal/service/src/main/proto/StorageService.proto +++ b/libsignal/service/src/main/proto/StorageService.proto @@ -87,6 +87,8 @@ message ContactRecord { uint64 mutedUntilTimestamp = 13; bool hideStory = 14; uint64 unregisteredAtTimestamp = 16; + string systemGivenName = 17; + string systemFamilyName = 18; bool hidden = 19; // NEXT ID: 20 } diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/storage/SignalContactRecordTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/storage/SignalContactRecordTest.java index e626377938..63a71c4723 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/storage/SignalContactRecordTest.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/storage/SignalContactRecordTest.java @@ -2,7 +2,6 @@ import org.junit.Test; import org.whispersystems.signalservice.api.push.ServiceId; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; @@ -53,6 +52,6 @@ private static SignalContactRecord.Builder contactBuilder(int key, { return new SignalContactRecord.Builder(byteArray(key), serviceId, null) .setE164(e164) - .setGivenName(givenName); + .setProfileGivenName(givenName); } } From e2a842b4409798031d3ea6c75ac6134d745e9010 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 29 Sep 2022 09:52:22 -0300 Subject: [PATCH 02/78] Fix inability to forward videos to stories. --- .../main/java/org/thoughtcrime/securesms/stories/Stories.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/Stories.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/Stories.kt index 27f757bdc0..af68ae11db 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/Stories.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/Stories.kt @@ -11,11 +11,11 @@ import com.google.android.exoplayer2.SimpleExoPlayer import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.schedulers.Schedulers import org.signal.core.util.ThreadUtil -import org.signal.core.util.isAbsent import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.contacts.HeaderAction import org.thoughtcrime.securesms.database.AttachmentDatabase +import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.DistributionListId import org.thoughtcrime.securesms.database.model.MmsMessageRecord @@ -267,7 +267,7 @@ object Stories { private fun getContentDuration(media: Media): DurationResult { return if (MediaUtil.isVideo(media.mimeType)) { - val mediaDuration = if (media.duration == 0L && media.transformProperties.isAbsent()) { + val mediaDuration = if (media.duration == 0L && media.transformProperties.map(TransformProperties::shouldSkipTransform).orElse(true)) { getVideoDuration(media.uri) } else if (media.transformProperties.map { it.isVideoTrim }.orElse(false)) { TimeUnit.MICROSECONDS.toMillis(media.transformProperties.get().videoTrimEndTimeUs - media.transformProperties.get().videoTrimStartTimeUs) From b3672273e8f4e55a2c032767719aa5ac9c8512cf Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 29 Sep 2022 09:00:50 -0400 Subject: [PATCH 03/78] Update BodyRange to use unsigned ints. --- libsignal/service/src/main/proto/SignalService.proto | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libsignal/service/src/main/proto/SignalService.proto b/libsignal/service/src/main/proto/SignalService.proto index affade218a..d11e143116 100644 --- a/libsignal/service/src/main/proto/SignalService.proto +++ b/libsignal/service/src/main/proto/SignalService.proto @@ -133,8 +133,8 @@ message DataMessage { } message BodyRange { - optional int32 start = 1; - optional int32 length = 2; + optional uint32 start = 1; + optional uint32 length = 2; oneof associatedValue { string mentionUuid = 3; From 13bd0035645bc8b282fec187b68bbd8e65a0f599 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 29 Sep 2022 09:15:50 -0400 Subject: [PATCH 04/78] Improve quote model generation. --- .../main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java index b8fdfaf52a..167df0923c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java @@ -357,6 +357,8 @@ protected Optional getQuoteFor(OutgoingMediaMess if (quoteAuthorRecipient.isMaybeRegistered()) { return Optional.of(new SignalServiceDataMessage.Quote(quoteId, RecipientUtil.getOrFetchServiceId(context, quoteAuthorRecipient), quoteBody, quoteAttachments, quoteMentions, quoteType.getDataMessageType())); + } else if (quoteAuthorRecipient.hasServiceId()) { + return Optional.of(new SignalServiceDataMessage.Quote(quoteId, quoteAuthorRecipient.requireServiceId(), quoteBody, quoteAttachments, quoteMentions, quoteType.getDataMessageType())); } else { return Optional.empty(); } From 86a345a4f3f571cf26d664807c4696a6b83a620b Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 29 Sep 2022 10:20:08 -0300 Subject: [PATCH 05/78] Add proper treatment for story pager sending state bar. --- .../viewer/page/StoryViewerPageFragment.kt | 23 +++++++++++++++---- .../layout/stories_viewer_fragment_page.xml | 23 ++++++++++++++++++- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt index 9e43a21628..35b952eba5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt @@ -111,6 +111,8 @@ class StoryViewerPageFragment : private lateinit var storyCaptionContainer: FrameLayout private lateinit var storyContentContainer: FrameLayout private lateinit var storyFirstTimeNavigationViewStub: StoryFirstNavigationStub + private lateinit var sendingBarTextView: TextView + private lateinit var sendingBar: View private lateinit var callback: Callback @@ -182,6 +184,8 @@ class StoryViewerPageFragment : progressBar = view.findViewById(R.id.progress) viewsAndReplies = view.findViewById(R.id.views_and_replies_bar) storyFirstTimeNavigationViewStub = StoryFirstNavigationStub(view.findViewById(R.id.story_first_time_nav_stub)) + sendingBarTextView = view.findViewById(R.id.sending_text_view) + sendingBar = view.findViewById(R.id.sending_bar) storySlate.callback = this storyFirstTimeNavigationViewStub.setCallback(this) @@ -913,6 +917,8 @@ class StoryViewerPageFragment : viewsAndReplies.visible = true } + sendingBar.visible = false + viewsAndReplies.isEnabled = true viewsAndReplies.iconTint = ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.signal_colorOnSurface)) when (replyState) { @@ -934,13 +940,20 @@ class StoryViewerPageFragment : indicatorColors = intArrayOf(ContextCompat.getColor(requireContext(), R.color.signal_dark_colorNeutralInverse)) trackThickness = 2.dp } - ) + ).apply { + setBounds(0, 0, 20.dp, 20.dp) + } } - viewsAndReplies.icon = sendingProgressDrawable - viewsAndReplies.iconGravity = MaterialButton.ICON_GRAVITY_TEXT_START - viewsAndReplies.iconSize = 20.dp - viewsAndReplies.setText(R.string.StoriesLandingItem__sending) + sendingBarTextView.setCompoundDrawablesRelativeWithIntrinsicBounds( + sendingProgressDrawable, + null, + null, + null + ) + + sendingBar.visible = true + viewsAndReplies.isEnabled = false } private fun presentPartialSendBottomBar() { diff --git a/app/src/main/res/layout/stories_viewer_fragment_page.xml b/app/src/main/res/layout/stories_viewer_fragment_page.xml index 0448ad2e6d..2a263b2110 100644 --- a/app/src/main/res/layout/stories_viewer_fragment_page.xml +++ b/app/src/main/res/layout/stories_viewer_fragment_page.xml @@ -84,6 +84,27 @@ tools:icon="@drawable/ic_reply_24_outline" tools:text="6 views 4 replies" /> + + + + + + Date: Thu, 29 Sep 2022 10:20:47 -0300 Subject: [PATCH 06/78] Always display the date in story viewer. --- .../stories/viewer/page/StoryViewerPageFragment.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt index 35b952eba5..8993d77d68 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt @@ -889,11 +889,7 @@ class StoryViewerPageFragment : } private fun presentDate(date: TextView, storyPost: StoryPost) { - val messageRecord = storyPost.conversationMessage.messageRecord - date.text = when { - messageRecord.isOutgoing && !messageRecord.isSent -> getString(R.string.StoriesLandingItem__sending) - else -> DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), storyPost.dateInMilliseconds) - } + date.text = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), storyPost.dateInMilliseconds) } private fun presentSenderAvatar(senderAvatar: AvatarImageView, post: StoryPost) { From f63ce79f1630fe9881c58e379af209ae2cc2ee32 Mon Sep 17 00:00:00 2001 From: Nicholas Date: Fri, 30 Sep 2022 09:42:06 -0400 Subject: [PATCH 07/78] Create new Media Preview infrastructure, behind feature flag. --- app/src/main/AndroidManifest.xml | 4 ++ .../securesms/MediaPreviewActivity.java | 51 ++++++-------- .../animation/DepthPageTransformer.java | 3 +- .../components/ThreadPhotoRailView.java | 2 +- .../conversation/ConversationItem.java | 29 ++++---- .../securesms/database/MediaDatabase.java | 2 +- .../loaders/GroupedThreadMediaLoader.java | 2 +- .../MediaOverviewPageFragment.java | 28 ++++---- .../mediapreview/MediaIntentFactory.kt | 66 +++++++++++++++++++ .../mediapreview/MediaPreviewRepository.kt | 65 ++++++++++++++++++ .../mediapreview/MediaPreviewV2Activity.kt | 28 ++++++++ .../mediapreview/MediaPreviewV2Fragment.kt | 57 ++++++++++++++++ .../mediapreview/MediaPreviewV2State.kt | 10 +++ .../mediapreview/MediaPreviewV2ViewModel.kt | 30 +++++++++ .../mediapreview/MediaPreviewViewModel.java | 6 +- .../mediapreview/PreviewMediaAdapter.kt | 39 +++++++++++ .../securesms/mms/AttachmentManager.java | 22 +++++-- .../securesms/util/FeatureFlags.java | 14 +++- .../res/layout/activity_mediapreview_v2.xml | 6 ++ .../res/layout/fragment_media_preview_v2.xml | 13 ++++ 20 files changed, 406 insertions(+), 71 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaIntentFactory.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewRepository.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Activity.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2State.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2ViewModel.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediapreview/PreviewMediaAdapter.kt create mode 100644 app/src/main/res/layout/activity_mediapreview_v2.xml create mode 100644 app/src/main/res/layout/fragment_media_preview_v2.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index eb7611d2cb..55883783d2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -686,6 +686,10 @@ android:screenOrientation="portrait" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> + + diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java index 53fcd38366..ffb3c432da 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java @@ -65,6 +65,7 @@ import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord; import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader; import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity; +import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory; import org.thoughtcrime.securesms.mediapreview.MediaPreviewFragment; import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel; import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter; @@ -98,18 +99,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity private final static String TAG = Log.tag(MediaPreviewActivity.class); - private static final int NOT_IN_A_THREAD = -2; - - public static final String THREAD_ID_EXTRA = "thread_id"; - public static final String DATE_EXTRA = "date"; - public static final String SIZE_EXTRA = "size"; - public static final String CAPTION_EXTRA = "caption"; - public static final String LEFT_IS_RECENT_EXTRA = "left_is_recent"; - public static final String HIDE_ALL_MEDIA_EXTRA = "came_from_all_media"; - public static final String SHOW_THREAD_EXTRA = "show_thread"; - public static final String SORTING_EXTRA = "sorting"; - public static final String IS_VIDEO_GIF = "is_video_gif"; - private ViewPager mediaPager; private View detailsContainer; private TextView caption; @@ -127,7 +116,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity private ViewPagerListener viewPagerListener; private int restartItem = -1; - private long threadId = NOT_IN_A_THREAD; + private long threadId = MediaIntentFactory.NOT_IN_A_THREAD; private boolean cameFromAllMedia; private boolean showThread; private MediaDatabase.Sorting sorting; @@ -143,12 +132,12 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity { DatabaseAttachment attachment = Objects.requireNonNull(mediaRecord.getAttachment()); Intent intent = new Intent(context, MediaPreviewActivity.class); - intent.putExtra(MediaPreviewActivity.THREAD_ID_EXTRA, mediaRecord.getThreadId()); - intent.putExtra(MediaPreviewActivity.DATE_EXTRA, mediaRecord.getDate()); - intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, attachment.getSize()); - intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, attachment.getCaption()); - intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, leftIsRecent); - intent.putExtra(MediaPreviewActivity.IS_VIDEO_GIF, attachment.isVideoGif()); + intent.putExtra(MediaIntentFactory.THREAD_ID_EXTRA, mediaRecord.getThreadId()); + intent.putExtra(MediaIntentFactory.DATE_EXTRA, mediaRecord.getDate()); + intent.putExtra(MediaIntentFactory.SIZE_EXTRA, attachment.getSize()); + intent.putExtra(MediaIntentFactory.CAPTION_EXTRA, attachment.getCaption()); + intent.putExtra(MediaIntentFactory.LEFT_IS_RECENT_EXTRA, leftIsRecent); + intent.putExtra(MediaIntentFactory.IS_VIDEO_GIF, attachment.isVideoGif()); intent.setDataAndType(attachment.getUri(), mediaRecord.getContentType()); return intent; } @@ -305,17 +294,17 @@ private void initializeViews() { private void initializeResources() { Intent intent = getIntent(); - threadId = intent.getLongExtra(THREAD_ID_EXTRA, NOT_IN_A_THREAD); - cameFromAllMedia = intent.getBooleanExtra(HIDE_ALL_MEDIA_EXTRA, false); - showThread = intent.getBooleanExtra(SHOW_THREAD_EXTRA, false); - sorting = MediaDatabase.Sorting.values()[intent.getIntExtra(SORTING_EXTRA, 0)]; + threadId = intent.getLongExtra(MediaIntentFactory.THREAD_ID_EXTRA, MediaIntentFactory.NOT_IN_A_THREAD); + cameFromAllMedia = intent.getBooleanExtra(MediaIntentFactory.HIDE_ALL_MEDIA_EXTRA, false); + showThread = intent.getBooleanExtra(MediaIntentFactory.SHOW_THREAD_EXTRA, false); + sorting = MediaDatabase.Sorting.values()[intent.getIntExtra(MediaIntentFactory.SORTING_EXTRA, 0)]; initialMediaUri = intent.getData(); initialMediaType = intent.getType(); - initialMediaSize = intent.getLongExtra(SIZE_EXTRA, 0); - initialCaption = intent.getStringExtra(CAPTION_EXTRA); - leftIsRecent = intent.getBooleanExtra(LEFT_IS_RECENT_EXTRA, false); - initialMediaIsVideoGif = intent.getBooleanExtra(IS_VIDEO_GIF, false); + initialMediaSize = intent.getLongExtra(MediaIntentFactory.SIZE_EXTRA, 0); + initialCaption = intent.getStringExtra(MediaIntentFactory.CAPTION_EXTRA); + leftIsRecent = intent.getBooleanExtra(MediaIntentFactory.LEFT_IS_RECENT_EXTRA, false); + initialMediaIsVideoGif = intent.getBooleanExtra(MediaIntentFactory.IS_VIDEO_GIF, false); restartItem = -1; } @@ -533,7 +522,7 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) { } private boolean isMediaInDb() { - return threadId != NOT_IN_A_THREAD; + return threadId != MediaIntentFactory.NOT_IN_A_THREAD; } private @Nullable MediaItem getCurrentMediaItem() { @@ -789,7 +778,7 @@ public Fragment getItem(int position) { cursor.moveToPosition(cursorPosition); - MediaDatabase.MediaRecord mediaRecord = MediaDatabase.MediaRecord.from(context, cursor); + MediaDatabase.MediaRecord mediaRecord = MediaDatabase.MediaRecord.from(cursor); DatabaseAttachment attachment = Objects.requireNonNull(mediaRecord.getAttachment()); MediaPreviewFragment fragment = MediaPreviewFragment.newInstance(attachment, autoPlay); @@ -819,7 +808,7 @@ public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Obj cursor.moveToPosition(cursorPosition); - MediaRecord mediaRecord = MediaRecord.from(context, cursor); + MediaRecord mediaRecord = MediaRecord.from(cursor); DatabaseAttachment attachment = Objects.requireNonNull(mediaRecord.getAttachment()); RecipientId recipientId = mediaRecord.getRecipientId(); RecipientId threadRecipientId = mediaRecord.getThreadRecipientId(); @@ -890,4 +879,4 @@ interface MediaItemAdapter { @Nullable View getPlaybackControls(int position); boolean hasFragmentFor(int position); } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/animation/DepthPageTransformer.java b/app/src/main/java/org/thoughtcrime/securesms/animation/DepthPageTransformer.java index da935281b3..85ca770bfe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/animation/DepthPageTransformer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/animation/DepthPageTransformer.java @@ -4,11 +4,12 @@ import androidx.annotation.NonNull; import androidx.viewpager.widget.ViewPager; +import androidx.viewpager2.widget.ViewPager2; /** * Based on https://developer.android.com/training/animation/screen-slide#depth-page */ -public final class DepthPageTransformer implements ViewPager.PageTransformer { +public final class DepthPageTransformer implements ViewPager.PageTransformer, ViewPager2.PageTransformer { private static final float MIN_SCALE = 0.75f; public void transformPage(@NonNull View view, float position) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ThreadPhotoRailView.java b/app/src/main/java/org/thoughtcrime/securesms/components/ThreadPhotoRailView.java index 05d1b56b53..4c09af37ef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ThreadPhotoRailView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ThreadPhotoRailView.java @@ -89,7 +89,7 @@ public ThreadPhotoViewHolder onCreateItemViewHolder(ViewGroup parent, int viewTy @Override public void onBindItemViewHolder(ThreadPhotoViewHolder viewHolder, @NonNull Cursor cursor) { ThumbnailView imageView = viewHolder.imageView; - MediaDatabase.MediaRecord mediaRecord = MediaDatabase.MediaRecord.from(getContext(), cursor); + MediaDatabase.MediaRecord mediaRecord = MediaDatabase.MediaRecord.from(cursor); Slide slide = MediaUtil.getSlideForAttachment(getContext(), mediaRecord.getAttachment()); if (slide != null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java index 66e2dd01fb..7272b35f65 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -18,6 +18,7 @@ import android.animation.ValueAnimator; import android.annotation.SuppressLint; +import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; @@ -96,6 +97,7 @@ import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectCollection; import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart; import org.thoughtcrime.securesms.database.AttachmentDatabase; +import org.thoughtcrime.securesms.database.MediaDatabase; import org.thoughtcrime.securesms.database.MessageDatabase; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; @@ -111,6 +113,8 @@ import org.thoughtcrime.securesms.jobs.SmsSendJob; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.linkpreview.LinkPreview; +import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory; +import org.thoughtcrime.securesms.mediapreview.MediaPreviewV2Activity; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.ImageSlide; import org.thoughtcrime.securesms.mms.PartAuthority; @@ -126,6 +130,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.revealable.ViewOnceMessageView; import org.thoughtcrime.securesms.util.DateUtils; +import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.InterceptableLongClickCopyLinkSpan; import org.thoughtcrime.securesms.util.LinkUtil; import org.thoughtcrime.securesms.util.LongClickMovementMethod; @@ -2312,17 +2317,19 @@ public void onClick(final View v, final Slide slide) { } else if (!canPlayContent && mediaItem != null && eventListener != null) { eventListener.onPlayInlineContent(conversationMessage); } else if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) { - Intent intent = new Intent(context, MediaPreviewActivity.class); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - intent.setDataAndType(slide.getUri(), slide.getContentType()); - intent.putExtra(MediaPreviewActivity.THREAD_ID_EXTRA, messageRecord.getThreadId()); - intent.putExtra(MediaPreviewActivity.DATE_EXTRA, messageRecord.getTimestamp()); - intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, slide.asAttachment().getSize()); - intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, slide.getCaption().orElse(null)); - intent.putExtra(MediaPreviewActivity.IS_VIDEO_GIF, slide.isVideoGif()); - intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, false); - - context.startActivity(intent); + MediaIntentFactory.MediaPreviewArgs args = new MediaIntentFactory.MediaPreviewArgs( + messageRecord.getThreadId(), + messageRecord.getTimestamp(), + slide.getUri(), + slide.getContentType(), + slide.asAttachment().getSize(), + slide.getCaption().orElse(null), + false, + false, + false, + MediaDatabase.Sorting.Newest.ordinal(), + slide.isVideoGif()); + context.startActivity(MediaIntentFactory.create(context, args)); } else if (slide.getUri() != null) { Log.i(TAG, "Clicked: " + slide.getUri() + " , " + slide.getContentType()); Uri publicUri = PartAuthority.getAttachmentPublicUri(slide.getUri()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java index 7a4fd14633..ea5313c3b6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java @@ -192,7 +192,7 @@ private MediaRecord(@Nullable DatabaseAttachment attachment, this.outgoing = outgoing; } - public static MediaRecord from(@NonNull Context context, @NonNull Cursor cursor) { + public static MediaRecord from(@NonNull Cursor cursor) { AttachmentDatabase attachmentDatabase = SignalDatabase.attachments(); List attachments = attachmentDatabase.getAttachments(cursor); RecipientId recipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.RECIPIENT_ID))); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/GroupedThreadMediaLoader.java b/app/src/main/java/org/thoughtcrime/securesms/database/loaders/GroupedThreadMediaLoader.java index 26da50d088..8b2169ea81 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/GroupedThreadMediaLoader.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/loaders/GroupedThreadMediaLoader.java @@ -75,7 +75,7 @@ public GroupedThreadMedia loadInBackground() { try (Cursor cursor = ThreadMediaLoader.createThreadMediaCursor(context, threadId, mediaType, sorting)) { while (cursor != null && cursor.moveToNext()) { - mediaGrouping.add(MediaDatabase.MediaRecord.from(context, cursor)); + mediaGrouping.add(MediaDatabase.MediaRecord.from(cursor)); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaOverviewPageFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaOverviewPageFragment.java index b1f5a5d8c5..ebc41f7b0e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaOverviewPageFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaOverviewPageFragment.java @@ -28,7 +28,6 @@ import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager; import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.MediaPreviewActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.components.menu.ActionItem; @@ -38,6 +37,7 @@ import org.thoughtcrime.securesms.database.MediaDatabase; import org.thoughtcrime.securesms.database.loaders.GroupedThreadMediaLoader; import org.thoughtcrime.securesms.database.loaders.MediaLoader; +import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.util.BottomOffsetDecoration; @@ -46,6 +46,7 @@ import org.thoughtcrime.securesms.util.ViewUtil; import java.util.Arrays; +import java.util.Objects; public final class MediaOverviewPageFragment extends Fragment implements MediaGalleryAllAdapter.ItemClickListener, @@ -236,18 +237,19 @@ private void handleMediaPreviewClick(@NonNull MediaDatabase.MediaRecord mediaRec DatabaseAttachment attachment = mediaRecord.getAttachment(); if (MediaUtil.isVideo(attachment) || MediaUtil.isImage(attachment)) { - Intent intent = new Intent(context, MediaPreviewActivity.class); - intent.putExtra(MediaPreviewActivity.DATE_EXTRA, mediaRecord.getDate()); - intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, mediaRecord.getAttachment().getSize()); - intent.putExtra(MediaPreviewActivity.THREAD_ID_EXTRA, threadId); - intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, true); - intent.putExtra(MediaPreviewActivity.HIDE_ALL_MEDIA_EXTRA, true); - intent.putExtra(MediaPreviewActivity.SHOW_THREAD_EXTRA, threadId == MediaDatabase.ALL_THREADS); - intent.putExtra(MediaPreviewActivity.SORTING_EXTRA, sorting.ordinal()); - intent.putExtra(MediaPreviewActivity.IS_VIDEO_GIF, attachment.isVideoGif()); - - intent.setDataAndType(mediaRecord.getAttachment().getUri(), mediaRecord.getContentType()); - context.startActivity(intent); + MediaIntentFactory.MediaPreviewArgs args = new MediaIntentFactory.MediaPreviewArgs( + threadId, + mediaRecord.getDate(), + Objects.requireNonNull(mediaRecord.getAttachment().getUri()), + mediaRecord.getContentType(), + mediaRecord.getAttachment().getSize(), + mediaRecord.getAttachment().getCaption(), + true, + true, + threadId == MediaDatabase.ALL_THREADS, + sorting.ordinal(), + attachment.isVideoGif()); + context.startActivity(MediaIntentFactory.create(context, args)); } else { if (!MediaUtil.isAudio(attachment)) { showFileExternally(context, mediaRecord); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaIntentFactory.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaIntentFactory.kt new file mode 100644 index 0000000000..326fa61702 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaIntentFactory.kt @@ -0,0 +1,66 @@ +package org.thoughtcrime.securesms.mediapreview + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import org.thoughtcrime.securesms.MediaPreviewActivity +import org.thoughtcrime.securesms.util.FeatureFlags + +object MediaIntentFactory { + private const val ARGS_KEY = "args" + + const val NOT_IN_A_THREAD = -2 + const val UNKNOWN_TIMESTAMP = -2 + const val THREAD_ID_EXTRA = "thread_id" + const val DATE_EXTRA = "date" + const val SIZE_EXTRA = "size" + const val CAPTION_EXTRA = "caption" + const val LEFT_IS_RECENT_EXTRA = "left_is_recent" + const val HIDE_ALL_MEDIA_EXTRA = "came_from_all_media" + const val SHOW_THREAD_EXTRA = "show_thread" + const val SORTING_EXTRA = "sorting" + const val IS_VIDEO_GIF = "is_video_gif" + + @Parcelize + data class MediaPreviewArgs( + val threadId: Long, + val date: Long, + val initialMediaUri: Uri, + val initialMediaType: String, + val initialMediaSize: Long, + val initialCaption: String? = null, + val leftIsRecent: Boolean = false, + val hideAllMedia: Boolean = false, + val showThread: Boolean= false, + val sorting: Int, + val isVideoGif: Boolean + ) : Parcelable + + @JvmStatic + fun requireArguments(bundle: Bundle): MediaPreviewArgs = bundle.getParcelable(ARGS_KEY)!! + + @JvmStatic + fun create(context: Context, args: MediaPreviewArgs): Intent { + + return if (FeatureFlags.mediaPreviewV2()) { + val intent = Intent(context, MediaPreviewV2Activity::class.java) + intent.putExtra(ARGS_KEY, args) + return intent + } else { + val intent = Intent(context, MediaPreviewActivity::class.java).apply { + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + setDataAndType(args.initialMediaUri, args.initialMediaType) + putExtra(THREAD_ID_EXTRA, args.threadId) + putExtra(DATE_EXTRA, args.date) + putExtra(SIZE_EXTRA, args.initialMediaSize) + putExtra(CAPTION_EXTRA, args.initialCaption) + putExtra(IS_VIDEO_GIF, args.isVideoGif) + putExtra(LEFT_IS_RECENT_EXTRA, args.leftIsRecent) + } + return intent + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewRepository.kt new file mode 100644 index 0000000000..ee0c94b4d5 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewRepository.kt @@ -0,0 +1,65 @@ +package org.thoughtcrime.securesms.mediapreview + +import android.net.Uri +import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.schedulers.Schedulers +import org.signal.core.util.logging.Log +import org.signal.core.util.requireLong +import org.thoughtcrime.securesms.attachments.AttachmentId +import org.thoughtcrime.securesms.attachments.DatabaseAttachment +import org.thoughtcrime.securesms.database.AttachmentDatabase +import org.thoughtcrime.securesms.database.MediaDatabase +import org.thoughtcrime.securesms.database.MediaDatabase.Sorting +import org.thoughtcrime.securesms.database.SignalDatabase.Companion.media +import org.thoughtcrime.securesms.mms.PartAuthority + +/** + * Repository for accessing the attachments in the encrypted database. + */ +class MediaPreviewRepository { + companion object { + private val TAG: String = Log.tag(MediaPreviewRepository::class.java) + } + + /** + * Accessor for database attachments. + * @param startingUri the initial position to select from + * @param threadId the thread to select from + * @param sorting the ordering of the results + * @param limit the maximum quantity of the results + */ + fun getAttachments(startingUri: Uri, threadId: Long, sorting: Sorting, limit: Int = 500): Flowable> { + return Single.fromCallable { + val cursor = media.getGalleryMediaForThread(threadId, sorting) + + val acc = mutableListOf() + var attachmentUri: Uri? = null + while (cursor.moveToNext()) { + val attachmentId = AttachmentId(cursor.requireLong(AttachmentDatabase.ROW_ID), cursor.requireLong(AttachmentDatabase.UNIQUE_ID)) + attachmentUri = PartAuthority.getAttachmentDataUri(attachmentId) + if (attachmentUri == startingUri) { + break + } + } + + if (attachmentUri == startingUri) { + for (i in 0..limit) { + val element = MediaDatabase.MediaRecord.from(cursor).attachment + if (element != null) { + acc.add(element) + } + if (!cursor.isLast) { + cursor.moveToNext() + } else { + break + } + } + acc.toList() + } else { + Log.e(TAG, "Could not find $startingUri in thread $threadId") + emptyList() + } + }.subscribeOn(Schedulers.io()).toFlowable() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Activity.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Activity.kt new file mode 100644 index 0000000000..3735f8fc00 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Activity.kt @@ -0,0 +1,28 @@ +package org.thoughtcrime.securesms.mediapreview + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.commit +import org.thoughtcrime.securesms.R + +class MediaPreviewV2Activity : AppCompatActivity(R.layout.activity_mediapreview_v2) { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (savedInstanceState == null) { + val bundle = Bundle() + val args = MediaIntentFactory.requireArguments(intent.extras!!) + bundle.putParcelable(MediaPreviewV2Fragment.ARGS_KEY, args) + supportFragmentManager.commit { + setReorderingAllowed(true) + add(R.id.fragment_container_view, MediaPreviewV2Fragment::class.java, bundle, FRAGMENT_TAG) + } + } + } + + companion object { + private const val FRAGMENT_TAG = "media_preview_fragment_v2" + private const val NOT_IN_A_THREAD = -2 + + const val THREAD_ID_EXTRA = "thread_id" + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt new file mode 100644 index 0000000000..3a22843c4c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt @@ -0,0 +1,57 @@ +package org.thoughtcrime.securesms.mediapreview + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.animation.DepthPageTransformer +import org.thoughtcrime.securesms.components.ViewBinderDelegate +import org.thoughtcrime.securesms.database.MediaDatabase +import org.thoughtcrime.securesms.databinding.FragmentMediaPreviewV2Binding +import org.thoughtcrime.securesms.util.LifecycleDisposable + +class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), MediaPreviewFragment.Events { + private val TAG = Log.tag(MediaPreviewV2Fragment::class.java) + + private val lifecycleDisposable = LifecycleDisposable() + private val binding by ViewBinderDelegate(FragmentMediaPreviewV2Binding::bind) + private val viewModel: MediaPreviewV2ViewModel by viewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.mediaPager.offscreenPageLimit = 1 + binding.mediaPager.setPageTransformer(DepthPageTransformer()) + lifecycleDisposable += viewModel.state.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread()).subscribe { + if (it.loadState == MediaPreviewV2State.LoadState.READY) { + binding.mediaPager.adapter = PreviewMediaAdapter(this, it.attachments) + } + } + initializeViewModel() + } + + private fun initializeViewModel() { + val args = MediaIntentFactory.requireArguments(requireArguments()) + val sorting = MediaDatabase.Sorting.values()[args.sorting] + viewModel.fetchAttachments(args.initialMediaUri, args.threadId, sorting) + } + + override fun singleTapOnMedia(): Boolean { + Log.d(TAG, "singleTapOnMedia()") + return true + } + + override fun mediaNotAvailable() { + Log.d(TAG, "mediaNotAvailable()") + } + + override fun onMediaReady() { + Log.d(TAG, "onMediaReady()") + } + + companion object { + val ARGS_KEY: String = "args" + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2State.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2State.kt new file mode 100644 index 0000000000..ef873fc32f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2State.kt @@ -0,0 +1,10 @@ +package org.thoughtcrime.securesms.mediapreview + +import org.thoughtcrime.securesms.attachments.Attachment + +data class MediaPreviewV2State( + val attachments: List = emptyList(), + val loadState: LoadState = LoadState.INIT +) { + enum class LoadState { INIT, READY, } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2ViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2ViewModel.kt new file mode 100644 index 0000000000..6de2873d91 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2ViewModel.kt @@ -0,0 +1,30 @@ +package org.thoughtcrime.securesms.mediapreview + +import android.net.Uri +import androidx.lifecycle.ViewModel +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.kotlin.plusAssign +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.database.MediaDatabase +import org.thoughtcrime.securesms.util.rx.RxStore + +class MediaPreviewV2ViewModel : ViewModel() { + private val TAG = Log.tag(MediaPreviewV2ViewModel::class.java) + private val store = RxStore(MediaPreviewV2State()) + private val disposables = CompositeDisposable() + private val repository: MediaPreviewRepository = MediaPreviewRepository() + + val state: Flowable = store.stateFlowable.observeOn(AndroidSchedulers.mainThread()) + + fun fetchAttachments(startingUri: Uri, threadId: Long, sorting: MediaDatabase.Sorting) { + disposables += store.update(repository.getAttachments(startingUri, threadId, sorting)) { attachments, oldState -> + oldState.copy(attachments = attachments, loadState = MediaPreviewV2State.LoadState.READY) + } + } + + override fun onCleared() { + disposables.dispose() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java index 2ec38b0003..70a9863658 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java @@ -47,14 +47,14 @@ public void setActiveAlbumRailItem(@NonNull Context context, int activePosition) cursor.moveToPosition(activePosition); - MediaRecord activeRecord = MediaRecord.from(context, cursor); + MediaRecord activeRecord = MediaRecord.from(cursor); LinkedList rail = new LinkedList<>(); Media activeMedia = toMedia(activeRecord); if (activeMedia != null) rail.add(activeMedia); while (cursor.moveToPrevious()) { - MediaRecord record = MediaRecord.from(context, cursor); + MediaRecord record = MediaRecord.from(cursor); if (record.getAttachment().getMmsId() == activeRecord.getAttachment().getMmsId()) { Media media = toMedia(record); if (media != null) rail.addFirst(media); @@ -66,7 +66,7 @@ public void setActiveAlbumRailItem(@NonNull Context context, int activePosition) cursor.moveToPosition(activePosition); while (cursor.moveToNext()) { - MediaRecord record = MediaRecord.from(context, cursor); + MediaRecord record = MediaRecord.from(cursor); if (record.getAttachment().getMmsId() == activeRecord.getAttachment().getMmsId()) { Media media = toMedia(record); if (media != null) rail.addLast(media); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/PreviewMediaAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/PreviewMediaAdapter.kt new file mode 100644 index 0000000000..781a461538 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/PreviewMediaAdapter.kt @@ -0,0 +1,39 @@ +package org.thoughtcrime.securesms.mediapreview + +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import androidx.viewpager2.adapter.FragmentStateAdapter +import org.thoughtcrime.securesms.attachments.Attachment +import org.thoughtcrime.securesms.util.MediaUtil + +class PreviewMediaAdapter(val fragment: Fragment, val items: List) : FragmentStateAdapter(fragment) { + var autoPlayPosition = -1 + + override fun getItemCount(): Int { + return items.count() + } + + override fun createFragment(position: Int): Fragment { + val attachment: Attachment = items[position] + + val contentType = attachment.contentType + val args = bundleOf( + MediaPreviewFragment.DATA_URI to attachment.uri, + MediaPreviewFragment.DATA_CONTENT_TYPE to contentType, + MediaPreviewFragment.DATA_SIZE to attachment.size, + MediaPreviewFragment.AUTO_PLAY to (position == autoPlayPosition), + MediaPreviewFragment.VIDEO_GIF to attachment.isVideoGif, + ) + val fragment = if (MediaUtil.isVideo(contentType)) { + VideoMediaPreviewFragment() + } else if (MediaUtil.isImageType(contentType)) { + ImageMediaPreviewFragment() + } else { + throw AssertionError("Unexpected media type: $contentType") + } + + fragment.arguments = args + + return fragment + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java b/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java index 61caa9e94f..2e7adbea06 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java @@ -52,8 +52,10 @@ import org.thoughtcrime.securesms.components.location.SignalPlace; import org.thoughtcrime.securesms.conversation.MessageSendType; import org.thoughtcrime.securesms.database.AttachmentDatabase; +import org.thoughtcrime.securesms.database.MediaDatabase; import org.thoughtcrime.securesms.giph.ui.GiphyActivity; import org.thoughtcrime.securesms.maps.PlacePickerActivity; +import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory; import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity; import org.thoughtcrime.securesms.payments.create.CreatePaymentFragmentArgs; import org.thoughtcrime.securesms.payments.preferences.PaymentsActivity; @@ -469,13 +471,19 @@ private boolean areConstraintsSatisfied(final @NonNull Context context, private void previewImageDraft(final @NonNull Slide slide) { if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) { - Intent intent = new Intent(context, MediaPreviewActivity.class); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, slide.asAttachment().getSize()); - intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, slide.getCaption().orElse(null)); - intent.setDataAndType(slide.getUri(), slide.getContentType()); - - context.startActivity(intent); + MediaIntentFactory.MediaPreviewArgs args = new MediaIntentFactory.MediaPreviewArgs( + MediaIntentFactory.NOT_IN_A_THREAD, + MediaIntentFactory.UNKNOWN_TIMESTAMP, + slide.getUri(), + slide.getContentType(), + slide.asAttachment().getSize(), + slide.getCaption().orElse(null), + false, + false, + false, + MediaDatabase.Sorting.Newest.ordinal(), + slide.isVideoGif()); + context.startActivity(MediaIntentFactory.create(context, args)); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index 107b917aed..431e127e07 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -103,6 +103,7 @@ public final class FeatureFlags { private static final String CDS_V2_COMPAT = "android.cdsV2Compat.4"; public static final String STORIES_LOCALE = "android.stories.locale"; private static final String HIDE_CONTACTS = "android.hide.contacts"; + public static final String MEDIA_PREVIEW_V2 = "android.mediaPreviewV2"; /** * We will only store remote values for flags in this set. If you want a flag to be controllable @@ -157,7 +158,8 @@ public final class FeatureFlags { SMS_EXPORTER, CDS_V2_COMPAT, STORIES_LOCALE, - HIDE_CONTACTS + HIDE_CONTACTS, + MEDIA_PREVIEW_V2 ); @VisibleForTesting @@ -220,7 +222,8 @@ public final class FeatureFlags { RECIPIENT_MERGE_V2, CDS_V2_LOAD_TEST, CDS_V2_COMPAT, - STORIES + STORIES, + MEDIA_PREVIEW_V2 ); /** @@ -565,6 +568,13 @@ public static boolean hideContacts() { return getBoolean(HIDE_CONTACTS, false); } + /** + * Whether or not we should use the new media preview fragment implementation. + */ + public static boolean mediaPreviewV2() { + return getBoolean(MEDIA_PREVIEW_V2, false); + } + /** Only for rendering debug info. */ public static synchronized @NonNull Map getMemoryValues() { return new TreeMap<>(REMOTE_VALUES); diff --git a/app/src/main/res/layout/activity_mediapreview_v2.xml b/app/src/main/res/layout/activity_mediapreview_v2.xml new file mode 100644 index 0000000000..b6ed0ae4a4 --- /dev/null +++ b/app/src/main/res/layout/activity_mediapreview_v2.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_media_preview_v2.xml b/app/src/main/res/layout/fragment_media_preview_v2.xml new file mode 100644 index 0000000000..02f8c6defa --- /dev/null +++ b/app/src/main/res/layout/fragment_media_preview_v2.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file From afe36b982f8780c73a06a4915ee76853f47021a9 Mon Sep 17 00:00:00 2001 From: Varsha Date: Thu, 29 Sep 2022 17:13:10 -0700 Subject: [PATCH 08/78] Prompt to setup payment bioauth, require to disable payment lock. --- .../app/privacy/PrivacySettingsFragment.kt | 53 ++++++++- .../app/privacy/PrivacySettingsViewModel.kt | 4 +- .../securesms/keyvalue/PaymentsValues.kt | 1 + .../mediapreview/MediaIntentFactory.kt | 2 +- .../backup/PaymentsRecoveryStartFragment.java | 2 +- .../confirm/ConfirmPaymentFragment.java | 2 +- .../preferences/PaymentsHomeFragment.java | 3 + .../preferences/PaymentsHomeViewModel.java | 1 + .../PaymentsSecuritySetupFragment.kt | 46 ++++++++ .../ic_payment_security_setup_lock.xml | 16 +++ .../payments_security_setup_fragment.xml | 104 ++++++++++++++++++ .../res/navigation/payments_preferences.xml | 30 +++++ app/src/main/res/values/strings.xml | 20 ++++ 13 files changed, 278 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/payments/securitysetup/PaymentsSecuritySetupFragment.kt create mode 100644 app/src/main/res/drawable/ic_payment_security_setup_lock.xml create mode 100644 app/src/main/res/layout/payments_security_setup_fragment.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsFragment.kt index 2e4edb843c..b9042a5902 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsFragment.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.DialogInterface import android.content.Intent import android.os.Build +import android.os.Bundle import android.provider.Settings import android.text.SpannableStringBuilder import android.text.Spanned @@ -13,7 +14,11 @@ import android.view.View import android.view.WindowManager import android.widget.TextView import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher import androidx.annotation.StringRes +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricPrompt +import androidx.biometric.BiometricPrompt.PromptInfo import androidx.core.content.ContextCompat import androidx.lifecycle.ViewModelProvider import androidx.navigation.Navigation @@ -25,6 +30,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import mobi.upod.timedurationpicker.TimeDurationPicker import mobi.upod.timedurationpicker.TimeDurationPickerDialog import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.BiometricDeviceAuthentication +import org.thoughtcrime.securesms.BiometricDeviceLockContract import org.thoughtcrime.securesms.PassphraseChangeActivity import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.settings.ClickPreference @@ -59,6 +66,8 @@ private val TAG = Log.tag(PrivacySettingsFragment::class.java) class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privacy) { private lateinit var viewModel: PrivacySettingsViewModel + private lateinit var biometricAuth: BiometricDeviceAuthentication + private lateinit var biometricDeviceLockLauncher: ActivityResultLauncher private val incognitoSummary: CharSequence by lazy { SpannableStringBuilder(getString(R.string.preferences__this_setting_is_not_a_guarantee)) @@ -70,11 +79,35 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac ) } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + biometricDeviceLockLauncher = registerForActivityResult(BiometricDeviceLockContract()) { result: Int -> + if (result == BiometricDeviceAuthentication.AUTHENTICATED) { + viewModel.togglePaymentLock(false) + } + } + val promptInfo = PromptInfo.Builder() + .setAllowedAuthenticators(BiometricDeviceAuthentication.ALLOWED_AUTHENTICATORS) + .setTitle(requireContext().getString(R.string.BiometricDeviceAuthentication__signal)) + .setConfirmationRequired(false) + .build() + biometricAuth = BiometricDeviceAuthentication( + BiometricManager.from(requireActivity()), + BiometricPrompt(requireActivity(), BiometricAuthenticationListener()), + promptInfo + ) + } + override fun onResume() { super.onResume() viewModel.refreshBlockedCount() } + override fun onPause() { + super.onPause() + biometricAuth.cancelAuthentication() + } + override fun bindAdapter(adapter: MappingAdapter) { adapter.registerFactory(ValueClickPreference::class.java, LayoutFactory(::ValueClickPreferenceViewHolder, R.layout.value_click_preference_item)) @@ -323,8 +356,10 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac onClick = { if (!ServiceUtil.getKeyguardManager(requireContext()).isKeyguardSecure) { showGoToPhoneSettings() + } else if (state.paymentLock) { + biometricAuth.authenticate(requireContext(), true) { biometricDeviceLockLauncher?.launch(getString(R.string.BiometricDeviceAuthentication__signal)) } } else { - viewModel.togglePaymentLock() + viewModel.togglePaymentLock(true) } } ) @@ -512,4 +547,20 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac valueText.text = model.value.resolve(context) } } + + inner class BiometricAuthenticationListener : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError(errorCode: Int, errorString: CharSequence) { + Log.w(TAG, "Authentication error: $errorCode") + onAuthenticationFailed() + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + Log.i(TAG, "onAuthenticationSucceeded") + viewModel.togglePaymentLock(false) + } + + override fun onAuthenticationFailed() { + Log.w(TAG, "Unable to authenticate payment lock") + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsViewModel.kt index 6d2a2e7c9a..1a897c3bed 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsViewModel.kt @@ -74,8 +74,8 @@ class PrivacySettingsViewModel( refresh() } - fun togglePaymentLock() { - SignalStore.paymentsValues().paymentLock = state.value?.let { !it.paymentLock } ?: false + fun togglePaymentLock(enable: Boolean) { + SignalStore.paymentsValues().paymentLock = enable refresh() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PaymentsValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PaymentsValues.kt index 6fa68d7db5..fd5a328da4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PaymentsValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PaymentsValues.kt @@ -55,6 +55,7 @@ internal class PaymentsValues internal constructor(store: KeyValueStore) : Signa const val MOB_PAYMENTS_ENABLED = "mob_payments_enabled" } + @get:JvmName("isPaymentLockEnabled") var paymentLock: Boolean by booleanValue(PAYMENT_LOCK_ENABLED, false) var paymentLockTimestamp: Long by longValue(PAYMENT_LOCK_TIMESTAMP, 0) var paymentLockSkipCount: Int by integerValue(PAYMENT_LOCK_SKIP_COUNT, 0) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaIntentFactory.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaIntentFactory.kt index 326fa61702..1ecff88560 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaIntentFactory.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaIntentFactory.kt @@ -34,7 +34,7 @@ object MediaIntentFactory { val initialCaption: String? = null, val leftIsRecent: Boolean = false, val hideAllMedia: Boolean = false, - val showThread: Boolean= false, + val showThread: Boolean = false, val sorting: Int, val isVideoGif: Boolean ) : Parcelable diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/backup/PaymentsRecoveryStartFragment.java b/app/src/main/java/org/thoughtcrime/securesms/payments/backup/PaymentsRecoveryStartFragment.java index 52783129b4..974766248f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/backup/PaymentsRecoveryStartFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/backup/PaymentsRecoveryStartFragment.java @@ -65,7 +65,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat message.setText(getDescription(state)); message.setLink(getString(R.string.PaymentsRecoveryStartFragment__learn_more__view)); startButton.setOnClickListener(v -> { - if (state == RecoveryPhraseStates.FROM_PAYMENTS_MENU_WITH_MNEMONIC_CONFIRMED && ServiceUtil.getKeyguardManager(requireContext()).isKeyguardSecure() && SignalStore.paymentsValues().getPaymentLock()) { + if (state == RecoveryPhraseStates.FROM_PAYMENTS_MENU_WITH_MNEMONIC_CONFIRMED && ServiceUtil.getKeyguardManager(requireContext()).isKeyguardSecure() && SignalStore.paymentsValues().isPaymentLockEnabled()) { BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo .Builder() .setAllowedAuthenticators(BiometricDeviceAuthentication.ALLOWED_AUTHENTICATORS) diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/confirm/ConfirmPaymentFragment.java b/app/src/main/java/org/thoughtcrime/securesms/payments/confirm/ConfirmPaymentFragment.java index bac3bcf42b..2b9acdc058 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/confirm/ConfirmPaymentFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/confirm/ConfirmPaymentFragment.java @@ -211,7 +211,7 @@ private static CharSequence mono(Context context, CharSequence address) { private boolean isPaymentLockEnabled(Context context) { - return SignalStore.paymentsValues().getPaymentLock() && ServiceUtil.getKeyguardManager(context).isKeyguardSecure(); + return SignalStore.paymentsValues().isPaymentLockEnabled() && ServiceUtil.getKeyguardManager(context).isKeyguardSecure(); } private class Callbacks implements ConfirmPaymentAdapter.Callbacks { diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeFragment.java b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeFragment.java index c7788ce644..22e162d937 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeFragment.java @@ -222,6 +222,9 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat }); break; case ACTIVATED: + if (!SignalStore.paymentsValues().isPaymentLockEnabled()) { + SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), R.id.action_paymentsHome_to_securitySetup); + } return; default: throw new IllegalStateException("Unsupported event type: " + paymentStateEvent.name()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeViewModel.java index 7cbde5424f..66bc21d477 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeViewModel.java @@ -203,6 +203,7 @@ public void activatePayments() { @Override public void onComplete(@Nullable Void result) { store.update(state -> state.updatePaymentsEnabled(PaymentsHomeState.PaymentsState.ACTIVATED)); + paymentStateEvents.postValue(PaymentStateEvent.ACTIVATED); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/securitysetup/PaymentsSecuritySetupFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/payments/securitysetup/PaymentsSecuritySetupFragment.kt new file mode 100644 index 0000000000..54f0696449 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/securitysetup/PaymentsSecuritySetupFragment.kt @@ -0,0 +1,46 @@ +package org.thoughtcrime.securesms.payments.securitysetup + +import android.os.Bundle +import android.view.View +import androidx.activity.OnBackPressedCallback +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.databinding.PaymentsSecuritySetupFragmentBinding +import org.thoughtcrime.securesms.payments.preferences.PaymentsHomeFragmentDirections +import org.thoughtcrime.securesms.util.navigation.safeNavigate + +/** + * Fragment to let user know to enable payment lock to protect their funds + */ +class PaymentsSecuritySetupFragment : Fragment(R.layout.payments_security_setup_fragment) { + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val binding = PaymentsSecuritySetupFragmentBinding.bind(view) + + requireActivity().onBackPressedDispatcher.addCallback( + viewLifecycleOwner, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + showSkipDialog() + } + } + ) + binding.paymentsSecuritySetupEnableLock.setOnClickListener { + findNavController().safeNavigate(PaymentsHomeFragmentDirections.actionPaymentsHomeToPrivacySettings(true)) + } + binding.paymentsSecuritySetupFragmentNotNow.setOnClickListener { showSkipDialog() } + binding.toolbar.setNavigationOnClickListener { showSkipDialog() } + } + + private fun showSkipDialog() { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.PaymentsSecuritySetupFragment__skip_this_step)) + .setMessage(getString(R.string.PaymentsSecuritySetupFragment__skipping_this_step)) + .setPositiveButton(R.string.PaymentsSecuritySetupFragment__skip) { _, _ -> findNavController().popBackStack() } + .setNegativeButton(R.string.PaymentsSecuritySetupFragment__cancel) { _, _ -> } + .setCancelable(false) + .show() + } +} diff --git a/app/src/main/res/drawable/ic_payment_security_setup_lock.xml b/app/src/main/res/drawable/ic_payment_security_setup_lock.xml new file mode 100644 index 0000000000..2696890429 --- /dev/null +++ b/app/src/main/res/drawable/ic_payment_security_setup_lock.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/layout/payments_security_setup_fragment.xml b/app/src/main/res/layout/payments_security_setup_fragment.xml new file mode 100644 index 0000000000..f28c96d797 --- /dev/null +++ b/app/src/main/res/layout/payments_security_setup_fragment.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/navigation/payments_preferences.xml b/app/src/main/res/navigation/payments_preferences.xml index f052ec26e7..8d0c8798c2 100644 --- a/app/src/main/res/navigation/payments_preferences.xml +++ b/app/src/main/res/navigation/payments_preferences.xml @@ -112,6 +112,14 @@ + + + + + + + + + + + + Not Now + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Add funds Your Wallet Address From 52965da8a5f4f12a0cc9a7126908ea2837d7595b Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Fri, 30 Sep 2022 10:49:29 -0400 Subject: [PATCH 09/78] Stop checking very old capabilities. --- .../InternalConversationSettingsFragment.kt | 29 ++++++---- .../securesms/database/RecipientDatabase.kt | 41 ++++++++++---- .../database/model/RecipientRecord.kt | 34 +++++++++--- .../groups/GroupsV1MigrationUtil.java | 14 +---- .../GroupsV1MigrationRepository.java | 18 +------ .../securesms/jobs/GroupV1MigrationJob.java | 43 --------------- .../jobs/SenderKeyDistributionSendJob.java | 5 -- .../keyvalue/MiscellaneousValues.java | 9 ---- .../logsubmit/LogSectionCapabilities.java | 45 ++++++++++------ .../securesms/messages/GroupSendUtil.java | 7 +-- .../messages/MessageDecryptionUtil.java | 2 +- .../securesms/recipients/Recipient.java | 53 +++---------------- .../recipients/RecipientDetails.java | 24 ++------- .../database/RecipientDatabaseTestUtils.kt | 18 ++++--- 14 files changed, 130 insertions(+), 212 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsFragment.kt index 6caa16f27f..737861e116 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsFragment.kt @@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.database.model.RecipientRecord import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.keyvalue.SignalStore @@ -266,17 +267,23 @@ class InternalConversationSettingsFragment : DSLSettingsFragment( } private fun buildCapabilitySpan(recipient: Recipient): CharSequence { - return TextUtils.concat( - colorize("GV1Migration", recipient.groupsV1MigrationCapability), - ", ", - colorize("AnnouncementGroup", recipient.announcementGroupCapability), - ", ", - colorize("SenderKey", recipient.senderKeyCapability), - ", ", - colorize("ChangeNumber", recipient.changeNumberCapability), - ", ", - colorize("Stories", recipient.storiesCapability), - ) + val capabilities: RecipientRecord.Capabilities? = SignalDatabase.recipients.getCapabilities(recipient.id) + + return if (capabilities != null) { + TextUtils.concat( + colorize("GV1Migration", capabilities.groupsV1MigrationCapability), + ", ", + colorize("AnnouncementGroup", capabilities.announcementGroupCapability), + ", ", + colorize("SenderKey", capabilities.senderKeyCapability), + ", ", + colorize("ChangeNumber", capabilities.changeNumberCapability), + ", ", + colorize("Stories", capabilities.storiesCapability), + ) + } else { + "Recipient not found!" + } } private fun colorize(name: String, support: Recipient.Capability): CharSequence { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt index 6549198157..76d314f434 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt @@ -3431,6 +3431,21 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : updateExtras(recipientId) { b: RecipientExtras.Builder -> b.setManuallyShownAvatar(true) } } + fun getCapabilities(id: RecipientId): RecipientRecord.Capabilities? { + readableDatabase + .select(CAPABILITIES) + .from(TABLE_NAME) + .where("$ID = ?", id) + .run() + .use { cursor -> + return if (cursor.moveToFirst()) { + readCapabilities(cursor) + } else { + null + } + } + } + private fun updateExtras(recipientId: RecipientId, updater: java.util.function.Function) { val db = writableDatabase db.beginTransaction() @@ -3626,7 +3641,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : SYSTEM_PHONE_LABEL to secondaryRecord.systemPhoneLabel, SYSTEM_CONTACT_URI to secondaryRecord.systemContactUri, PROFILE_SHARING to (primaryRecord.profileSharing || secondaryRecord.profileSharing), - CAPABILITIES to max(primaryRecord.rawCapabilities, secondaryRecord.rawCapabilities), + CAPABILITIES to max(primaryRecord.capabilities.rawBits, secondaryRecord.capabilities.rawBits), MENTION_SETTING to if (primaryRecord.mentionSetting != MentionSetting.ALWAYS_NOTIFY) primaryRecord.mentionSetting.id else secondaryRecord.mentionSetting.id ) @@ -3901,7 +3916,6 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : } val recipientId = RecipientId.from(cursor.requireLong(idColumnName)) - val capabilities = cursor.requireLong(CAPABILITIES) val distributionListId: DistributionListId? = DistributionListId.fromNullable(cursor.requireLong(DISTRIBUTION_LIST_ID)) val avatarColor: AvatarColor = if (distributionListId != null) AvatarColor.UNKNOWN else AvatarColor.deserialize(cursor.requireString(AVATAR_COLOR)) @@ -3939,14 +3953,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : notificationChannel = cursor.requireString(NOTIFICATION_CHANNEL), unidentifiedAccessMode = UnidentifiedAccessMode.fromMode(cursor.requireInt(UNIDENTIFIED_ACCESS_MODE)), forceSmsSelection = cursor.requireBoolean(FORCE_SMS_SELECTION), - rawCapabilities = capabilities, - groupsV1MigrationCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.GROUPS_V1_MIGRATION, Capabilities.BIT_LENGTH).toInt()), - senderKeyCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.SENDER_KEY, Capabilities.BIT_LENGTH).toInt()), - announcementGroupCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.ANNOUNCEMENT_GROUPS, Capabilities.BIT_LENGTH).toInt()), - changeNumberCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.CHANGE_NUMBER, Capabilities.BIT_LENGTH).toInt()), - storiesCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.STORIES, Capabilities.BIT_LENGTH).toInt()), - giftBadgesCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.GIFT_BADGES, Capabilities.BIT_LENGTH).toInt()), - pnpCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.PNP, Capabilities.BIT_LENGTH).toInt()), + capabilities = readCapabilities(cursor), insightsBannerTier = InsightsBannerTier.fromId(cursor.requireInt(SEEN_INVITE_REMINDER)), storageId = Base64.decodeNullableOrThrow(cursor.requireString(STORAGE_SERVICE_ID)), mentionSetting = MentionSetting.fromId(cursor.requireInt(MENTION_SETTING)), @@ -3964,6 +3971,20 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : ) } + private fun readCapabilities(cursor: Cursor): RecipientRecord.Capabilities { + val capabilities = cursor.requireLong(CAPABILITIES) + return RecipientRecord.Capabilities( + rawBits = capabilities, + groupsV1MigrationCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.GROUPS_V1_MIGRATION, Capabilities.BIT_LENGTH).toInt()), + senderKeyCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.SENDER_KEY, Capabilities.BIT_LENGTH).toInt()), + announcementGroupCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.ANNOUNCEMENT_GROUPS, Capabilities.BIT_LENGTH).toInt()), + changeNumberCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.CHANGE_NUMBER, Capabilities.BIT_LENGTH).toInt()), + storiesCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.STORIES, Capabilities.BIT_LENGTH).toInt()), + giftBadgesCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.GIFT_BADGES, Capabilities.BIT_LENGTH).toInt()), + pnpCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.PNP, Capabilities.BIT_LENGTH).toInt()), + ) + } + private fun parseBadgeList(serializedBadgeList: ByteArray?): List { var badgeList: BadgeList? = null if (serializedBadgeList != null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt index 80a23c8497..118909df9f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt @@ -63,14 +63,7 @@ data class RecipientRecord( val unidentifiedAccessMode: UnidentifiedAccessMode, @get:JvmName("isForceSmsSelection") val forceSmsSelection: Boolean, - val rawCapabilities: Long, - val groupsV1MigrationCapability: Recipient.Capability, - val senderKeyCapability: Recipient.Capability, - val announcementGroupCapability: Recipient.Capability, - val changeNumberCapability: Recipient.Capability, - val storiesCapability: Recipient.Capability, - val giftBadgesCapability: Recipient.Capability, - val pnpCapability: Recipient.Capability, + val capabilities: Capabilities, val insightsBannerTier: InsightsBannerTier, val storageId: ByteArray?, val mentionSetting: MentionSetting, @@ -122,4 +115,29 @@ data class RecipientRecord( val isForcedUnread: Boolean, val unregisteredTimestamp: Long ) + + data class Capabilities( + val rawBits: Long, + val groupsV1MigrationCapability: Recipient.Capability, + val senderKeyCapability: Recipient.Capability, + val announcementGroupCapability: Recipient.Capability, + val changeNumberCapability: Recipient.Capability, + val storiesCapability: Recipient.Capability, + val giftBadgesCapability: Recipient.Capability, + val pnpCapability: Recipient.Capability, + ) { + companion object { + @JvmField + val UNKNOWN = Capabilities( + 0, + Recipient.Capability.UNKNOWN, + Recipient.Capability.UNKNOWN, + Recipient.Capability.UNKNOWN, + Recipient.Capability.UNKNOWN, + Recipient.Capability.UNKNOWN, + Recipient.Capability.UNKNOWN, + Recipient.Capability.UNKNOWN + ) + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV1MigrationUtil.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV1MigrationUtil.java index e45b76c68e..f6c8fb0c96 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV1MigrationUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV1MigrationUtil.java @@ -88,7 +88,7 @@ public static void migrate(@NonNull Context context, @NonNull RecipientId recipi registeredMembers = Stream.of(registeredMembers).map(Recipient::fresh).toList(); } - List possibleMembers = forced ? getMigratableManualMigrationMembers(registeredMembers) + List possibleMembers = forced ? registeredMembers : getMigratableAutoMigrationMembers(registeredMembers); if (!forced && !groupRecipient.hasName()) { @@ -200,17 +200,8 @@ private static void handleLeftBehind(@NonNull GroupId.V1 gv1Id) { * to consider them migratable in an auto-migration. */ private static @NonNull List getMigratableAutoMigrationMembers(@NonNull List registeredMembers) { - return Stream.of(getMigratableManualMigrationMembers(registeredMembers)) - .filter(r -> r.getProfileKey() != null) - .toList(); - } - - /** - * You can only migrate users that have the required capabilities. - */ - private static @NonNull List getMigratableManualMigrationMembers(@NonNull List registeredMembers) { return Stream.of(registeredMembers) - .filter(r -> r.getGroupsV1MigrationCapability() == Recipient.Capability.SUPPORTED) + .filter(r -> r.getProfileKey() != null) .toList(); } @@ -219,7 +210,6 @@ private static void handleLeftBehind(@NonNull GroupId.V1 gv1Id) { */ public static boolean isAutoMigratable(@NonNull Recipient recipient) { return recipient.hasServiceId() && - recipient.getGroupsV1MigrationCapability() == Recipient.Capability.SUPPORTED && recipient.getRegistered() == RecipientDatabase.RegisteredState.REGISTERED && recipient.getProfileKey() != null; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/migration/GroupsV1MigrationRepository.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/migration/GroupsV1MigrationRepository.java index 9f827a1199..e813f9270f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/migration/GroupsV1MigrationRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/migration/GroupsV1MigrationRepository.java @@ -68,19 +68,7 @@ private MigrationState getMigrationState(@NonNull RecipientId groupRecipientId) return new MigrationState(Collections.emptyList(), Collections.emptyList()); } - List members = Recipient.resolvedList(group.getParticipantIds()); - Set needsRefresh = Stream.of(members) - .filter(r -> r.getGroupsV1MigrationCapability() != Recipient.Capability.SUPPORTED) - .map(Recipient::getId) - .collect(Collectors.toSet()); - - List jobs = RetrieveProfileJob.forRecipients(needsRefresh); - - for (Job job : jobs) { - if (!ApplicationDependencies.getJobManager().runSynchronously(job, TimeUnit.SECONDS.toMillis(3)).isPresent()) { - Log.w(TAG, "Failed to refresh capabilities in time!"); - } - } + List members = Recipient.resolvedList(group.getParticipantIds()); try { List registered = Stream.of(members) @@ -95,9 +83,7 @@ private MigrationState getMigrationState(@NonNull RecipientId groupRecipientId) group = group.fresh(); List ineligible = Stream.of(members) - .filter(r -> !r.hasServiceId() || - r.getGroupsV1MigrationCapability() != Recipient.Capability.SUPPORTED || - r.getRegistered() != RecipientDatabase.RegisteredState.REGISTERED) + .filter(r -> !r.hasServiceId() || r.getRegistered() != RecipientDatabase.RegisteredState.REGISTERED) .toList(); List invites = Stream.of(members) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupV1MigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupV1MigrationJob.java index 2f37c995a8..36e423e441 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupV1MigrationJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupV1MigrationJob.java @@ -66,49 +66,6 @@ public static void enqueuePossibleAutoMigrate(@NonNull RecipientId recipientId) }); } - public static void enqueueRoutineMigrationsIfNecessary(@NonNull Application application) { - if (!SignalStore.registrationValues().isRegistrationComplete() || - !SignalStore.account().isRegistered() || - SignalStore.account().getAci() == null) - { - Log.i(TAG, "Registration not complete. Skipping."); - return; - } - - long timeSinceRefresh = System.currentTimeMillis() - SignalStore.misc().getLastGv1RoutineMigrationTime(); - - if (timeSinceRefresh < REFRESH_INTERVAL) { - Log.i(TAG, "Too soon to refresh. Did the last refresh " + timeSinceRefresh + " ms ago."); - return; - } - - SignalStore.misc().setLastGv1RoutineMigrationTime(System.currentTimeMillis()); - - SignalExecutors.BOUNDED.execute(() -> { - JobManager jobManager = ApplicationDependencies.getJobManager(); - List threads = SignalDatabase.threads().getRecentV1Groups(ROUTINE_LIMIT); - Set needsRefresh = new HashSet<>(); - - if (threads.size() > 0) { - Log.d(TAG, "About to enqueue refreshes for " + threads.size() + " groups."); - } - - for (ThreadRecord thread : threads) { - jobManager.add(new GroupV1MigrationJob(thread.getRecipient().getId())); - - needsRefresh.addAll(Stream.of(Recipient.resolvedList(thread.getRecipient().getParticipantIds())) - .filter(r -> r.getGroupsV1MigrationCapability() != Recipient.Capability.SUPPORTED) - .map(Recipient::getId) - .toList()); - } - - if (needsRefresh.size() > 0) { - Log.w(TAG, "Enqueuing profile refreshes for " + needsRefresh.size() + " GV1 participants."); - RetrieveProfileJob.enqueue(needsRefresh); - } - }); - } - @Override public @NonNull Data serialize() { return new Data.Builder().putString(KEY_RECIPIENT_ID, recipientId.serialize()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SenderKeyDistributionSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SenderKeyDistributionSendJob.java index 55a3291003..eaeea0af5c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SenderKeyDistributionSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SenderKeyDistributionSendJob.java @@ -79,11 +79,6 @@ protected void onRun() throws Exception { Recipient targetRecipient = Recipient.resolved(targetRecipientId); Recipient threadRecipient = Recipient.resolved(threadRecipientId); - if (targetRecipient.getSenderKeyCapability() != Recipient.Capability.SUPPORTED) { - Log.w(TAG, targetRecipientId + " does not support sender key! Not sending."); - return; - } - if (targetRecipient.isUnregistered()) { Log.w(TAG, threadRecipient.getId() + " not registered!"); return; diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java index 8b35d39b5e..104636d9b9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java @@ -13,7 +13,6 @@ public final class MiscellaneousValues extends SignalStoreValues { private static final String LAST_PREKEY_REFRESH_TIME = "last_prekey_refresh_time"; private static final String MESSAGE_REQUEST_ENABLE_TIME = "message_request_enable_time"; private static final String LAST_PROFILE_REFRESH_TIME = "misc.last_profile_refresh_time"; - private static final String LAST_GV1_ROUTINE_MIGRATION_TIME = "misc.last_gv1_routine_migration_time"; private static final String USERNAME_SHOW_REMINDER = "username.show.reminder"; private static final String CLIENT_DEPRECATED = "misc.client_deprecated"; private static final String OLD_DEVICE_TRANSFER_LOCKED = "misc.old_device.transfer.locked"; @@ -62,14 +61,6 @@ public void setLastProfileRefreshTime(long time) { putLong(LAST_PROFILE_REFRESH_TIME, time); } - public long getLastGv1RoutineMigrationTime() { - return getLong(LAST_GV1_ROUTINE_MIGRATION_TIME, 0); - } - - public void setLastGv1RoutineMigrationTime(long time) { - putLong(LAST_GV1_ROUTINE_MIGRATION_TIME, time); - } - public void hideUsernameReminder() { putBoolean(USERNAME_SHOW_REMINDER, false); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionCapabilities.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionCapabilities.java index 7f2f2bbf0b..acec125f56 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionCapabilities.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionCapabilities.java @@ -5,6 +5,9 @@ import androidx.annotation.NonNull; import org.thoughtcrime.securesms.AppCapabilities; +import org.thoughtcrime.securesms.database.RecipientDatabase; +import org.thoughtcrime.securesms.database.SignalDatabase; +import org.thoughtcrime.securesms.database.model.RecipientRecord; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.recipients.Recipient; import org.whispersystems.signalservice.api.account.AccountAttributes; @@ -28,23 +31,31 @@ public final class LogSectionCapabilities implements LogSection { Recipient self = Recipient.self(); - AccountAttributes.Capabilities capabilities = AppCapabilities.getCapabilities(false); - - return new StringBuilder().append("-- Local").append("\n") - .append("GV2 : ").append(capabilities.isGv2()).append("\n") - .append("GV1 Migration : ").append(capabilities.isGv1Migration()).append("\n") - .append("Sender Key : ").append(capabilities.isSenderKey()).append("\n") - .append("Announcement Groups: ").append(capabilities.isAnnouncementGroup()).append("\n") - .append("Change Number : ").append(capabilities.isChangeNumber()).append("\n") - .append("Stories : ").append(capabilities.isStories()).append("\n") - .append("Gift Badges : ").append(capabilities.isGiftBadges()).append("\n") + AccountAttributes.Capabilities localCapabilities = AppCapabilities.getCapabilities(false); + RecipientRecord.Capabilities globalCapabilities = SignalDatabase.recipients().getCapabilities(self.getId()); + + StringBuilder builder = new StringBuilder().append("-- Local").append("\n") + .append("GV2 : ").append(localCapabilities.isGv2()).append("\n") + .append("GV1 Migration : ").append(localCapabilities.isGv1Migration()).append("\n") + .append("Sender Key : ").append(localCapabilities.isSenderKey()).append("\n") + .append("Announcement Groups: ").append(localCapabilities.isAnnouncementGroup()).append("\n") + .append("Change Number : ").append(localCapabilities.isChangeNumber()).append("\n") + .append("Stories : ").append(localCapabilities.isStories()).append("\n") + .append("Gift Badges : ").append(localCapabilities.isGiftBadges()).append("\n") .append("\n") - .append("-- Global").append("\n") - .append("GV1 Migration : ").append(self.getGroupsV1MigrationCapability()).append("\n") - .append("Sender Key : ").append(self.getSenderKeyCapability()).append("\n") - .append("Announcement Groups: ").append(self.getAnnouncementGroupCapability()).append("\n") - .append("Change Number : ").append(self.getChangeNumberCapability()).append("\n") - .append("Stories : ").append(self.getStoriesCapability()).append("\n") - .append("Gift Badges : ").append(self.getGiftBadgesCapability()).append("\n"); + .append("-- Global").append("\n"); + + if (globalCapabilities != null) { + builder.append("GV1 Migration : ").append(globalCapabilities.getGroupsV1MigrationCapability()).append("\n") + .append("Sender Key : ").append(globalCapabilities.getSenderKeyCapability()).append("\n") + .append("Announcement Groups: ").append(globalCapabilities.getAnnouncementGroupCapability()).append("\n") + .append("Change Number : ").append(globalCapabilities.getChangeNumberCapability()).append("\n") + .append("Stories : ").append(globalCapabilities.getStoriesCapability()).append("\n") + .append("Gift Badges : ").append(globalCapabilities.getGiftBadgesCapability()).append("\n"); + } else { + builder.append("Self not found!"); + } + + return builder; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java b/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java index 5b083501ca..fbf9886ff6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java @@ -242,8 +242,7 @@ private static List sendMessage(@NonNull Context context, validMembership = false; } - if (recipient.getSenderKeyCapability() == Recipient.Capability.SUPPORTED && - recipient.hasServiceId() && + if (recipient.hasServiceId() && access.isPresent() && access.get().getTargetUnidentifiedAccess().isPresent() && validMembership) @@ -258,10 +257,6 @@ private static List sendMessage(@NonNull Context context, Log.i(TAG, "No DistributionId. Using legacy."); legacyTargets.addAll(senderKeyTargets); senderKeyTargets.clear(); - } else if (Recipient.self().getSenderKeyCapability() != Recipient.Capability.SUPPORTED) { - Log.i(TAG, "All of our devices do not support sender key. Using legacy."); - legacyTargets.addAll(senderKeyTargets); - senderKeyTargets.clear(); } else if (SignalStore.internalValues().removeSenderKeyMinimum()) { Log.i(TAG, "Sender key minimum removed. Using for " + senderKeyTargets.size() + " recipients."); } else if (senderKeyTargets.size() < 2) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptionUtil.java b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptionUtil.java index e97fa69dbf..819a9c9239 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptionUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptionUtil.java @@ -127,7 +127,7 @@ private MessageDecryptionUtil() {} Log.w(TAG, String.valueOf(envelope.getTimestamp()), e, true); Recipient sender = Recipient.external(context, e.getSender()); - if (sender.supportsMessageRetries() && Recipient.self().supportsMessageRetries() && FeatureFlags.retryReceipts()) { + if (FeatureFlags.retryReceipts()) { jobs.add(handleRetry(context, sender, envelope, e)); postInternalErrorNotification(context); } else { 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 e89f9082e8..de69e8e431 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java @@ -36,6 +36,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.model.DistributionListId; import org.thoughtcrime.securesms.database.model.ProfileAvatarFileDetails; +import org.thoughtcrime.securesms.database.model.RecipientRecord; import org.thoughtcrime.securesms.database.model.databaseprotos.RecipientExtras; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupId; @@ -118,13 +119,7 @@ public class Recipient { private final String notificationChannel; private final UnidentifiedAccessMode unidentifiedAccessMode; private final boolean forceSmsSelection; - private final Capability groupsV1MigrationCapability; - private final Capability senderKeyCapability; - private final Capability announcementGroupCapability; - private final Capability changeNumberCapability; - private final Capability storiesCapability; - private final Capability giftBadgesCapability; - private final Capability pnpCapability; + private final RecipientRecord.Capabilities capabilities; private final InsightsBannerTier insightsBannerTier; private final byte[] storageId; private final MentionSetting mentionSetting; @@ -421,13 +416,7 @@ public static boolean isSelfSet() { this.notificationChannel = null; this.unidentifiedAccessMode = UnidentifiedAccessMode.DISABLED; this.forceSmsSelection = false; - this.groupsV1MigrationCapability = Capability.UNKNOWN; - this.senderKeyCapability = Capability.UNKNOWN; - this.announcementGroupCapability = Capability.UNKNOWN; - this.changeNumberCapability = Capability.UNKNOWN; - this.storiesCapability = Capability.UNKNOWN; - this.giftBadgesCapability = Capability.UNKNOWN; - this.pnpCapability = Capability.UNKNOWN; + this.capabilities = RecipientRecord.Capabilities.UNKNOWN; this.storageId = null; this.mentionSetting = MentionSetting.ALWAYS_NOTIFY; this.wallpaper = null; @@ -481,13 +470,7 @@ public Recipient(@NonNull RecipientId id, @NonNull RecipientDetails details, boo this.notificationChannel = details.notificationChannel; this.unidentifiedAccessMode = details.unidentifiedAccessMode; this.forceSmsSelection = details.forceSmsSelection; - this.groupsV1MigrationCapability = details.groupsV1MigrationCapability; - this.senderKeyCapability = details.senderKeyCapability; - this.announcementGroupCapability = details.announcementGroupCapability; - this.changeNumberCapability = details.changeNumberCapability; - this.storiesCapability = details.storiesCapability; - this.giftBadgesCapability = details.giftBadgesCapability; - this.pnpCapability = details.pnpCapability; + this.capabilities = details.capabilities; this.storageId = details.storageId; this.mentionSetting = details.mentionSetting; this.wallpaper = details.wallpaper; @@ -1022,39 +1005,20 @@ public boolean isForceSmsSelection() { return forceSmsSelection; } - public @NonNull Capability getGroupsV1MigrationCapability() { - return groupsV1MigrationCapability; - } - - public @NonNull Capability getSenderKeyCapability() { - return senderKeyCapability; - } - - public @NonNull Capability getAnnouncementGroupCapability() { - return announcementGroupCapability; - } - public @NonNull Capability getChangeNumberCapability() { - return changeNumberCapability; + return capabilities.getChangeNumberCapability(); } public @NonNull Capability getStoriesCapability() { - return storiesCapability; + return capabilities.getStoriesCapability(); } public @NonNull Capability getGiftBadgesCapability() { - return giftBadgesCapability; + return capabilities.getGiftBadgesCapability(); } public @NonNull Capability getPnpCapability() { - return pnpCapability; - } - - /** - * True if this recipient supports the message retry system, or false if we should use the legacy session reset system. - */ - public boolean supportsMessageRetries() { - return getSenderKeyCapability() == Capability.SUPPORTED; + return capabilities.getPnpCapability(); } public @Nullable byte[] getProfileKey() { @@ -1336,7 +1300,6 @@ public boolean hasSameContent(@NonNull Recipient other) { Objects.equals(profileAvatar, other.profileAvatar) && Objects.equals(notificationChannel, other.notificationChannel) && unidentifiedAccessMode == other.unidentifiedAccessMode && - groupsV1MigrationCapability == other.groupsV1MigrationCapability && insightsBannerTier == other.insightsBannerTier && Arrays.equals(storageId, other.storageId) && mentionSetting == other.mentionSetting && diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java index 98be1ce31a..4d854eb464 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java @@ -69,13 +69,7 @@ public class RecipientDetails { final String notificationChannel; final UnidentifiedAccessMode unidentifiedAccessMode; final boolean forceSmsSelection; - final Recipient.Capability groupsV1MigrationCapability; - final Recipient.Capability senderKeyCapability; - final Recipient.Capability announcementGroupCapability; - final Recipient.Capability changeNumberCapability; - final Recipient.Capability storiesCapability; - final Recipient.Capability giftBadgesCapability; - final Recipient.Capability pnpCapability; + final RecipientRecord.Capabilities capabilities; final InsightsBannerTier insightsBannerTier; final byte[] storageId; final MentionSetting mentionSetting; @@ -134,13 +128,7 @@ public RecipientDetails(@Nullable String groupName, this.notificationChannel = record.getNotificationChannel(); this.unidentifiedAccessMode = record.getUnidentifiedAccessMode(); this.forceSmsSelection = record.isForceSmsSelection(); - this.groupsV1MigrationCapability = record.getGroupsV1MigrationCapability(); - this.senderKeyCapability = record.getSenderKeyCapability(); - this.announcementGroupCapability = record.getAnnouncementGroupCapability(); - this.changeNumberCapability = record.getChangeNumberCapability(); - this.storiesCapability = record.getStoriesCapability(); - this.giftBadgesCapability = record.getGiftBadgesCapability(); - this.pnpCapability = record.getPnpCapability(); + this.capabilities = record.getCapabilities(); this.insightsBannerTier = record.getInsightsBannerTier(); this.storageId = record.getStorageId(); this.mentionSetting = record.getMentionSetting(); @@ -195,13 +183,7 @@ private RecipientDetails() { this.unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN; this.forceSmsSelection = false; this.groupName = null; - this.groupsV1MigrationCapability = Recipient.Capability.UNKNOWN; - this.senderKeyCapability = Recipient.Capability.UNKNOWN; - this.announcementGroupCapability = Recipient.Capability.UNKNOWN; - this.changeNumberCapability = Recipient.Capability.UNKNOWN; - this.storiesCapability = Recipient.Capability.UNKNOWN; - this.giftBadgesCapability = Recipient.Capability.UNKNOWN; - this.pnpCapability = Recipient.Capability.UNKNOWN; + this.capabilities = RecipientRecord.Capabilities.UNKNOWN; this.storageId = null; this.mentionSetting = MentionSetting.ALWAYS_NOTIFY; this.wallpaper = null; diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt b/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt index 570af53f24..16ee8025e9 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt @@ -127,14 +127,16 @@ object RecipientDatabaseTestUtils { notificationChannel, unidentifiedAccessMode, forceSmsSelection, - capabilities, - Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientDatabase.Capabilities.GROUPS_V1_MIGRATION, RecipientDatabase.Capabilities.BIT_LENGTH).toInt()), - Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientDatabase.Capabilities.SENDER_KEY, RecipientDatabase.Capabilities.BIT_LENGTH).toInt()), - Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientDatabase.Capabilities.ANNOUNCEMENT_GROUPS, RecipientDatabase.Capabilities.BIT_LENGTH).toInt()), - Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientDatabase.Capabilities.CHANGE_NUMBER, RecipientDatabase.Capabilities.BIT_LENGTH).toInt()), - Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientDatabase.Capabilities.STORIES, RecipientDatabase.Capabilities.BIT_LENGTH).toInt()), - Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientDatabase.Capabilities.GIFT_BADGES, RecipientDatabase.Capabilities.BIT_LENGTH).toInt()), - Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientDatabase.Capabilities.PNP, RecipientDatabase.Capabilities.BIT_LENGTH).toInt()), + RecipientRecord.Capabilities( + capabilities, + Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientDatabase.Capabilities.GROUPS_V1_MIGRATION, RecipientDatabase.Capabilities.BIT_LENGTH).toInt()), + Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientDatabase.Capabilities.SENDER_KEY, RecipientDatabase.Capabilities.BIT_LENGTH).toInt()), + Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientDatabase.Capabilities.ANNOUNCEMENT_GROUPS, RecipientDatabase.Capabilities.BIT_LENGTH).toInt()), + Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientDatabase.Capabilities.CHANGE_NUMBER, RecipientDatabase.Capabilities.BIT_LENGTH).toInt()), + Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientDatabase.Capabilities.STORIES, RecipientDatabase.Capabilities.BIT_LENGTH).toInt()), + Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientDatabase.Capabilities.GIFT_BADGES, RecipientDatabase.Capabilities.BIT_LENGTH).toInt()), + Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientDatabase.Capabilities.PNP, RecipientDatabase.Capabilities.BIT_LENGTH).toInt()), + ), insightBannerTier, storageId, mentionSetting, From c1f3e27101b3280589040e4fee6b417874c0f47d Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Fri, 30 Sep 2022 11:00:00 -0400 Subject: [PATCH 10/78] Fix missed call notification when busy on another device. --- .../org/thoughtcrime/securesms/keyvalue/InternalValues.java | 2 +- .../service/webrtc/ActiveCallActionProcessorDelegate.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java index f5f1372fcf..30cf73acdb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java @@ -159,7 +159,7 @@ public synchronized CallManager.BandwidthMode callingBandwidthMode() { */ public synchronized boolean callingDisableTelecom() { if (FeatureFlags.internalUser()) { - return getBoolean(CALLING_DISABLE_TELECOM, false); + return getBoolean(CALLING_DISABLE_TELECOM, true); } else { return false; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ActiveCallActionProcessorDelegate.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ActiveCallActionProcessorDelegate.java index a748a47903..f977dc3599 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ActiveCallActionProcessorDelegate.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ActiveCallActionProcessorDelegate.java @@ -165,7 +165,7 @@ public ActiveCallActionProcessorDelegate(@NonNull WebRtcInteractor webRtcInterac state = Objects.requireNonNull(ENDED_REMOTE_EVENT_TO_STATE.get(endedRemoteEvent)); } - if (endedRemoteEvent == CallEvent.ENDED_REMOTE_HANGUP) { + if (endedRemoteEvent == CallEvent.ENDED_REMOTE_HANGUP || endedRemoteEvent == CallEvent.ENDED_REMOTE_HANGUP_BUSY) { if (remotePeerIsActive) { state = outgoingBeforeAccept ? WebRtcViewModel.State.RECIPIENT_UNAVAILABLE : WebRtcViewModel.State.CALL_DISCONNECTED; } From 083219888c5f37c463303d357905b60f60cffd64 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Fri, 30 Sep 2022 15:18:52 -0300 Subject: [PATCH 11/78] Add logging around attachment id update. --- .../securesms/database/AttachmentDatabase.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java index b7a08bf341..3dc93134d6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java @@ -757,10 +757,15 @@ public void updateMessageId(@NonNull Collection attachmentIds, lon values.putNull(CAPTION); } + int updatedCount = 0; + int attachmentIdSize = 0; for (AttachmentId attachmentId : attachmentIds) { - db.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()); + attachmentIdSize++; + updatedCount = db.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()); } + Log.d(TAG, "[updateMessageId] Updated " + updatedCount + " out of " + attachmentIdSize + " ids."); + db.setTransactionSuccessful(); } finally { db.endTransaction(); From 437c3ffd669e43b2024c54ddfe7bc53cfd5d7d45 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Fri, 30 Sep 2022 15:37:25 -0300 Subject: [PATCH 12/78] Add logging to forward fragment closes. --- .../mutiselect/forward/MultiselectForwardActivity.kt | 7 ++++++- .../mutiselect/forward/MultiselectForwardFragment.kt | 9 +++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardActivity.kt index 53b835f989..223b378907 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardActivity.kt @@ -8,6 +8,7 @@ import androidx.activity.result.contract.ActivityResultContract import androidx.appcompat.widget.Toolbar import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment +import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.FragmentWrapperActivity import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey @@ -16,6 +17,7 @@ import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectFor open class MultiselectForwardActivity : FragmentWrapperActivity(), MultiselectForwardFragment.Callback, SearchConfigurationProvider { companion object { + private val TAG = Log.tag(MultiselectForwardActivity::class.java) private const val ARGS = "args" } @@ -43,9 +45,12 @@ open class MultiselectForwardActivity : FragmentWrapperActivity(), MultiselectFo ) } - override fun onFinishForwardAction() = Unit + override fun onFinishForwardAction() { + Log.d(TAG, "Completed forward action...") + } override fun exitFlow() { + Log.d(TAG, "Exiting flow...") onBackPressedDispatcher.onBackPressed() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt index 185a084e02..7c6ebba575 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt @@ -29,6 +29,7 @@ import androidx.fragment.app.viewModels import androidx.recyclerview.widget.RecyclerView import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.signal.core.util.DimensionUnit +import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.ContactFilterView import org.thoughtcrime.securesms.components.TooltipPopup @@ -318,6 +319,8 @@ class MultiselectForwardFragment : } private fun dismissAndShowToast(@PluralsRes toastTextResId: Int) { + Log.d(TAG, "dismissAndShowToast") + val argCount = getMessageCount() callback.onFinishForwardAction() @@ -329,6 +332,8 @@ class MultiselectForwardFragment : private fun getMessageCount(): Int = args.multiShareArgs.size + if (addMessage.text.isNotEmpty()) 1 else 0 private fun handleMessageExpired() { + Log.d(TAG, "handleMessageExpired") + callback.onFinishForwardAction() dismissibleDialog?.dismiss() Toast.makeText(requireContext(), resources.getQuantityString(R.plurals.MultiselectForwardFragment__couldnt_forward_messages, args.multiShareArgs.size), Toast.LENGTH_LONG).show() @@ -336,6 +341,8 @@ class MultiselectForwardFragment : } private fun dismissWithSelection(selectedContacts: Set) { + Log.d(TAG, "dismissWithSelection") + callback.onFinishForwardAction() dismissibleDialog?.dismiss() @@ -486,6 +493,8 @@ class MultiselectForwardFragment : } companion object { + private val TAG = Log.tag(MultiselectForwardActivity::class.java) + const val DIALOG_TITLE = "title" const val ARGS = "args" const val RESULT_KEY = "result_key" From afedbf40e3c34d96090cffa2b9ed2fa2fd85bc0c Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Fri, 30 Sep 2022 16:41:00 -0400 Subject: [PATCH 13/78] Prepare the websocket keepalive for API 31. --- .../app/internal/InternalSettingsFragment.kt | 20 +++++++ .../app/internal/InternalSettingsState.kt | 1 + .../app/internal/InternalSettingsViewModel.kt | 6 ++ .../ApplicationDependencyProvider.java | 2 +- .../securesms/jobs/RefreshAttributesJob.java | 2 +- .../securesms/keyvalue/InternalValues.java | 12 ++++ .../messages/IncomingMessageObserver.java | 19 ++++--- .../securesms/util/AlarmSleepTimer.java | 57 ++++++++++++------- 8 files changed, 86 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt index 738fd9c1de..af5a454a5b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt @@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.util.navigation.safeNavigate import java.util.Optional import java.util.concurrent.TimeUnit import kotlin.math.max +import kotlin.time.Duration.Companion.seconds class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__internal_preferences) { @@ -201,6 +202,25 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter sectionHeaderPref(R.string.preferences__internal_network) + switchPref( + title = DSLSettingsText.from("Force websocket mode"), + summary = DSLSettingsText.from("Pretend you have no Play Services. Ignores websocket messages and keeps the websocket open in a foreground service. You have to manually force-stop the app for changes to take effect."), + isChecked = state.forceWebsocketMode, + onClick = { + viewModel.setForceWebsocketMode(!state.forceWebsocketMode) + SimpleTask.run({ + val jobState = ApplicationDependencies.getJobManager().runSynchronously(RefreshAttributesJob(), 10.seconds.inWholeMilliseconds) + return@run jobState.isPresent && jobState.get().isComplete + }, { success -> + if (success) { + Toast.makeText(context, "Successfully refreshed attributes. Force-stop the app for changes to take effect.", Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(context, "Failed to refresh attributes.", Toast.LENGTH_SHORT).show() + } + }) + } + ) + switchPref( title = DSLSettingsText.from(R.string.preferences__internal_allow_censorship_toggle), summary = DSLSettingsText.from(R.string.preferences__internal_allow_censorship_toggle_description), diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsState.kt index 0013621335..8def66aa89 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsState.kt @@ -10,6 +10,7 @@ data class InternalSettingsState( val gv2ignoreServerChanges: Boolean, val gv2ignoreP2PChanges: Boolean, val allowCensorshipSetting: Boolean, + val forceWebsocketMode: Boolean, val callingServer: String, val callingAudioProcessingMethod: CallManager.AudioProcessingMethod, val callingBandwidthMode: CallManager.BandwidthMode, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsViewModel.kt index a230cc732a..79ca353d33 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsViewModel.kt @@ -59,6 +59,11 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito refresh() } + fun setForceWebsocketMode(enabled: Boolean) { + preferenceDataStore.putBoolean(InternalValues.FORCE_WEBSOCKET_MODE, enabled) + refresh() + } + fun setUseBuiltInEmoji(enabled: Boolean) { preferenceDataStore.putBoolean(InternalValues.FORCE_BUILT_IN_EMOJI, enabled) refresh() @@ -109,6 +114,7 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito gv2ignoreServerChanges = SignalStore.internalValues().gv2IgnoreServerChanges(), gv2ignoreP2PChanges = SignalStore.internalValues().gv2IgnoreP2PChanges(), allowCensorshipSetting = SignalStore.internalValues().allowChangingCensorshipSetting(), + forceWebsocketMode = SignalStore.internalValues().isWebsocketModeForced, callingServer = SignalStore.internalValues().groupCallingServer(), callingAudioProcessingMethod = SignalStore.internalValues().callingAudioProcessingMethod(), callingBandwidthMode = SignalStore.internalValues().callingBandwidthMode(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java index 003a71266d..24511d0149 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java @@ -283,7 +283,7 @@ public ApplicationDependencyProvider(@NonNull Application context) { @Override public @NonNull SignalWebSocket provideSignalWebSocket(@NonNull Supplier signalServiceConfigurationSupplier) { - SleepTimer sleepTimer = SignalStore.account().isFcmEnabled() ? new UptimeSleepTimer() : new AlarmSleepTimer(context); + SleepTimer sleepTimer = !SignalStore.account().isFcmEnabled() || SignalStore.internalValues().isWebsocketModeForced() ? new AlarmSleepTimer(context) : new UptimeSleepTimer() ; SignalWebSocketHealthMonitor healthMonitor = new SignalWebSocketHealthMonitor(context, sleepTimer); SignalWebSocket signalWebSocket = new SignalWebSocket(provideWebSocketFactory(signalServiceConfigurationSupplier, healthMonitor)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java index 58e8a7a7ad..524301f081 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java @@ -84,7 +84,7 @@ public void onRun() throws IOException { } int registrationId = SignalStore.account().getRegistrationId(); - boolean fetchesMessages = !SignalStore.account().isFcmEnabled(); + boolean fetchesMessages = !SignalStore.account().isFcmEnabled() || SignalStore.internalValues().isWebsocketModeForced(); byte[] unidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey()); boolean universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context); String registrationLockV1 = null; diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java index 30cf73acdb..e27614a07a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java @@ -26,6 +26,7 @@ public final class InternalValues extends SignalStoreValues { public static final String CALLING_DISABLE_TELECOM = "internal.calling_disable_telecom"; public static final String SHAKE_TO_REPORT = "internal.shake_to_report"; public static final String DISABLE_STORAGE_SERVICE = "internal.disable_storage_service"; + public static final String FORCE_WEBSOCKET_MODE = "internal.force_websocket_mode"; InternalValues(KeyValueStore store) { super(store); @@ -164,4 +165,15 @@ public synchronized boolean callingDisableTelecom() { return false; } } + + /** + * Whether or not the system is forced to be in 'websocket mode', where FCM is ignored and we use a foreground service to keep the app alive. + */ + public boolean isWebsocketModeForced() { + if (FeatureFlags.internalUser()) { + return getBoolean(FORCE_WEBSOCKET_MODE, false); + } else { + return false; + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.java b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.java index bf9b647422..b0f876549c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.java @@ -81,7 +81,7 @@ public IncomingMessageObserver(@NonNull Application context) { new MessageRetrievalThread().start(); - if (!SignalStore.account().isFcmEnabled()) { + if (!SignalStore.account().isFcmEnabled() || SignalStore.internalValues().isWebsocketModeForced()) { ContextCompat.startForegroundService(context, new Intent(context, ForegroundService.class)); } @@ -157,22 +157,23 @@ private synchronized void onAppBackgrounded() { } private synchronized boolean isConnectionNecessary() { - boolean registered = SignalStore.account().isRegistered(); - boolean fcmEnabled = SignalStore.account().isFcmEnabled(); - boolean hasNetwork = NetworkConstraint.isMet(context); - boolean hasProxy = SignalStore.proxy().isProxyEnabled(); - long oldRequest = System.currentTimeMillis() - OLD_REQUEST_WINDOW_MS; + boolean registered = SignalStore.account().isRegistered(); + boolean fcmEnabled = SignalStore.account().isFcmEnabled(); + boolean hasNetwork = NetworkConstraint.isMet(context); + boolean hasProxy = SignalStore.proxy().isProxyEnabled(); + boolean forceWebsocket = SignalStore.internalValues().isWebsocketModeForced(); + long oldRequest = System.currentTimeMillis() - OLD_REQUEST_WINDOW_MS; boolean removedRequests = keepAliveTokens.entrySet().removeIf(e -> e.getValue() < oldRequest); if (removedRequests) { Log.d(TAG, "Removed old keep web socket open requests."); } - Log.d(TAG, String.format("Network: %s, Foreground: %s, FCM: %s, Stay open requests: [%s], Censored: %s, Registered: %s, Proxy: %s", - hasNetwork, appVisible, fcmEnabled, Util.join(keepAliveTokens.entrySet(), ","), networkAccess.isCensored(), registered, hasProxy)); + Log.d(TAG, String.format("Network: %s, Foreground: %s, FCM: %s, Stay open requests: [%s], Censored: %s, Registered: %s, Proxy: %s, Force websocket: %s", + hasNetwork, appVisible, fcmEnabled, Util.join(keepAliveTokens.entrySet(), ","), networkAccess.isCensored(), registered, hasProxy, forceWebsocket)); return registered && - (appVisible || !fcmEnabled || Util.hasItems(keepAliveTokens)) && + (appVisible || !fcmEnabled || forceWebsocket || Util.hasItems(keepAliveTokens)) && hasNetwork && !networkAccess.isCensored(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/AlarmSleepTimer.java b/app/src/main/java/org/thoughtcrime/securesms/util/AlarmSleepTimer.java index 70ea8699bc..a50140358d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/AlarmSleepTimer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/AlarmSleepTimer.java @@ -2,10 +2,12 @@ import android.app.AlarmManager; import android.app.PendingIntent; +import android.app.Service; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.os.Build; import android.os.SystemClock; import androidx.core.app.AlarmManagerCompat; @@ -18,13 +20,12 @@ import java.util.concurrent.ConcurrentSkipListSet; /** - * A sleep timer that is based on elapsed realtime, so - * that it works properly, even in low-power sleep modes. - * + * A sleep timer that is based on elapsed realtime, so that it works properly, even in low-power sleep modes. */ public class AlarmSleepTimer implements SleepTimer { private static final String TAG = Log.tag(AlarmSleepTimer.class); - private static ConcurrentSkipListSet actionIdList = new ConcurrentSkipListSet<>(); + + private static final ConcurrentSkipListSet actionIdList = new ConcurrentSkipListSet<>(); private final Context context; @@ -33,23 +34,25 @@ public AlarmSleepTimer(Context context) { } @Override - public void sleep(long millis) { - final AlarmReceiver alarmReceiver = new AlarmSleepTimer.AlarmReceiver(); - int actionId = 0; + public void sleep(long sleepDuration) { + AlarmReceiver alarmReceiver = new AlarmSleepTimer.AlarmReceiver(); + int actionId = 0; + while (!actionIdList.add(actionId)){ actionId++; } + try { - context.registerReceiver(alarmReceiver, - new IntentFilter(AlarmReceiver.WAKE_UP_THREAD_ACTION + "." + actionId)); + String actionName = buildActionName(actionId); + context.registerReceiver(alarmReceiver, new IntentFilter(actionName)); - final long startTime = System.currentTimeMillis(); - alarmReceiver.setAlarm(millis, AlarmReceiver.WAKE_UP_THREAD_ACTION + "." + actionId); + long startTime = System.currentTimeMillis(); + alarmReceiver.setAlarm(sleepDuration, actionName); - while (System.currentTimeMillis() - startTime < millis) { + while (System.currentTimeMillis() - startTime < sleepDuration) { try { synchronized (this) { - wait(millis - System.currentTimeMillis() + startTime); + wait(sleepDuration - (System.currentTimeMillis() - startTime)); } } catch (InterruptedException e) { Log.w(TAG, e); @@ -58,25 +61,35 @@ public void sleep(long millis) { context.unregisterReceiver(alarmReceiver); } catch(Exception e) { Log.w(TAG, "Exception during sleep ...",e); - }finally { + } finally { actionIdList.remove(actionId); } } + private static String buildActionName(int actionId) { + return AlarmReceiver.WAKE_UP_THREAD_ACTION + "." + actionId; + } + private class AlarmReceiver extends BroadcastReceiver { private static final String WAKE_UP_THREAD_ACTION = "org.thoughtcrime.securesms.util.AlarmSleepTimer.AlarmReceiver.WAKE_UP_THREAD"; private void setAlarm(long millis, String action) { final Intent intent = new Intent(action); final PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntentFlags.mutable()); - final AlarmManager alarmManager = ContextCompat.getSystemService(context, AlarmManager.class); - - Log.w(TAG, "Setting alarm to wake up in " + millis + "ms."); - - AlarmManagerCompat.setExactAndAllowWhileIdle(alarmManager, - AlarmManager.ELAPSED_REALTIME_WAKEUP, - SystemClock.elapsedRealtime() + millis, - pendingIntent); + final AlarmManager alarmManager = ServiceUtil.getAlarmManager(context); + + if (Build.VERSION.SDK_INT < 31 || alarmManager.canScheduleExactAlarms()) { + Log.d(TAG, "Setting an exact alarm to wake up in " + millis + "ms."); + AlarmManagerCompat.setExactAndAllowWhileIdle(alarmManager, + AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + millis, + pendingIntent); + } else { + Log.w(TAG, "Setting an inexact alarm to wake up in " + millis + "ms. CanScheduleAlarms: " + alarmManager.canScheduleExactAlarms()); + alarmManager.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + millis, + pendingIntent); + } } @Override From 79b3b9190ad73b094ed9aa1ae29122665b3305a8 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Mon, 3 Oct 2022 12:26:55 -0300 Subject: [PATCH 14/78] Add blocklist for mixed-mode capture. --- .../securesms/mediasend/CameraXFragment.java | 29 ++---- .../mediasend/CameraXVideoCaptureHelper.java | 6 ++ .../mediasend/camerax/CameraXModePolicy.kt | 92 +++++++++++++++++++ .../securesms/util/FeatureFlags.java | 7 ++ 4 files changed, 115 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXModePolicy.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.java index 1a3f25f7cf..15de7c43bd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.java @@ -49,6 +49,7 @@ import org.thoughtcrime.securesms.animation.AnimationCompleteListener; import org.thoughtcrime.securesms.components.TooltipPopup; import org.thoughtcrime.securesms.mediasend.camerax.CameraXFlashToggleView; +import org.thoughtcrime.securesms.mediasend.camerax.CameraXModePolicy; import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil; import org.thoughtcrime.securesms.mediasend.v2.MediaAnimations; import org.thoughtcrime.securesms.mediasend.v2.MediaCountIndicatorButton; @@ -87,6 +88,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment { private MemoryFileDescriptor videoFileDescriptor; private LifecycleCameraController cameraController; private Disposable mostRecentItemDisposable = Disposable.disposed(); + private CameraXModePolicy cameraXModePolicy; private boolean isThumbAvailable; private boolean isMediaSelected; @@ -136,13 +138,18 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat this.previewView = view.findViewById(R.id.camerax_camera); this.controlsContainer = view.findViewById(R.id.camerax_controls_container); + this.cameraXModePolicy = CameraXModePolicy.acquire(requireContext(), + controller.getMediaConstraints(), + requireArguments().getBoolean(IS_VIDEO_ENABLED, true)); + + Log.d(TAG, "Starting CameraX with mode policy " + cameraXModePolicy.getClass().getSimpleName()); cameraController = new LifecycleCameraController(requireContext()); cameraController.bindToLifecycle(getViewLifecycleOwner()); cameraController.setCameraSelector(CameraXUtil.toCameraSelector(TextSecurePreferences.getDirectCaptureCameraId(requireContext()))); cameraController.setTapToFocusEnabled(true); cameraController.setImageCaptureMode(CameraXUtil.getOptimalCaptureMode()); - cameraController.setEnabledUseCases(getSupportedUseCases()); + cameraXModePolicy.initialize(cameraController); previewView.setScaleType(PREVIEW_SCALE_TYPE); previewView.setController(cameraController); @@ -335,7 +342,7 @@ private void initControls() { galleryButton.setOnClickListener(v -> controller.onGalleryClicked()); countButton.setOnClickListener(v -> controller.onCameraCountButtonClicked()); - if (isVideoRecordingSupported(requireContext())) { + if (Build.VERSION.SDK_INT >= 26 && cameraXModePolicy.isVideoSupported()) { try { closeVideoFileDescriptor(); videoFileDescriptor = CameraXVideoCaptureHelper.createFileDescriptor(requireContext()); @@ -356,6 +363,7 @@ private void initControls() { cameraController, previewView, videoFileDescriptor, + cameraXModePolicy, maxDuration, new CameraXVideoCaptureHelper.Callback() { @Override @@ -389,23 +397,6 @@ public void onVideoError(@Nullable Throwable cause) { } } - @CameraController.UseCases - private int getSupportedUseCases() { - if (isVideoRecordingSupported(requireContext())) { - return CameraController.IMAGE_CAPTURE | CameraController.VIDEO_CAPTURE; - } else { - return CameraController.IMAGE_CAPTURE; - } - } - - private boolean isVideoRecordingSupported(@NonNull Context context) { - return Build.VERSION.SDK_INT >= 26 && - requireArguments().getBoolean(IS_VIDEO_ENABLED, true) && - MediaConstraints.isVideoTranscodeAvailable() && - CameraXUtil.isMixedModeSupported(context) && - VideoUtil.getMaxVideoRecordDurationInSeconds(context, controller.getMediaConstraints()) > 0; - } - private void displayVideoRecordingTooltipIfNecessary(CameraButtonView captureButton) { if (shouldDisplayVideoRecordingTooltip()) { int displayRotation = requireActivity().getWindowManager().getDefaultDisplay().getRotation(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXVideoCaptureHelper.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXVideoCaptureHelper.java index 56bc38dcf9..4da3e0e3c1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXVideoCaptureHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXVideoCaptureHelper.java @@ -26,6 +26,7 @@ import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.mediasend.camerax.CameraXModePolicy; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.util.Debouncer; import org.thoughtcrime.securesms.util.MemoryFileDescriptor; @@ -51,6 +52,7 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener private final @NonNull MemoryFileDescriptor memoryFileDescriptor; private final @NonNull ValueAnimator updateProgressAnimator; private final @NonNull Debouncer debouncer; + private final @NonNull CameraXModePolicy cameraXModePolicy; private ValueAnimator cameraMetricsAnimator; @@ -81,6 +83,7 @@ public void onError(int videoCaptureError, @NonNull String message, @Nullable Th @NonNull CameraController cameraController, @NonNull PreviewView previewView, @NonNull MemoryFileDescriptor memoryFileDescriptor, + @NonNull CameraXModePolicy cameraXModePolicy, int maxVideoDurationSec, @NonNull Callback callback) { @@ -91,6 +94,7 @@ public void onError(int videoCaptureError, @NonNull String message, @Nullable Th this.callback = callback; this.updateProgressAnimator = ValueAnimator.ofFloat(0f, 1f).setDuration(TimeUnit.SECONDS.toMillis(maxVideoDurationSec)); this.debouncer = new Debouncer(TimeUnit.SECONDS.toMillis(maxVideoDurationSec)); + this.cameraXModePolicy = cameraXModePolicy; updateProgressAnimator.setInterpolator(new LinearInterpolator()); updateProgressAnimator.addUpdateListener(anim -> captureButton.setProgress(anim.getAnimatedFraction())); @@ -123,6 +127,7 @@ private void displayAudioRecordingPermissionsDialog() { @SuppressLint("RestrictedApi") private void beginCameraRecording() { + cameraXModePolicy.setToVideo(cameraController); this.cameraController.setZoomRatio(Objects.requireNonNull(this.cameraController.getZoomState().getValue()).getMinZoomRatio()); callback.onVideoRecordStarted(); shrinkCaptureArea(); @@ -196,6 +201,7 @@ public void onVideoCaptureComplete() { updateProgressAnimator.cancel(); debouncer.clear(); + cameraXModePolicy.setToImage(cameraController); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXModePolicy.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXModePolicy.kt new file mode 100644 index 0000000000..a4bc5d808b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXModePolicy.kt @@ -0,0 +1,92 @@ +package org.thoughtcrime.securesms.mediasend.camerax + +import android.content.Context +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.camera.view.CameraController +import androidx.camera.view.video.ExperimentalVideo +import org.signal.core.util.asListContains +import org.thoughtcrime.securesms.mms.MediaConstraints +import org.thoughtcrime.securesms.util.FeatureFlags +import org.thoughtcrime.securesms.video.VideoUtil + +/** + * Describes device capabilities + */ +@RequiresApi(21) +@ExperimentalVideo +sealed class CameraXModePolicy { + + abstract val isVideoSupported: Boolean + + abstract fun initialize(cameraController: CameraController) + + open fun setToImage(cameraController: CameraController) = Unit + + open fun setToVideo(cameraController: CameraController) = Unit + + /** + * The device supports having Image and Video enabled at the same time + */ + object Mixed : CameraXModePolicy() { + + override val isVideoSupported: Boolean = true + + override fun initialize(cameraController: CameraController) { + cameraController.setEnabledUseCases(CameraController.IMAGE_CAPTURE or CameraController.VIDEO_CAPTURE) + } + } + + /** + * The device supports image and video, but only one mode at a time. + */ + object Single : CameraXModePolicy() { + + override val isVideoSupported: Boolean = true + + override fun initialize(cameraController: CameraController) { + setToImage(cameraController) + } + + override fun setToImage(cameraController: CameraController) { + cameraController.setEnabledUseCases(CameraController.IMAGE_CAPTURE) + } + + override fun setToVideo(cameraController: CameraController) { + cameraController.setEnabledUseCases(CameraController.VIDEO_CAPTURE) + } + } + + /** + * The device supports taking images only. + */ + object ImageOnly : CameraXModePolicy() { + + override val isVideoSupported: Boolean = false + + override fun initialize(cameraController: CameraController) { + cameraController.setEnabledUseCases(CameraController.IMAGE_CAPTURE) + } + } + + companion object { + @JvmStatic + fun acquire(context: Context, mediaConstraints: MediaConstraints, isVideoEnabled: Boolean): CameraXModePolicy { + val isVideoSupported = Build.VERSION.SDK_INT >= 26 && + isVideoEnabled && + MediaConstraints.isVideoTranscodeAvailable() && + VideoUtil.getMaxVideoRecordDurationInSeconds(context, mediaConstraints) > 0 + + val isMixedModeSupported = isVideoSupported && + Build.VERSION.SDK_INT >= 26 && + CameraXUtil.isMixedModeSupported(context) && + !FeatureFlags.cameraXMixedModelBlocklist().asListContains(Build.MODEL) + + return when { + isMixedModeSupported -> Mixed + isVideoSupported -> Single + else -> ImageOnly + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index 431e127e07..23559335b3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -97,6 +97,7 @@ public final class FeatureFlags { private static final String TELECOM_MANUFACTURER_ALLOWLIST = "android.calling.telecomAllowList"; private static final String TELECOM_MODEL_BLOCKLIST = "android.calling.telecomModelBlockList"; private static final String CAMERAX_MODEL_BLOCKLIST = "android.cameraXModelBlockList"; + private static final String CAMERAX_MIXED_MODEL_BLOCKLIST = "android.cameraXMixedModelBlockList"; private static final String RECIPIENT_MERGE_V2 = "android.recipientMergeV2"; private static final String CDS_V2_LOAD_TEST = "android.cdsV2LoadTest"; private static final String SMS_EXPORTER = "android.sms.exporter"; @@ -153,6 +154,7 @@ public final class FeatureFlags { TELECOM_MANUFACTURER_ALLOWLIST, TELECOM_MODEL_BLOCKLIST, CAMERAX_MODEL_BLOCKLIST, + CAMERAX_MIXED_MODEL_BLOCKLIST, RECIPIENT_MERGE_V2, CDS_V2_LOAD_TEST, SMS_EXPORTER, @@ -492,6 +494,11 @@ public static boolean storiesTextFunctions() { return getString(CAMERAX_MODEL_BLOCKLIST, ""); } + /** A comma-separated list of manufacturers that should *not* use CameraX mixed mode. */ + public static @NonNull String cameraXMixedModelBlocklist() { + return getString(CAMERAX_MIXED_MODEL_BLOCKLIST, ""); + } + /** Whether or not hardware AEC should be used for calling on devices older than API 29. */ public static boolean useHardwareAecIfOlderThanApi29() { return getBoolean(USE_HARDWARE_AEC_IF_OLD, false); From 4f3910e3ae87e2eaa9d4b52bc72e7e50b055ff6e Mon Sep 17 00:00:00 2001 From: Nicholas Date: Mon, 3 Oct 2022 15:10:30 -0400 Subject: [PATCH 15/78] Add toolbar to MediaPreviewV2 implementation. --- .../mediapreview/MediaPreviewRepository.kt | 7 +- .../mediapreview/MediaPreviewV2Activity.kt | 3 - ...diaAdapter.kt => MediaPreviewV2Adapter.kt} | 12 ++- .../mediapreview/MediaPreviewV2Fragment.kt | 82 ++++++++++++++++++- .../mediapreview/MediaPreviewV2State.kt | 8 +- .../mediapreview/MediaPreviewV2ViewModel.kt | 19 ++++- .../res/layout/fragment_media_preview_v2.xml | 29 ++++++- 7 files changed, 139 insertions(+), 21 deletions(-) rename app/src/main/java/org/thoughtcrime/securesms/mediapreview/{PreviewMediaAdapter.kt => MediaPreviewV2Adapter.kt} (77%) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewRepository.kt index ee0c94b4d5..25c83262b1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewRepository.kt @@ -7,7 +7,6 @@ import io.reactivex.rxjava3.schedulers.Schedulers import org.signal.core.util.logging.Log import org.signal.core.util.requireLong import org.thoughtcrime.securesms.attachments.AttachmentId -import org.thoughtcrime.securesms.attachments.DatabaseAttachment import org.thoughtcrime.securesms.database.AttachmentDatabase import org.thoughtcrime.securesms.database.MediaDatabase import org.thoughtcrime.securesms.database.MediaDatabase.Sorting @@ -29,11 +28,11 @@ class MediaPreviewRepository { * @param sorting the ordering of the results * @param limit the maximum quantity of the results */ - fun getAttachments(startingUri: Uri, threadId: Long, sorting: Sorting, limit: Int = 500): Flowable> { + fun getAttachments(startingUri: Uri, threadId: Long, sorting: Sorting, limit: Int = 500): Flowable> { return Single.fromCallable { val cursor = media.getGalleryMediaForThread(threadId, sorting) - val acc = mutableListOf() + val acc = mutableListOf() var attachmentUri: Uri? = null while (cursor.moveToNext()) { val attachmentId = AttachmentId(cursor.requireLong(AttachmentDatabase.ROW_ID), cursor.requireLong(AttachmentDatabase.UNIQUE_ID)) @@ -45,7 +44,7 @@ class MediaPreviewRepository { if (attachmentUri == startingUri) { for (i in 0..limit) { - val element = MediaDatabase.MediaRecord.from(cursor).attachment + val element = MediaDatabase.MediaRecord.from(cursor) if (element != null) { acc.add(element) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Activity.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Activity.kt index 3735f8fc00..cc1f34a0c5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Activity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Activity.kt @@ -21,8 +21,5 @@ class MediaPreviewV2Activity : AppCompatActivity(R.layout.activity_mediapreview_ companion object { private const val FRAGMENT_TAG = "media_preview_fragment_v2" - private const val NOT_IN_A_THREAD = -2 - - const val THREAD_ID_EXTRA = "thread_id" } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/PreviewMediaAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Adapter.kt similarity index 77% rename from app/src/main/java/org/thoughtcrime/securesms/mediapreview/PreviewMediaAdapter.kt rename to app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Adapter.kt index 781a461538..3992b53842 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/PreviewMediaAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Adapter.kt @@ -6,8 +6,9 @@ import androidx.viewpager2.adapter.FragmentStateAdapter import org.thoughtcrime.securesms.attachments.Attachment import org.thoughtcrime.securesms.util.MediaUtil -class PreviewMediaAdapter(val fragment: Fragment, val items: List) : FragmentStateAdapter(fragment) { - var autoPlayPosition = -1 +class MediaPreviewV2Adapter(val fragment: Fragment) : FragmentStateAdapter(fragment) { + private var items: List = listOf() + private var autoPlayPosition = -1 override fun getItemCount(): Int { return items.count() @@ -36,4 +37,11 @@ class PreviewMediaAdapter(val fragment: Fragment, val items: List) : return fragment } + + fun updateBackingItems(newItems: Collection) { + if (newItems != items) { + items = newItems.toList() + notifyDataSetChanged() + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt index 3a22843c4c..e89cc7c77e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt @@ -1,9 +1,11 @@ package org.thoughtcrime.securesms.mediapreview +import android.content.Context import android.os.Bundle import android.view.View import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R @@ -11,7 +13,11 @@ import org.thoughtcrime.securesms.animation.DepthPageTransformer import org.thoughtcrime.securesms.components.ViewBinderDelegate import org.thoughtcrime.securesms.database.MediaDatabase import org.thoughtcrime.securesms.databinding.FragmentMediaPreviewV2Binding +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.util.DateUtils +import org.thoughtcrime.securesms.util.FullscreenHelper import org.thoughtcrime.securesms.util.LifecycleDisposable +import java.util.Locale class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), MediaPreviewFragment.Events { private val TAG = Log.tag(MediaPreviewV2Fragment::class.java) @@ -20,24 +26,92 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med private val binding by ViewBinderDelegate(FragmentMediaPreviewV2Binding::bind) private val viewModel: MediaPreviewV2ViewModel by viewModels() + private lateinit var fullscreenHelper: FullscreenHelper + + override fun onAttach(context: Context) { + super.onAttach(context) + fullscreenHelper = FullscreenHelper(requireActivity()) + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + initializeViewModel() binding.mediaPager.offscreenPageLimit = 1 binding.mediaPager.setPageTransformer(DepthPageTransformer()) - lifecycleDisposable += viewModel.state.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread()).subscribe { - if (it.loadState == MediaPreviewV2State.LoadState.READY) { - binding.mediaPager.adapter = PreviewMediaAdapter(this, it.attachments) + val adapter = MediaPreviewV2Adapter(this) + binding.mediaPager.adapter = adapter + binding.mediaPager.registerOnPageChangeCallback(object : OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + super.onPageSelected(position) + viewModel.setCurrentPage(position) } + }) + initializeFullScreenUi() + lifecycleDisposable += viewModel.state.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread()).subscribe { + bindCurrentState(it) } - initializeViewModel() + } + + private fun initializeFullScreenUi() { + fullscreenHelper.configureToolbarLayout(binding.toolbarCutoutSpacer, binding.toolbar) + fullscreenHelper.hideSystemUI() } private fun initializeViewModel() { val args = MediaIntentFactory.requireArguments(requireArguments()) + viewModel.setShowThread(args.showThread) val sorting = MediaDatabase.Sorting.values()[args.sorting] viewModel.fetchAttachments(args.initialMediaUri, args.threadId, sorting) } + private fun bindCurrentState(currentState: MediaPreviewV2State) { + when (currentState.loadState) { + MediaPreviewV2State.LoadState.READY -> bindReadyState(currentState) + // INIT, else -> no-op + } + } + + private fun bindReadyState(currentState: MediaPreviewV2State) { + (binding.mediaPager.adapter as MediaPreviewV2Adapter).updateBackingItems(currentState.mediaRecords.mapNotNull { it.attachment }) + val currentItem: MediaDatabase.MediaRecord = currentState.mediaRecords[currentState.position] + binding.toolbar.title = getTitleText(currentItem, currentState.showThread) + binding.toolbar.subtitle = getSubTitleText(currentItem) + } + + private fun getTitleText(mediaRecord: MediaDatabase.MediaRecord, showThread: Boolean): String { + val recipient: Recipient = Recipient.live(mediaRecord.recipientId).get() + val defaultFromString: String = if (mediaRecord.isOutgoing) { + getString(R.string.MediaPreviewActivity_you) + } else { + recipient.getDisplayName(requireContext()) + } + if (!showThread) { + return defaultFromString + } + + val threadRecipient = Recipient.live(mediaRecord.threadRecipientId).get() + return if (mediaRecord.isOutgoing) { + if (threadRecipient.isSelf) { + getString(R.string.note_to_self) + } else { + getString(R.string.MediaPreviewActivity_you_to_s, threadRecipient.getDisplayName(requireContext())) + } + } else { + if (threadRecipient.isGroup) { + getString(R.string.MediaPreviewActivity_s_to_s, defaultFromString, threadRecipient.getDisplayName(requireContext())) + } else { + getString(R.string.MediaPreviewActivity_s_to_you, defaultFromString) + } + } + } + + private fun getSubTitleText(mediaRecord: MediaDatabase.MediaRecord): String = + if (mediaRecord.date > 0) { + DateUtils.getExtendedRelativeTimeSpanString(requireContext(), Locale.getDefault(), mediaRecord.date) + } else { + getString(R.string.MediaPreviewActivity_draft) + } + override fun singleTapOnMedia(): Boolean { Log.d(TAG, "singleTapOnMedia()") return true diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2State.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2State.kt index ef873fc32f..062468d932 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2State.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2State.kt @@ -1,10 +1,12 @@ package org.thoughtcrime.securesms.mediapreview -import org.thoughtcrime.securesms.attachments.Attachment +import org.thoughtcrime.securesms.database.MediaDatabase data class MediaPreviewV2State( - val attachments: List = emptyList(), - val loadState: LoadState = LoadState.INIT + val mediaRecords: List = emptyList(), + val loadState: LoadState = LoadState.INIT, + val position: Int = 0, + val showThread: Boolean = false ) { enum class LoadState { INIT, READY, } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2ViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2ViewModel.kt index 6de2873d91..247d6cf0fa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2ViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2ViewModel.kt @@ -19,8 +19,23 @@ class MediaPreviewV2ViewModel : ViewModel() { val state: Flowable = store.stateFlowable.observeOn(AndroidSchedulers.mainThread()) fun fetchAttachments(startingUri: Uri, threadId: Long, sorting: MediaDatabase.Sorting) { - disposables += store.update(repository.getAttachments(startingUri, threadId, sorting)) { attachments, oldState -> - oldState.copy(attachments = attachments, loadState = MediaPreviewV2State.LoadState.READY) + disposables += store.update(repository.getAttachments(startingUri, threadId, sorting)) { mediaRecords: List, oldState: MediaPreviewV2State -> + oldState.copy( + mediaRecords = mediaRecords, + loadState = MediaPreviewV2State.LoadState.READY + ) + } + } + + fun setShowThread(value: Boolean) { + store.update { oldState -> + oldState.copy(showThread = value) + } + } + + fun setCurrentPage(position: Int) { + store.update { oldState -> + oldState.copy(position = position) } } diff --git a/app/src/main/res/layout/fragment_media_preview_v2.xml b/app/src/main/res/layout/fragment_media_preview_v2.xml index 02f8c6defa..6657aba085 100644 --- a/app/src/main/res/layout/fragment_media_preview_v2.xml +++ b/app/src/main/res/layout/fragment_media_preview_v2.xml @@ -1,9 +1,32 @@ - + android:layout_height="match_parent" + android:background="@color/core_grey_95" + android:theme="@style/TextSecure.MediaPreview"> + + + + + + + Date: Wed, 5 Oct 2022 12:06:47 -0300 Subject: [PATCH 16/78] Fix possible RxStore memory leak. --- .../received/ViewReceivedGiftViewModel.kt | 1 + .../viewgift/sent/ViewSentGiftViewModel.kt | 1 + .../donor/DonorErrorConfigurationViewModel.kt | 1 + .../contacts/ContactChipViewModel.kt | 1 + .../conversation/ConversationViewModel.java | 12 ++++--- .../conversation/drafts/DraftViewModel.kt | 4 +++ .../mediapreview/MediaPreviewV2ViewModel.kt | 1 + .../v2/capture/MediaCaptureViewModel.kt | 4 +++ .../pnp/WhoCanSeeMyPhoneNumberViewModel.kt | 1 + .../manage/UsernameEditViewModel.java | 1 + .../SafetyNumberBottomSheetViewModel.kt | 2 ++ .../securesms/sharing/v2/ShareViewModel.kt | 1 + ...ChooseInitialMyStoryMembershipViewModel.kt | 1 + .../story/StoriesPrivacySettingsViewModel.kt | 3 +- .../stories/viewer/StoryViewerViewModel.kt | 1 + .../stories/viewer/StoryVolumeViewModel.kt | 4 +++ .../stories/viewer/info/StoryInfoViewModel.kt | 1 + .../viewer/page/StoryViewerPageViewModel.kt | 1 + .../reply/group/StoryGroupReplyViewModel.kt | 1 + .../thoughtcrime/securesms/util/rx/RxStore.kt | 31 ++++++++++++++----- .../securesms/util/rx/RxStoreTest.kt | 3 ++ 21 files changed, 63 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/viewgift/received/ViewReceivedGiftViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/viewgift/received/ViewReceivedGiftViewModel.kt index 46cf5683d5..1a1ad08d92 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/viewgift/received/ViewReceivedGiftViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/viewgift/received/ViewReceivedGiftViewModel.kt @@ -70,6 +70,7 @@ class ViewReceivedGiftViewModel( override fun onCleared() { disposables.dispose() + store.dispose() } fun setChecked(isChecked: Boolean) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/viewgift/sent/ViewSentGiftViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/viewgift/sent/ViewSentGiftViewModel.kt index 0c82cd7f0d..a9bedda9c1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/viewgift/sent/ViewSentGiftViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/viewgift/sent/ViewSentGiftViewModel.kt @@ -38,6 +38,7 @@ class ViewSentGiftViewModel( override fun onCleared() { disposables.dispose() + store.dispose() } class Factory( diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/donor/DonorErrorConfigurationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/donor/DonorErrorConfigurationViewModel.kt index bd3205f6ed..aee2f131bd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/donor/DonorErrorConfigurationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/donor/DonorErrorConfigurationViewModel.kt @@ -62,6 +62,7 @@ class DonorErrorConfigurationViewModel : ViewModel() { override fun onCleared() { disposables.clear() + store.dispose() } fun setSelectedBadge(badgeIndex: Int) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactChipViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactChipViewModel.kt index 0cd7ce6d0b..33e9b20b5b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactChipViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactChipViewModel.kt @@ -35,6 +35,7 @@ class ContactChipViewModel : ViewModel() { disposables.clear() disposableMap.values.forEach { it.dispose() } disposableMap.clear() + store.dispose() } fun add(selectedContact: SelectedContact) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java index a7d24b05e5..17e15105c9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java @@ -59,6 +59,7 @@ import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.disposables.CompositeDisposable; +import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.processors.PublishProcessor; import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.subjects.BehaviorSubject; @@ -135,10 +136,12 @@ private ConversationViewModel() { .map(Recipient::resolved) .subscribe(recipientCache); - conversationStateStore.update(Observable.combineLatest(recipientId.distinctUntilChanged(), conversationStateTick, (id, tick) -> id) - .switchMap(conversationRepository::getSecurityInfo) - .toFlowable(BackpressureStrategy.LATEST), - (securityInfo, state) -> state.withSecurityInfo(securityInfo)); + Disposable disposable = conversationStateStore.update(Observable.combineLatest(recipientId.distinctUntilChanged(), conversationStateTick, (id, tick) -> id) + .switchMap(conversationRepository::getSecurityInfo) + .toFlowable(BackpressureStrategy.LATEST), + (securityInfo, state) -> state.withSecurityInfo(securityInfo)); + + disposables.add(disposable); BehaviorSubject conversationMetadata = BehaviorSubject.create(); @@ -435,6 +438,7 @@ protected void onCleared() { ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageUpdateObserver); ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageInsertObserver); disposables.clear(); + conversationStateStore.dispose(); EventBus.getDefault().unregister(this); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftViewModel.kt index 41ba3ee6f1..a97b9e7cb0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftViewModel.kt @@ -33,6 +33,10 @@ class DraftViewModel @JvmOverloads constructor( val voiceNoteDraft: Draft? get() = store.state.voiceNoteDraft + override fun onCleared() { + store.dispose() + } + fun setThreadId(threadId: Long) { store.update { it.copy(threadId = threadId) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2ViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2ViewModel.kt index 247d6cf0fa..d5cbe58280 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2ViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2ViewModel.kt @@ -41,5 +41,6 @@ class MediaPreviewV2ViewModel : ViewModel() { override fun onCleared() { disposables.dispose() + store.dispose() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureViewModel.kt index 40f201d544..564cf727a6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureViewModel.kt @@ -26,6 +26,10 @@ class MediaCaptureViewModel(private val repository: MediaCaptureRepository) : Vi } } + override fun onCleared() { + store.dispose() + } + fun onImageCaptured(data: ByteArray, width: Int, height: Int) { repository.renderImageToMedia(data, width, height, this::onMediaRendered, this::onMediaRenderFailed) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/pnp/WhoCanSeeMyPhoneNumberViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/pnp/WhoCanSeeMyPhoneNumberViewModel.kt index a38c53ba02..0c8735cd49 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/pnp/WhoCanSeeMyPhoneNumberViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/pnp/WhoCanSeeMyPhoneNumberViewModel.kt @@ -29,5 +29,6 @@ class WhoCanSeeMyPhoneNumberViewModel : ViewModel() { override fun onCleared() { disposables.clear() + store.dispose() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.java index ef3b6afec4..68a50a55b7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.java @@ -65,6 +65,7 @@ private UsernameEditViewModel(boolean isInRegistration) { protected void onCleared() { super.onCleared(); disposables.clear(); + uiState.dispose(); } void onNicknameUpdated(@NonNull String nickname) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/safety/SafetyNumberBottomSheetViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/safety/SafetyNumberBottomSheetViewModel.kt index 5cbd6847fa..2066277fa9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/safety/SafetyNumberBottomSheetViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/safety/SafetyNumberBottomSheetViewModel.kt @@ -66,6 +66,8 @@ class SafetyNumberBottomSheetViewModel( override fun onCleared() { disposables.clear() + destinationStore.dispose() + store.dispose() } fun getIdentityRecord(recipientId: RecipientId): Maybe { diff --git a/app/src/main/java/org/thoughtcrime/securesms/sharing/v2/ShareViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/sharing/v2/ShareViewModel.kt index acb5727b7d..95ad1a920b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sharing/v2/ShareViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/sharing/v2/ShareViewModel.kt @@ -72,6 +72,7 @@ class ShareViewModel( override fun onCleared() { disposables.clear() + store.dispose() } private fun moveToFailedState(throwable: Throwable? = null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipViewModel.kt index 64ee05a75f..bb0d88f637 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipViewModel.kt @@ -31,6 +31,7 @@ class ChooseInitialMyStoryMembershipViewModel @JvmOverloads constructor( override fun onCleared() { disposables.clear() + store.dispose() } fun select(selection: DistributionListPrivacyMode): Single { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsViewModel.kt index a17d08ed08..f326015845 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsViewModel.kt @@ -59,13 +59,14 @@ class StoriesPrivacySettingsViewModel : ViewModel() { pagingController.set(observablePagedData.controller) - store.update(observablePagedData.data.toFlowable(BackpressureStrategy.LATEST)) { data, state -> + disposables += store.update(observablePagedData.data.toFlowable(BackpressureStrategy.LATEST)) { data, state -> state.copy(storyContactItems = data) } } override fun onCleared() { disposables.clear() + store.dispose() } fun setStoriesEnabled(isEnabled: Boolean) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerViewModel.kt index db9116615f..b7cd12412e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerViewModel.kt @@ -143,6 +143,7 @@ class StoryViewerViewModel( override fun onCleared() { disposables.clear() + store.dispose() } fun setSelectedPage(page: Int) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryVolumeViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryVolumeViewModel.kt index 8cbfd370d3..3e422577d0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryVolumeViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryVolumeViewModel.kt @@ -10,6 +10,10 @@ class StoryVolumeViewModel : ViewModel() { val state: Flowable = store.stateFlowable val snapshot: StoryVolumeState get() = store.state + override fun onCleared() { + store.dispose() + } + fun mute() { store.update { it.copy(isMuted = true) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoViewModel.kt index c2d9c642a7..1ed2e043a4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoViewModel.kt @@ -75,6 +75,7 @@ class StoryInfoViewModel(storyId: Long, repository: StoryInfoRepository = StoryI override fun onCleared() { disposables.clear() + store.dispose() } class Factory(private val storyId: Long) : ViewModelProvider.Factory { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModel.kt index 6b0136dc08..0601202476 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModel.kt @@ -99,6 +99,7 @@ class StoryViewerPageViewModel( override fun onCleared() { disposables.clear() storyCache.clear() + store.dispose() } fun hideStory(): Completable { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyViewModel.kt index 81fd72e42d..be7861927e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyViewModel.kt @@ -51,6 +51,7 @@ class StoryGroupReplyViewModel(storyId: Long, repository: StoryGroupReplyReposit override fun onCleared() { disposables.clear() + store.dispose() } class Factory(private val storyId: Long, private val repository: StoryGroupReplyRepository) : ViewModelProvider.Factory { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/rx/RxStore.kt b/app/src/main/java/org/thoughtcrime/securesms/util/rx/RxStore.kt index a36ced2e07..bab6d2cefa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/rx/RxStore.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/rx/RxStore.kt @@ -1,8 +1,10 @@ package org.thoughtcrime.securesms.util.rx +import androidx.annotation.CheckResult import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Scheduler import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.kotlin.plusAssign import io.reactivex.rxjava3.processors.BehaviorProcessor import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.subjects.PublishSubject @@ -10,11 +12,14 @@ import io.reactivex.rxjava3.subjects.PublishSubject /** * Rx replacement for Store. * Actions are run on the computation thread by default. + * + * This class is disposable, and should be explicitly disposed of in a ViewModel's onCleared method + * to prevent memory leaks. Disposing instances of this class is a terminal action. */ class RxStore( defaultValue: T, scheduler: Scheduler = Schedulers.computation() -) { +) : Disposable { private val behaviorProcessor = BehaviorProcessor.createDefault(defaultValue) private val actionSubject = PublishSubject.create<(T) -> T>().toSerialized() @@ -22,20 +27,30 @@ class RxStore( val state: T get() = behaviorProcessor.value!! val stateFlowable: Flowable = behaviorProcessor.onBackpressureLatest() - init { - actionSubject - .observeOn(scheduler) - .scan(defaultValue) { v, f -> f(v) } - .subscribe { behaviorProcessor.onNext(it) } - } + val actionDisposable: Disposable = actionSubject + .observeOn(scheduler) + .scan(defaultValue) { v, f -> f(v) } + .subscribe { behaviorProcessor.onNext(it) } fun update(transformer: (T) -> T) { actionSubject.onNext(transformer) } - fun update(flowable: Flowable, transformer: (U, T) -> T): Disposable { + @CheckResult + fun update(flowable: Flowable, transformer: (U, T) -> T): Disposable { return flowable.subscribe { actionSubject.onNext { t -> transformer(it, t) } } } + + /** + * Dispose of the underlying scan chain. This is terminal. + */ + override fun dispose() { + actionDisposable.dispose() + } + + override fun isDisposed(): Boolean { + return actionDisposable.isDisposed + } } diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/rx/RxStoreTest.kt b/app/src/test/java/org/thoughtcrime/securesms/util/rx/RxStoreTest.kt index 2f97e577ed..3e70042a99 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/util/rx/RxStoreTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/util/rx/RxStoreTest.kt @@ -33,6 +33,7 @@ class RxStoreTest { // THEN subscriber.assertValueAt(0, 1) subscriber.assertNotComplete() + testSubject.dispose() } @Test @@ -50,6 +51,7 @@ class RxStoreTest { subscriber.assertValueAt(0, 1) subscriber.assertValueAt(1, 2) subscriber.assertNotComplete() + testSubject.dispose() } @Test @@ -66,5 +68,6 @@ class RxStoreTest { // THEN subscriber.assertValueAt(0, 2) subscriber.assertNotComplete() + testSubject.dispose() } } From ad1801108d8efa12425367d852b48dd5a3dde70a Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 5 Oct 2022 11:52:57 -0400 Subject: [PATCH 17/78] Fix issues with story thread when processing a sync message. --- .../securesms/messages/MessageContentProcessor.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java index e7a10ba40b..08f9f9e0ea 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java @@ -1641,6 +1641,10 @@ private void handleStoryMessage(@NonNull SignalServiceContent content, @NonNull Recipient threadRecipient = Objects.requireNonNull(SignalDatabase.threads().getRecipientForThreadId(story.getThreadId())); boolean groupStory = threadRecipient.isActiveGroup(); + if (!groupStory) { + threadRecipient = senderRecipient; + } + handlePossibleExpirationUpdate(content, message, threadRecipient.getGroupId(), senderRecipient, threadRecipient, receivedTime); if (message.getGroupContext().isPresent() ) { @@ -1935,7 +1939,7 @@ private long handleSynchronizeSentStoryReply(@NonNull SentTranscriptMessage mess } quoteModel = new QuoteModel(storyContext.getSentTimestamp(), storyAuthorRecipient, quoteBody, false, story.getSlideDeck().asAttachments(), Collections.emptyList(), QuoteModel.Type.NORMAL); - expiresInMillis = TimeUnit.SECONDS.toMillis(message.getExpirationStartTimestamp()); + expiresInMillis = TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds()); } else { warn(envelopeTimestamp, "Story has replies disabled. Dropping reply."); return -1L; From 4b94509a7a4390d02983c17e55c094f6ff20c708 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Wed, 5 Oct 2022 15:04:54 -0300 Subject: [PATCH 18/78] Add dialog protection and remote deletion to disabling stories and deleting lists. --- .../DialogFragmentDisplayManager.kt | 35 +++++++++++++++++++ .../components/ProgressCardDialogFragment.kt | 20 +++++++++++ .../internal/StoryDialogLauncherFragment.kt | 27 ++++++++++++++ .../securesms/stories/dialogs/StoryDialogs.kt | 32 +++++++++++++++++ .../custom/PrivateStorySettingsFragment.kt | 27 +++++++++----- .../custom/PrivateStorySettingsRepository.kt | 8 +++++ .../custom/PrivateStorySettingsState.kt | 3 +- .../custom/PrivateStorySettingsViewModel.kt | 4 ++- .../story/StoriesPrivacySettingsFragment.kt | 20 +++++++---- .../story/StoriesPrivacySettingsRepository.kt | 16 +++++++++ .../story/StoriesPrivacySettingsState.kt | 3 +- .../story/StoriesPrivacySettingsViewModel.kt | 10 ++++++ app/src/main/res/layout/progress_card.xml | 1 + .../main/res/layout/progress_card_dialog.xml | 12 +++++++ app/src/main/res/values/strings.xml | 13 ++++++- 15 files changed, 212 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/DialogFragmentDisplayManager.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/ProgressCardDialogFragment.kt create mode 100644 app/src/main/res/layout/progress_card_dialog.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/DialogFragmentDisplayManager.kt b/app/src/main/java/org/thoughtcrime/securesms/components/DialogFragmentDisplayManager.kt new file mode 100644 index 0000000000..c6b16998e5 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/DialogFragmentDisplayManager.kt @@ -0,0 +1,35 @@ +package org.thoughtcrime.securesms.components + +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner + +/** + * Manages the lifecycle of displaying a dialog fragment. Will automatically close and nullify the reference + * if the bound lifecycle is destroyed, and handles repeat calls to show such that no more than one dialog is + * displayed. + */ +class DialogFragmentDisplayManager(private val builder: () -> DialogFragment) : DefaultLifecycleObserver { + + private var dialogFragment: DialogFragment? = null + + fun show(lifecycleOwner: LifecycleOwner, fragmentManager: FragmentManager, tag: String? = null) { + val fragment = dialogFragment ?: builder() + if (fragment.dialog?.isShowing != true) { + fragment.show(fragmentManager, tag) + dialogFragment = fragment + lifecycleOwner.lifecycle.addObserver(this) + } + } + + fun hide() { + dialogFragment?.dismissNow() + dialogFragment = null + } + + override fun onDestroy(owner: LifecycleOwner) { + owner.lifecycle.removeObserver(this) + hide() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ProgressCardDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/ProgressCardDialogFragment.kt new file mode 100644 index 0000000000..41a649a812 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ProgressCardDialogFragment.kt @@ -0,0 +1,20 @@ +package org.thoughtcrime.securesms.components + +import android.app.Dialog +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import androidx.fragment.app.DialogFragment +import org.thoughtcrime.securesms.R + +/** + * Displays a small progress spinner in a card view, as a non-cancellable dialog fragment. + */ +class ProgressCardDialogFragment : DialogFragment(R.layout.progress_card_dialog) { + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + isCancelable = false + return super.onCreateDialog(savedInstanceState).apply { + this.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/StoryDialogLauncherFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/StoryDialogLauncherFragment.kt index b8946dc4f2..bc5dff2097 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/StoryDialogLauncherFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/StoryDialogLauncherFragment.kt @@ -44,6 +44,33 @@ class StoryDialogLauncherFragment : DSLSettingsFragment(titleId = R.string.prefe } } ) + + clickPref( + title = DSLSettingsText.from(R.string.preferences__internal_turn_off_stories), + onClick = { + StoryDialogs.disableStories(requireContext(), false) { + Toast.makeText(requireContext(), R.string.preferences__internal_turn_off_stories, Toast.LENGTH_SHORT).show() + } + } + ) + + clickPref( + title = DSLSettingsText.from(R.string.preferences__internal_turn_off_stories_with_stories_on_disk), + onClick = { + StoryDialogs.disableStories(requireContext(), true) { + Toast.makeText(requireContext(), R.string.preferences__internal_turn_off_stories_with_stories_on_disk, Toast.LENGTH_SHORT).show() + } + } + ) + + clickPref( + title = DSLSettingsText.from(R.string.preferences__internal_delete_private_story), + onClick = { + StoryDialogs.deleteDistributionList(requireContext(), "Family") { + Toast.makeText(requireContext(), R.string.preferences__internal_delete_private_story, Toast.LENGTH_SHORT).show() + } + } + ) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryDialogs.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryDialogs.kt index db1c781a55..37fc18de32 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryDialogs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryDialogs.kt @@ -10,6 +10,38 @@ import org.thoughtcrime.securesms.R object StoryDialogs { + fun deleteDistributionList( + context: Context, + distributionListName: String, + onDelete: () -> Unit + ) { + MaterialAlertDialogBuilder(context) + .setTitle(R.string.StoryDialogs__delete_private_story) + .setMessage(context.getString(R.string.StoryDialogs__s_and_updates_shared, distributionListName)) + .setPositiveButton(R.string.StoryDialogs__delete) { _, _ -> onDelete() } + .setNegativeButton(android.R.string.cancel) { _, _ -> } + .show() + } + + fun disableStories( + context: Context, + userHasStories: Boolean, + onDisable: () -> Unit + ) { + val positiveButtonMessage = if (userHasStories) { + R.string.StoryDialogs__turn_off_and_delete + } else { + R.string.StoriesPrivacySettingsFragment__turn_off_stories + } + + MaterialAlertDialogBuilder(context) + .setTitle(R.string.StoriesPrivacySettingsFragment__turn_off_stories_question) + .setMessage(R.string.StoriesPrivacySettingsFragment__you_will_no_longer_be_able_to_share) + .setPositiveButton(positiveButtonMessage) { _, _ -> onDisable() } + .setNegativeButton(android.R.string.cancel) { _, _ -> } + .show() + } + fun resendStory(context: Context, onDismiss: () -> Unit = {}, resend: () -> Unit) { MaterialAlertDialogBuilder(context) .setMessage(R.string.StoryDialogs__story_could_not_be_sent) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsFragment.kt index 89ae68b789..c4aea452bc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsFragment.kt @@ -10,6 +10,8 @@ import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.findNavController import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.DialogFragmentDisplayManager +import org.thoughtcrime.securesms.components.ProgressCardDialogFragment import org.thoughtcrime.securesms.components.WrapperDialogFragment import org.thoughtcrime.securesms.components.settings.DSLConfiguration import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment @@ -17,6 +19,7 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.database.model.DistributionListId import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.stories.dialogs.StoryDialogs import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter import org.thoughtcrime.securesms.util.fragments.findListener @@ -28,6 +31,8 @@ class PrivateStorySettingsFragment : DSLSettingsFragment( menuId = R.menu.story_private_menu ) { + private val progressDisplayManager = DialogFragmentDisplayManager { ProgressCardDialogFragment() } + private val viewModel: PrivateStorySettingsViewModel by viewModels( factoryProducer = { PrivateStorySettingsViewModel.Factory(PrivateStorySettingsFragmentArgs.fromBundle(requireArguments()).distributionListId, PrivateStorySettingsRepository()) @@ -49,6 +54,12 @@ class PrivateStorySettingsFragment : DSLSettingsFragment( val toolbar: Toolbar = requireView().findViewById(R.id.toolbar) viewModel.state.observe(viewLifecycleOwner) { state -> + if (state.isActionInProgress) { + progressDisplayManager.show(viewLifecycleOwner, childFragmentManager) + } else { + progressDisplayManager.hide() + } + toolbar.title = state.privateStory?.name adapter.submitList(getConfiguration(state).toMappingModelList()) } @@ -88,7 +99,8 @@ class PrivateStorySettingsFragment : DSLSettingsFragment( clickPref( title = DSLSettingsText.from(R.string.PrivateStorySettingsFragment__delete_private_story, DSLSettingsText.ColorModifier(ContextCompat.getColor(requireContext(), R.color.signal_alert_primary))), onClick = { - handleDeletePrivateStory() + val privateStoryName = viewModel.state.value?.privateStory?.name + handleDeletePrivateStory(privateStoryName) } ) } @@ -113,13 +125,12 @@ class PrivateStorySettingsFragment : DSLSettingsFragment( .show() } - private fun handleDeletePrivateStory() { - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.PrivateStorySettingsFragment__are_you_sure) - .setMessage(R.string.PrivateStorySettingsFragment__this_action_cannot) - .setNegativeButton(android.R.string.cancel) { _, _ -> } - .setPositiveButton(R.string.delete) { _, _ -> viewModel.delete().subscribe { findNavController().popBackStack() } } - .show() + private fun handleDeletePrivateStory(privateStoryName: String?) { + val name = privateStoryName ?: return + + StoryDialogs.deleteDistributionList(requireContext(), name) { + viewModel.delete().subscribe { findNavController().popBackStack() } + } } override fun onToolbarNavigationClicked() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsRepository.kt index 9a1c5037e6..3d6a9835cd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsRepository.kt @@ -7,6 +7,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.DistributionListId import org.thoughtcrime.securesms.database.model.DistributionListRecord import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.sms.MessageSender import org.thoughtcrime.securesms.stories.Stories class PrivateStorySettingsRepository { @@ -27,6 +28,13 @@ class PrivateStorySettingsRepository { return Completable.fromAction { SignalDatabase.distributionLists.deleteList(distributionListId) Stories.onStorySettingsChanged(distributionListId) + + val recipientId = SignalDatabase.recipients.getOrInsertFromDistributionListId(distributionListId) + SignalDatabase.mms.getAllStoriesFor(recipientId, -1).use { reader -> + for (record in reader) { + MessageSender.sendRemoteDelete(record.id, record.isMms) + } + } }.subscribeOn(Schedulers.io()) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsState.kt index b89926b7a6..44d727242f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsState.kt @@ -4,5 +4,6 @@ import org.thoughtcrime.securesms.database.model.DistributionListRecord data class PrivateStorySettingsState( val privateStory: DistributionListRecord? = null, - val areRepliesAndReactionsEnabled: Boolean = false + val areRepliesAndReactionsEnabled: Boolean = false, + val isActionInProgress: Boolean = false ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsViewModel.kt index c4197bf99c..93e4acbbd1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsViewModel.kt @@ -52,7 +52,9 @@ class PrivateStorySettingsViewModel(private val distributionListId: Distribution } fun delete(): Completable { - return repository.delete(distributionListId).observeOn(AndroidSchedulers.mainThread()) + return repository.delete(distributionListId) + .doOnSubscribe { store.update { it.copy(isActionInProgress = true) } } + .observeOn(AndroidSchedulers.mainThread()) } class Factory(private val privateStoryItemData: DistributionListId, private val repository: PrivateStorySettingsRepository) : ViewModelProvider.Factory { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsFragment.kt index 3070657d06..388793e2a4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsFragment.kt @@ -4,9 +4,10 @@ import androidx.core.content.ContextCompat import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.ConcatAdapter -import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.signal.core.util.dp import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.DialogFragmentDisplayManager +import org.thoughtcrime.securesms.components.ProgressCardDialogFragment import org.thoughtcrime.securesms.components.settings.DSLConfiguration import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment @@ -17,6 +18,7 @@ import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey import org.thoughtcrime.securesms.groups.ParcelableGroupId import org.thoughtcrime.securesms.mediasend.v2.stories.ChooseGroupStoryBottomSheet import org.thoughtcrime.securesms.mediasend.v2.stories.ChooseStoryTypeBottomSheet +import org.thoughtcrime.securesms.stories.dialogs.StoryDialogs import org.thoughtcrime.securesms.stories.settings.create.CreateStoryFlowDialogFragment import org.thoughtcrime.securesms.stories.settings.create.CreateStoryWithViewersFragment import org.thoughtcrime.securesms.util.BottomSheetUtil @@ -36,6 +38,7 @@ class StoriesPrivacySettingsFragment : private val viewModel: StoriesPrivacySettingsViewModel by viewModels() private val lifecycleDisposable = LifecycleDisposable() + private val progressDisplayManager = DialogFragmentDisplayManager { ProgressCardDialogFragment() } override fun createAdapters(): Array { return arrayOf(DSLSettingsAdapter(), PagingMappingAdapter(), DSLSettingsAdapter()) @@ -84,6 +87,12 @@ class StoriesPrivacySettingsFragment : } lifecycleDisposable += viewModel.state.subscribe { state -> + if (state.isUpdatingEnabledState) { + progressDisplayManager.show(viewLifecycleOwner, childFragmentManager) + } else { + progressDisplayManager.hide() + } + (top as MappingAdapter).submitList(getTopConfiguration(state).toMappingModelList()) middle.submitList(getMiddleConfiguration(state).toMappingModelList()) (bottom as MappingAdapter).submitList(getBottomConfiguration(state).toMappingModelList()) @@ -144,12 +153,9 @@ class StoriesPrivacySettingsFragment : DSLSettingsText.ColorModifier(ContextCompat.getColor(requireContext(), R.color.signal_colorOnSurfaceVariant)) ), onClick = { - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.StoriesPrivacySettingsFragment__turn_off_stories_question) - .setMessage(R.string.StoriesPrivacySettingsFragment__you_will_no_longer_be_able_to) - .setPositiveButton(R.string.StoriesPrivacySettingsFragment__turn_off_stories) { _, _ -> viewModel.setStoriesEnabled(false) } - .setNegativeButton(android.R.string.cancel) { _, _ -> } - .show() + StoryDialogs.disableStories(requireContext(), viewModel.userHasActiveStories) { + viewModel.setStoriesEnabled(false) + } } ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsRepository.kt index ca97f0616a..7e859cd6ae 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsRepository.kt @@ -1,12 +1,14 @@ package org.thoughtcrime.securesms.stories.settings.story import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.schedulers.Schedulers import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.sms.MessageSender import org.thoughtcrime.securesms.storage.StorageSyncHelper import org.thoughtcrime.securesms.stories.Stories @@ -23,6 +25,20 @@ class StoriesPrivacySettingsRepository { return Completable.fromAction { SignalStore.storyValues().isFeatureDisabled = !isEnabled Stories.onStorySettingsChanged(Recipient.self().id) + + SignalDatabase.mms.getAllOutgoingStories(false, -1).use { reader -> + reader.map { record -> record.id } + }.forEach { messageId -> + MessageSender.sendRemoteDelete(messageId, true) + } + }.subscribeOn(Schedulers.io()) + } + + fun userHasOutgoingStories(): Single { + return Single.fromCallable { + SignalDatabase.mms.getAllOutgoingStories(false, -1).use { + it.iterator().hasNext() + } }.subscribeOn(Schedulers.io()) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsState.kt index bc1e0b3b47..74107df974 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsState.kt @@ -5,5 +5,6 @@ import org.thoughtcrime.securesms.contacts.paged.ContactSearchData data class StoriesPrivacySettingsState( val areStoriesEnabled: Boolean, val isUpdatingEnabledState: Boolean = false, - val storyContactItems: List = emptyList() + val storyContactItems: List = emptyList(), + val userHasStories: Boolean = false ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsViewModel.kt index f326015845..be46426b1b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsViewModel.kt @@ -39,6 +39,7 @@ class StoriesPrivacySettingsViewModel : ViewModel() { private val headerActionRequestSubject = PublishSubject.create() val state: Flowable = store.stateFlowable.observeOn(AndroidSchedulers.mainThread()) + val userHasActiveStories: Boolean get() = store.state.userHasStories val pagingController = ProxyPagingController() val headerActionRequests: Observable = headerActionRequestSubject.debounce(100, TimeUnit.MILLISECONDS) @@ -59,6 +60,8 @@ class StoriesPrivacySettingsViewModel : ViewModel() { pagingController.set(observablePagedData.controller) + updateUserHasStories() + disposables += store.update(observablePagedData.data.toFlowable(BackpressureStrategy.LATEST)) { data, state -> state.copy(storyContactItems = data) } @@ -78,6 +81,7 @@ class StoriesPrivacySettingsViewModel : ViewModel() { areStoriesEnabled = Stories.isFeatureEnabled() ) } + updateUserHasStories() } } @@ -86,4 +90,10 @@ class StoriesPrivacySettingsViewModel : ViewModel() { pagingController.onDataInvalidated() } } + + private fun updateUserHasStories() { + disposables += repository.userHasOutgoingStories().subscribe { userHasActiveStories -> + store.update { it.copy(userHasStories = userHasActiveStories) } + } + } } diff --git a/app/src/main/res/layout/progress_card.xml b/app/src/main/res/layout/progress_card.xml index 0bd315bb58..413df39505 100644 --- a/app/src/main/res/layout/progress_card.xml +++ b/app/src/main/res/layout/progress_card.xml @@ -12,5 +12,6 @@ android:layout_gravity="center" android:layout_margin="24dp" android:indeterminate="true" + android:background="@color/transparent" app:indicatorColor="@color/signal_colorPrimary" /> diff --git a/app/src/main/res/layout/progress_card_dialog.xml b/app/src/main/res/layout/progress_card_dialog.xml new file mode 100644 index 0000000000..2130169a6b --- /dev/null +++ b/app/src/main/res/layout/progress_card_dialog.xml @@ -0,0 +1,12 @@ + + + + + + \ 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 d2038bcc66..32443b6da2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2753,6 +2753,9 @@ Customize option + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher @@ -4936,6 +4939,12 @@ Only share with… Done + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete Add to story? @@ -4948,6 +4957,8 @@ Story could not be sent. Check your connection and try again. Send + + Turn off and delete Share & View Stories @@ -5231,7 +5242,7 @@ Turn off stories? - You will no longer be able to share or view stories. Any stories you have recently sent will still be visible by others until they expire. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Story privacy From 2edb9eeb52614127b1b3c18027101a65529d7835 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Wed, 5 Oct 2022 16:11:51 -0300 Subject: [PATCH 19/78] Add stories beta dialog. --- .../contacts/paged/ContactSearchMediator.kt | 12 ++++++++++++ .../thoughtcrime/securesms/keyvalue/StoryValues.kt | 10 +++++++++- .../securesms/stories/dialogs/StoryDialogs.kt | 7 +++++++ app/src/main/res/values/strings.xml | 4 ++++ 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchMediator.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchMediator.kt index 16e340da5c..b9cdfe960a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchMediator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchMediator.kt @@ -12,6 +12,7 @@ import io.reactivex.rxjava3.core.Observable import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.groups.SelectionLimits import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.stories.dialogs.StoryDialogs import org.thoughtcrime.securesms.stories.settings.custom.PrivateStorySettingsFragment import org.thoughtcrime.securesms.stories.settings.my.MyStorySettingsFragment import org.thoughtcrime.securesms.stories.settings.privacy.ChooseInitialMyStoryMembershipBottomSheetDialogFragment @@ -97,6 +98,17 @@ class ContactSearchMediator( } private fun toggleStorySelection(view: View, contactSearchData: ContactSearchData.Story, isSelected: Boolean) { + if (SignalStore.storyValues().userHasSeenBetaDialog) { + performStoryToggle(view, contactSearchData, isSelected) + } else { + StoryDialogs.displayBetaDialog(view.context) { + SignalStore.storyValues().userHasSeenBetaDialog = true + performStoryToggle(view, contactSearchData, isSelected) + } + } + } + + private fun performStoryToggle(view: View, contactSearchData: ContactSearchData.Story, isSelected: Boolean) { if (contactSearchData.recipient.isMyStory && !SignalStore.storyValues().userHasBeenNotifiedAboutStories) { ChooseInitialMyStoryMembershipBottomSheetDialogFragment.show(fragment.childFragmentManager) } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StoryValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StoryValues.kt index 9d55e9a460..072e2351ff 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StoryValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StoryValues.kt @@ -39,6 +39,11 @@ internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) { * Marks whether the user has seen the onboarding story */ private const val USER_HAS_SEEN_ONBOARDING_STORY = "stories.user.has.seen.onboarding" + + /** + * Marks whether the user has seen the beta dialog + */ + private const val USER_HAS_SEEN_BETA_DIALOG = "stories.user.has.seen.beta.dialog" } override fun onFirstEverAppLaunch() = Unit @@ -47,7 +52,8 @@ internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) { MANUAL_FEATURE_DISABLE, USER_HAS_ADDED_TO_A_STORY, USER_HAS_SEEN_FIRST_NAV_VIEW, - HAS_DOWNLOADED_ONBOARDING_STORY + HAS_DOWNLOADED_ONBOARDING_STORY, + USER_HAS_SEEN_BETA_DIALOG ) var isFeatureDisabled: Boolean by booleanValue(MANUAL_FEATURE_DISABLE, false) @@ -62,6 +68,8 @@ internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) { var userHasSeenOnboardingStory: Boolean by booleanValue(USER_HAS_SEEN_ONBOARDING_STORY, false) + var userHasSeenBetaDialog: Boolean by booleanValue(USER_HAS_SEEN_BETA_DIALOG, false) + fun setLatestStorySend(storySend: StorySend) { synchronized(this) { val storySends: List = getList(LATEST_STORY_SENDS, StorySendSerializer) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryDialogs.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryDialogs.kt index 37fc18de32..73843a2878 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryDialogs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryDialogs.kt @@ -38,6 +38,13 @@ object StoryDialogs { .setTitle(R.string.StoriesPrivacySettingsFragment__turn_off_stories_question) .setMessage(R.string.StoriesPrivacySettingsFragment__you_will_no_longer_be_able_to_share) .setPositiveButton(positiveButtonMessage) { _, _ -> onDisable() } + } + + fun displayBetaDialog(context: Context, onConfirmed: () -> Unit) { + MaterialAlertDialogBuilder(context) + .setTitle(R.string.StoryDialogs__stories_is_available_to) + .setMessage(R.string.StoryDialogs__if_you_share_a_story) + .setPositiveButton(R.string.Permissions_continue) { _, _ -> onConfirmed() } .setNegativeButton(android.R.string.cancel) { _, _ -> } .show() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 32443b6da2..4ac9a779cc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4945,6 +4945,10 @@ \"%1$s\" and updates shared to this story will be deleted. Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Add to story? From a9a64a3f60dc719532f689e4c0650a465f3635c8 Mon Sep 17 00:00:00 2001 From: Nicholas Date: Wed, 5 Oct 2022 16:10:28 -0400 Subject: [PATCH 20/78] Update MediaPreviewV2 to use thumbnail rail & menu items. --- .../mediapreview/MediaPreviewV2Activity.kt | 9 + .../mediapreview/MediaPreviewV2Fragment.kt | 274 +++++++++++++++++- .../mediapreview/MediaPreviewV2State.kt | 2 +- .../mediapreview/MediaPreviewV2ViewModel.kt | 18 +- .../res/layout/activity_mediapreview_v2.xml | 3 +- .../res/layout/fragment_media_preview_v2.xml | 76 ++++- app/src/main/res/values/strings.xml | 1 + 7 files changed, 359 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Activity.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Activity.kt index cc1f34a0c5..3c7640a18d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Activity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Activity.kt @@ -1,13 +1,22 @@ package org.thoughtcrime.securesms.mediapreview +import android.content.Context import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.app.AppCompatDelegate import androidx.fragment.app.commit import org.thoughtcrime.securesms.R class MediaPreviewV2Activity : AppCompatActivity(R.layout.activity_mediapreview_v2) { + + override fun attachBaseContext(newBase: Context) { + delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES + super.attachBaseContext(newBase) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + setTheme(R.style.TextSecure_MediaPreview) if (savedInstanceState == null) { val bundle = Bundle() val args = MediaIntentFactory.requireArguments(intent.extras!!) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt index e89cc7c77e..6de23e6eb9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt @@ -1,23 +1,52 @@ package org.thoughtcrime.securesms.mediapreview +import android.Manifest +import android.content.ActivityNotFoundException import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.os.Build import android.os.Bundle +import android.view.Menu import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.MarginLayoutParams +import android.widget.Toast +import androidx.core.app.ShareCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback +import com.google.android.material.appbar.MaterialToolbar +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import org.signal.core.util.concurrent.SignalExecutors import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.animation.DepthPageTransformer +import org.thoughtcrime.securesms.attachments.DatabaseAttachment import org.thoughtcrime.securesms.components.ViewBinderDelegate +import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment +import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs import org.thoughtcrime.securesms.database.MediaDatabase import org.thoughtcrime.securesms.databinding.FragmentMediaPreviewV2Binding +import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity +import org.thoughtcrime.securesms.mediasend.Media +import org.thoughtcrime.securesms.mms.GlideApp +import org.thoughtcrime.securesms.mms.PartAuthority +import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.FullscreenHelper import org.thoughtcrime.securesms.util.LifecycleDisposable +import org.thoughtcrime.securesms.util.MediaUtil +import org.thoughtcrime.securesms.util.SaveAttachmentTask +import org.thoughtcrime.securesms.util.StorageUtil import java.util.Locale +import java.util.Optional class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), MediaPreviewFragment.Events { private val TAG = Log.tag(MediaPreviewV2Fragment::class.java) @@ -35,7 +64,11 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - initializeViewModel() + + val args = MediaIntentFactory.requireArguments(requireArguments()) + + initializeViewModel(args) + initializeToolbar(binding.toolbar, args) binding.mediaPager.offscreenPageLimit = 1 binding.mediaPager.setPageTransformer(DepthPageTransformer()) val adapter = MediaPreviewV2Adapter(this) @@ -47,18 +80,59 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med } }) initializeFullScreenUi() + initializeAlbumRail() + anchorMarginsToBottomInsets(binding.mediaPreviewDetailsContainer) lifecycleDisposable += viewModel.state.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread()).subscribe { bindCurrentState(it) } } + private fun initializeToolbar(toolbar: MaterialToolbar, args: MediaIntentFactory.MediaPreviewArgs) { + toolbar.setNavigationOnClickListener { + requireActivity().onBackPressed() + } + + binding.toolbar.inflateMenu(R.menu.media_preview) + + // Restricted to API26 because of MemoryFileUtil not supporting lower API levels well + binding.toolbar.menu.findItem(R.id.media_preview__share).isVisible = Build.VERSION.SDK_INT >= 26 + + if (args.hideAllMedia) { + binding.toolbar.menu.findItem(R.id.media_preview__overview).isVisible = false + } + } + + private fun initializeAlbumRail() { + binding.mediaPreviewAlbumRail.itemAnimator = null // Or can crash when set to INVISIBLE while animating by FullscreenHelper https://issuetracker.google.com/issues/148720682 + binding.mediaPreviewAlbumRail.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false) + binding.mediaPreviewAlbumRail.adapter = MediaRailAdapter( + GlideApp.with(this), + object : MediaRailAdapter.RailItemListener { + override fun onRailItemClicked(distanceFromActive: Int) { + binding.mediaPager.currentItem += distanceFromActive + } + + override fun onRailItemDeleteClicked(distanceFromActive: Int) { + throw UnsupportedOperationException("Callback unsupported.") + } + }, + false + ) + } + private fun initializeFullScreenUi() { fullscreenHelper.configureToolbarLayout(binding.toolbarCutoutSpacer, binding.toolbar) - fullscreenHelper.hideSystemUI() + fullscreenHelper.showAndHideWithSystemUI(requireActivity().window, binding.toolbarLayout, binding.mediaPreviewDetailsContainer) } - private fun initializeViewModel() { - val args = MediaIntentFactory.requireArguments(requireArguments()) + private fun initializeViewModel(args: MediaIntentFactory.MediaPreviewArgs) { + if (!MediaUtil.isImageType(args.initialMediaType) && !MediaUtil.isVideoType(args.initialMediaType)) { + Log.w(TAG, "Unsupported media type sent to MediaPreviewV2Fragment, finishing.") + Snackbar.make(binding.root, R.string.MediaPreviewActivity_unssuported_media_type, Snackbar.LENGTH_LONG) + .setAction(R.string.MediaPreviewActivity_dismiss_due_to_error) { + requireActivity().finish() + }.show() + } viewModel.setShowThread(args.showThread) val sorting = MediaDatabase.Sorting.values()[args.sorting] viewModel.fetchAttachments(args.initialMediaUri, args.threadId, sorting) @@ -67,7 +141,11 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med private fun bindCurrentState(currentState: MediaPreviewV2State) { when (currentState.loadState) { MediaPreviewV2State.LoadState.READY -> bindReadyState(currentState) - // INIT, else -> no-op + MediaPreviewV2State.LoadState.LOADED -> { + bindReadyState(currentState) + bindLoadedState(currentState) + } + else -> null } } @@ -76,6 +154,58 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med val currentItem: MediaDatabase.MediaRecord = currentState.mediaRecords[currentState.position] binding.toolbar.title = getTitleText(currentItem, currentState.showThread) binding.toolbar.subtitle = getSubTitleText(currentItem) + + val menu: Menu = binding.toolbar.menu + if (currentItem.threadId == MediaIntentFactory.NOT_IN_A_THREAD.toLong()) { + menu.findItem(R.id.media_preview__overview).isVisible = false + menu.findItem(R.id.delete).isVisible = false + } + + binding.toolbar.setOnMenuItemClickListener { + when (it.itemId) { + R.id.media_preview__overview -> showOverview(currentItem.threadId) + R.id.media_preview__forward -> forward(currentItem) + R.id.media_preview__share -> share(currentItem) + R.id.save -> saveToDisk(currentItem) + R.id.delete -> deleteMedia(currentItem) + android.R.id.home -> requireActivity().finish() + else -> return@setOnMenuItemClickListener false + } + return@setOnMenuItemClickListener true + } + } + + /** + * These are binding steps that need a reference to the actual fragment within the pager. + * This is not available until after a page has been chosen by the ViewPager, and we receive the + * {@link OnPageChangeCallback}. + */ + private fun bindLoadedState(currentState: MediaPreviewV2State) { + val currentItem: MediaDatabase.MediaRecord = currentState.mediaRecords[currentState.position] + val currentFragment: Fragment? = childFragmentManager.findFragmentByTag("f${currentState.position}") + val playbackControls = (currentFragment as? MediaPreviewFragment)?.playbackControls + val albumThumbnailMedia = currentState.mediaRecords.map { it.toMedia() } + val caption = currentItem.attachment?.caption + if (albumThumbnailMedia.isEmpty() && caption == null && playbackControls == null) { + binding.mediaPreviewDetailsContainer.visibility = View.GONE + } else { + binding.mediaPreviewDetailsContainer.visibility = View.VISIBLE + } + binding.mediaPreviewAlbumRail.visibility = if (albumThumbnailMedia.isEmpty()) View.GONE else View.VISIBLE + (binding.mediaPreviewAlbumRail.adapter as MediaRailAdapter).setMedia(albumThumbnailMedia, currentState.position) + binding.mediaPreviewAlbumRail.smoothScrollToPosition(currentState.position) + + binding.mediaPreviewCaptionContainer.visibility = if (caption == null) View.GONE else View.VISIBLE + binding.mediaPreviewCaption.text = caption + + if (playbackControls != null) { + val params = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + playbackControls.layoutParams = params + binding.mediaPreviewPlaybackControlsContainer.removeAllViews() + binding.mediaPreviewPlaybackControlsContainer.addView(playbackControls) + } else { + binding.mediaPreviewPlaybackControlsContainer.removeAllViews() + } } private fun getTitleText(mediaRecord: MediaDatabase.MediaRecord, showThread: Boolean): String { @@ -112,20 +242,148 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med getString(R.string.MediaPreviewActivity_draft) } + private fun anchorMarginsToBottomInsets(viewToAnchor: View) { + ViewCompat.setOnApplyWindowInsetsListener(viewToAnchor) { view: View, windowInsetsCompat: WindowInsetsCompat -> + val layoutParams = view.layoutParams as MarginLayoutParams + val systemBarInsets = windowInsetsCompat.getInsets(WindowInsetsCompat.Type.systemBars()) + layoutParams.setMargins( + systemBarInsets.left, + layoutParams.topMargin, + systemBarInsets.right, + systemBarInsets.bottom + ) + view.layoutParams = layoutParams + windowInsetsCompat + } + } + + private fun MediaDatabase.MediaRecord.toMedia(): Media? { + val attachment = this.attachment + val uri = attachment?.uri + if (attachment == null || uri == null) { + return null + } + + return Media( + uri, + this.contentType, + this.date, + attachment.width, + attachment.height, + attachment.size, + 0, + attachment.isBorderless, + attachment.isVideoGif, + Optional.empty(), + Optional.ofNullable(attachment.caption), + Optional.empty() + ) + } + override fun singleTapOnMedia(): Boolean { - Log.d(TAG, "singleTapOnMedia()") + fullscreenHelper.toggleUiVisibility() return true } override fun mediaNotAvailable() { - Log.d(TAG, "mediaNotAvailable()") + Snackbar.make(binding.root, R.string.MediaPreviewActivity_media_no_longer_available, Snackbar.LENGTH_LONG) + .setAction(R.string.MediaPreviewActivity_dismiss_due_to_error) { + requireActivity().finish() + }.show() } override fun onMediaReady() { Log.d(TAG, "onMediaReady()") } + private fun showOverview(threadId: Long) { + val context = requireContext() + context.startActivity(MediaOverviewActivity.forThread(context, threadId)) + } + + private fun forward(mediaItem: MediaDatabase.MediaRecord) { + val attachment = mediaItem.attachment + val uri = attachment?.uri + if (attachment != null && uri != null) { + MultiselectForwardFragmentArgs.create( + requireContext(), + mediaItem.threadId, + uri, + attachment.contentType + ) { args: MultiselectForwardFragmentArgs -> + MultiselectForwardFragment.showBottomSheet(childFragmentManager, args) + } + } + } + + private fun share(mediaItem: MediaDatabase.MediaRecord) { + val attachment = mediaItem.attachment + val uri = attachment?.uri + if (attachment != null && uri != null) { + val publicUri = PartAuthority.getAttachmentPublicUri(uri) + val mimeType = Intent.normalizeMimeType(attachment.contentType) + val shareIntent = ShareCompat.IntentBuilder(requireActivity()) + .setStream(publicUri) + .setType(mimeType) + .createChooserIntent() + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + + try { + startActivity(shareIntent) + } catch (e: ActivityNotFoundException) { + Log.w(TAG, "No activity existed to share the media.", e) + Toast.makeText(requireContext(), R.string.MediaPreviewActivity_cant_find_an_app_able_to_share_this_media, Toast.LENGTH_LONG).show() + } + } + } + + private fun saveToDisk(mediaItem: MediaDatabase.MediaRecord) { + SaveAttachmentTask.showWarningDialog(requireContext()) { _: DialogInterface?, _: Int -> + if (StorageUtil.canWriteToMediaStore()) { + performSaveToDisk(mediaItem) + return@showWarningDialog + } + Permissions.with(this) + .request(Manifest.permission.WRITE_EXTERNAL_STORAGE) + .ifNecessary() + .withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied)) + .onAnyDenied { Toast.makeText(requireContext(), R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show() } + .onAllGranted { performSaveToDisk(mediaItem) } + .execute() + } + } + + fun performSaveToDisk(mediaItem: MediaDatabase.MediaRecord) { + val saveTask = SaveAttachmentTask(requireContext()) + val saveDate = if (mediaItem.date > 0) mediaItem.date else System.currentTimeMillis() + val attachment = mediaItem.attachment + val uri = attachment?.uri + if (attachment != null && uri != null) { + saveTask.executeOnExecutor(SignalExecutors.BOUNDED_IO, SaveAttachmentTask.Attachment(uri, attachment.contentType, saveDate, null)) + } + } + + private fun deleteMedia(mediaItem: MediaDatabase.MediaRecord) { + val attachment: DatabaseAttachment = mediaItem.attachment ?: return + + MaterialAlertDialogBuilder(requireContext()) + .setIcon(R.drawable.ic_warning) + .setTitle(R.string.MediaPreviewActivity_media_delete_confirmation_title) + .setMessage(R.string.MediaPreviewActivity_media_delete_confirmation_message) + .setCancelable(true) + .setPositiveButton(R.string.delete) { _, _ -> + viewModel.deleteItem(requireContext(), attachment, onSuccess = { + requireActivity().finish() + }, onError = { + Log.e(TAG, "Delete failed!", it) + requireActivity().finish() + }) + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + companion object { - val ARGS_KEY: String = "args" + const val ARGS_KEY: String = "args" } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2State.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2State.kt index 062468d932..bf91f819d0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2State.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2State.kt @@ -8,5 +8,5 @@ data class MediaPreviewV2State( val position: Int = 0, val showThread: Boolean = false ) { - enum class LoadState { INIT, READY, } + enum class LoadState { INIT, READY, LOADED } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2ViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2ViewModel.kt index d5cbe58280..9f6e776ea5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2ViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2ViewModel.kt @@ -1,13 +1,19 @@ package org.thoughtcrime.securesms.mediapreview +import android.content.Context import android.net.Uri import androidx.lifecycle.ViewModel import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.functions.Consumer import io.reactivex.rxjava3.kotlin.plusAssign +import io.reactivex.rxjava3.schedulers.Schedulers import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.attachments.DatabaseAttachment import org.thoughtcrime.securesms.database.MediaDatabase +import org.thoughtcrime.securesms.util.AttachmentUtil import org.thoughtcrime.securesms.util.rx.RxStore class MediaPreviewV2ViewModel : ViewModel() { @@ -19,7 +25,8 @@ class MediaPreviewV2ViewModel : ViewModel() { val state: Flowable = store.stateFlowable.observeOn(AndroidSchedulers.mainThread()) fun fetchAttachments(startingUri: Uri, threadId: Long, sorting: MediaDatabase.Sorting) { - disposables += store.update(repository.getAttachments(startingUri, threadId, sorting)) { mediaRecords: List, oldState: MediaPreviewV2State -> + disposables += store.update(repository.getAttachments(startingUri, threadId, sorting)) { + mediaRecords: List, oldState: MediaPreviewV2State -> oldState.copy( mediaRecords = mediaRecords, loadState = MediaPreviewV2State.LoadState.READY @@ -35,10 +42,17 @@ class MediaPreviewV2ViewModel : ViewModel() { fun setCurrentPage(position: Int) { store.update { oldState -> - oldState.copy(position = position) + oldState.copy(position = position, loadState = MediaPreviewV2State.LoadState.LOADED) } } + fun deleteItem(context: Context, attachment: DatabaseAttachment, onSuccess: Consumer, onError: Consumer) { + disposables += Single.fromCallable { AttachmentUtil.deleteAttachment(context.applicationContext, attachment) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(onSuccess, onError) + } + override fun onCleared() { disposables.dispose() store.dispose() diff --git a/app/src/main/res/layout/activity_mediapreview_v2.xml b/app/src/main/res/layout/activity_mediapreview_v2.xml index b6ed0ae4a4..fa93ff97d4 100644 --- a/app/src/main/res/layout/activity_mediapreview_v2.xml +++ b/app/src/main/res/layout/activity_mediapreview_v2.xml @@ -3,4 +3,5 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/fragment_container_view" android:layout_width="match_parent" - android:layout_height="match_parent" /> \ No newline at end of file + android:layout_height="match_parent" + android:background="@color/signal_dark_colorNeutral"/> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_media_preview_v2.xml b/app/src/main/res/layout/fragment_media_preview_v2.xml index 6657aba085..83802f0b53 100644 --- a/app/src/main/res/layout/fragment_media_preview_v2.xml +++ b/app/src/main/res/layout/fragment_media_preview_v2.xml @@ -1,11 +1,67 @@ - + xmlns:tools="http://schemas.android.com/tools" + android:background="@color/signal_dark_colorNeutral"> + + + + + + + + + + + + + + - + android:background="@android:color/transparent" + app:navigationIcon="@drawable/ic_arrow_left_white_24" /> - - \ No newline at end of file + + \ 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 4ac9a779cc..c6699d48b8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1965,6 +1965,7 @@ %1$s to you Media no longer available. Can\'t find an app able to share this media. + Close %1$d new messages in %2$d conversations From 3895578d5188c7d2ef3378d61d7a9aa6efa03928 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 5 Oct 2022 16:25:33 -0400 Subject: [PATCH 21/78] Always use sealed sender when sending stories. --- .../AdvancedPrivacySettingsViewModel.kt | 2 +- .../crypto/UnidentifiedAccessUtil.java | 41 ++++-- .../dependencies/ApplicationDependencies.java | 2 +- .../ApplicationDependencyProvider.java | 7 +- .../jobs/CheckServiceReachabilityJob.kt | 4 +- .../securesms/jobs/PushDecryptMessageJob.java | 24 ++++ .../jobs/SenderKeyDistributionSendJob.java | 2 +- .../securesms/messages/GroupSendUtil.java | 26 ++-- .../securesms/messages/RestStrategy.java | 4 +- .../story/StoriesPrivacySettingsRepository.kt | 2 + .../securesms/util/FeatureFlags.java | 5 +- .../securesms/util/SignalProxyUtil.java | 4 +- .../api/SignalServiceMessageReceiver.java | 8 +- .../api/SignalServiceMessageSender.java | 126 +++++++++--------- .../api/services/MessagingService.java | 8 +- .../internal/push/PushServiceSocket.java | 18 +-- .../push/SendGroupMessageResponse.java | 5 +- .../websocket/WebSocketConnection.java | 18 ++- 18 files changed, 184 insertions(+), 122 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsViewModel.kt index 84c4f5566c..47c7b7b592 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsViewModel.kt @@ -77,7 +77,7 @@ class AdvancedPrivacySettingsViewModel( fun setCensorshipCircumventionEnabled(enabled: Boolean) { SignalStore.settings().setCensorshipCircumventionEnabled(enabled) SignalStore.misc().isServiceReachableWithoutCircumvention = false - ApplicationDependencies.resetNetworkConnectionsAfterProxyChange() + ApplicationDependencies.resetAllNetworkConnections() refresh() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java index 2e1bfeb1fb..a0a3a44578 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java @@ -17,6 +17,8 @@ import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.thoughtcrime.securesms.BuildConfig; +import org.thoughtcrime.securesms.database.RecipientDatabase; +import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode; import org.thoughtcrime.securesms.keyvalue.CertificateType; import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues; import org.thoughtcrime.securesms.keyvalue.SignalStore; @@ -68,8 +70,8 @@ public static List> getAccessFor(@NonNull Conte } @WorkerThread - public static Map> getAccessMapFor(@NonNull Context context, @NonNull List recipients) { - List> accessList = getAccessFor(context, recipients, true); + public static Map> getAccessMapFor(@NonNull Context context, @NonNull List recipients, boolean isForStory) { + List> accessList = getAccessFor(context, recipients, true, isForStory); Iterator recipientIterator = recipients.iterator(); Iterator> accessIterator = accessList.iterator(); @@ -82,9 +84,14 @@ public static Map> getAccessMapFor return accessMap; } - + @WorkerThread public static List> getAccessFor(@NonNull Context context, @NonNull List recipients, boolean log) { + return getAccessFor(context, recipients, false, log); + } + + @WorkerThread + public static List> getAccessFor(@NonNull Context context, @NonNull List recipients, boolean isForStory, boolean log) { byte[] ourUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey()); if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) { @@ -96,7 +103,7 @@ public static List> getAccessFor(@NonNull Conte Map typeCounts = new HashMap<>(); for (Recipient recipient : recipients) { - byte[] theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient); + byte[] theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient, isForStory); CertificateType certificateType = getUnidentifiedAccessCertificateType(recipient); byte[] ourUnidentifiedAccessCertificate = SignalStore.certificateValues().getUnidentifiedAccessCertificate(certificateType); @@ -168,28 +175,40 @@ private static byte[] getUnidentifiedAccessCertificate(@NonNull Recipient recipi .getUnidentifiedAccessCertificate(getUnidentifiedAccessCertificateType(recipient)); } - private static @Nullable byte[] getTargetUnidentifiedAccessKey(@NonNull Recipient recipient) { + private static @Nullable byte[] getTargetUnidentifiedAccessKey(@NonNull Recipient recipient, boolean isForStory) { ProfileKey theirProfileKey = ProfileKeyUtil.profileKeyOrNull(recipient.resolve().getProfileKey()); + byte[] accessKey; + switch (recipient.resolve().getUnidentifiedAccessMode()) { case UNKNOWN: if (theirProfileKey == null) { - return UNRESTRICTED_KEY; + accessKey = UNRESTRICTED_KEY; } else { - return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey); + accessKey = UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey); } + break; case DISABLED: - return null; + accessKey = null; + break; case ENABLED: if (theirProfileKey == null) { - return null; + accessKey = null; } else { - return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey); + accessKey = UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey); } + break; case UNRESTRICTED: - return UNRESTRICTED_KEY; + accessKey = UNRESTRICTED_KEY; + break; default: throw new AssertionError("Unknown mode: " + recipient.getUnidentifiedAccessMode().getMode()); } + + if (accessKey == null && isForStory) { + accessKey = UNRESTRICTED_KEY; + } + + return accessKey; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java index 99a9785bb9..b22849d965 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java @@ -258,7 +258,7 @@ public static void closeConnections() { } } - public static void resetNetworkConnectionsAfterProxyChange() { + public static void resetAllNetworkConnections() { synchronized (LOCK) { closeConnections(); if (signalWebSocket != null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java index 24511d0149..8a09899070 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java @@ -63,6 +63,7 @@ import org.thoughtcrime.securesms.service.TrimThreadsByDateManager; import org.thoughtcrime.securesms.service.webrtc.SignalCallManager; import org.thoughtcrime.securesms.shakereport.ShakeToReport; +import org.thoughtcrime.securesms.stories.Stories; import org.thoughtcrime.securesms.util.AlarmSleepTimer; import org.thoughtcrime.securesms.util.AppForegroundObserver; import org.thoughtcrime.securesms.util.ByteUnit; @@ -398,7 +399,8 @@ public WebSocketConnection createWebSocket() { signalServiceConfigurationSupplier.get(), Optional.of(new DynamicCredentialsProvider()), BuildConfig.SIGNAL_AGENT, - healthMonitor); + healthMonitor, + Stories.isFeatureEnabled()); } @Override @@ -407,7 +409,8 @@ public WebSocketConnection createUnidentifiedWebSocket() { signalServiceConfigurationSupplier.get(), Optional.empty(), BuildConfig.SIGNAL_AGENT, - healthMonitor); + healthMonitor, + Stories.isFeatureEnabled()); } }; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/CheckServiceReachabilityJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/CheckServiceReachabilityJob.kt index 90263cc145..eef5f39fd8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/CheckServiceReachabilityJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/CheckServiceReachabilityJob.kt @@ -7,6 +7,7 @@ import org.thoughtcrime.securesms.jobmanager.Data import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.stories.Stories import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider import org.whispersystems.signalservice.internal.websocket.WebSocketConnection @@ -78,7 +79,8 @@ class CheckServiceReachabilityJob private constructor(params: Parameters) : Base ), BuildConfig.SIGNAL_AGENT, null, - "" + "", + Stories.isFeatureEnabled() ) try { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java index a06d6bf78d..f387abb3f3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java @@ -102,6 +102,11 @@ public void onRun() throws RetryLaterException { List jobs = new LinkedList<>(); DecryptionResult result = MessageDecryptionUtil.decrypt(context, envelope); + if (result.getState() == MessageState.DECRYPTED_OK && envelope.isStory() && !isStoryMessage(result)) { + Log.w(TAG, "Envelope was flagged as a story, but it did not have any story-related content! Dropping."); + return; + } + if (result.getContent() != null) { if (result.getContent().getSenderKeyDistributionMessage().isPresent()) { handleSenderKeyDistributionMessage(result.getContent().getSender(), result.getContent().getSenderDevice(), result.getContent().getSenderKeyDistributionMessage().get()); @@ -172,6 +177,25 @@ private void handlePniSignatureMessage(@NonNull SignalServiceAddress address, in } } + private boolean isStoryMessage(@NonNull DecryptionResult result) { + if (result.getContent() == null) { + return false; + } + + if (result.getContent().getStoryMessage().isPresent()) { + return true; + } + + if (result.getContent().getDataMessage().isPresent() && + result.getContent().getDataMessage().get().getStoryContext().isPresent() && + result.getContent().getDataMessage().get().getGroupContext().isPresent()) + { + return true; + } + + return false; + } + private boolean needsMigration() { return TextSecurePreferences.getNeedsSqlCipherMigration(context); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SenderKeyDistributionSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SenderKeyDistributionSendJob.java index eaeea0af5c..c0fcaa03f9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SenderKeyDistributionSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SenderKeyDistributionSendJob.java @@ -120,7 +120,7 @@ protected void onRun() throws Exception { SenderKeyDistributionMessage message = messageSender.getOrCreateNewGroupSession(distributionId); List> access = UnidentifiedAccessUtil.getAccessFor(context, Collections.singletonList(targetRecipient)); - SendMessageResult result = messageSender.sendSenderKeyDistributionMessage(distributionId, address, access, message, Optional.ofNullable(groupId).map(GroupId::getDecodedId), false).get(0); + SendMessageResult result = messageSender.sendSenderKeyDistributionMessage(distributionId, address, access, message, Optional.ofNullable(groupId).map(GroupId::getDecodedId), false, false).get(0); if (result.isSuccess()) { List addresses = result.getSuccess() diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java b/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java index fbf9886ff6..95c27e41a7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java @@ -94,7 +94,7 @@ public static List sendResendableDataMessage(@NonNull Context boolean urgent) throws IOException, UntrustedIdentityException { - return sendMessage(context, groupId, getDistributionId(groupId), messageId, allTargets, isRecipientUpdate, DataSendOperation.resendable(message, contentHint, messageId, urgent), null); + return sendMessage(context, groupId, getDistributionId(groupId), messageId, allTargets, isRecipientUpdate, false, DataSendOperation.resendable(message, contentHint, messageId, urgent), null); } /** @@ -116,7 +116,7 @@ public static List sendUnresendableDataMessage(@NonNull Conte boolean urgent) throws IOException, UntrustedIdentityException { - return sendMessage(context, groupId, getDistributionId(groupId), null, allTargets, isRecipientUpdate, DataSendOperation.unresendable(message, contentHint, urgent), null); + return sendMessage(context, groupId, getDistributionId(groupId), null, allTargets, isRecipientUpdate, false, DataSendOperation.unresendable(message, contentHint, urgent), null); } /** @@ -133,7 +133,7 @@ public static List sendTypingMessage(@NonNull Context context @Nullable CancelationSignal cancelationSignal) throws IOException, UntrustedIdentityException { - return sendMessage(context, groupId, getDistributionId(groupId), null, allTargets, false, new TypingSendOperation(message), cancelationSignal); + return sendMessage(context, groupId, getDistributionId(groupId), null, allTargets, false, false, new TypingSendOperation(message), cancelationSignal); } /** @@ -149,11 +149,11 @@ public static List sendCallMessage(@NonNull Context context, @NonNull SignalServiceCallMessage message) throws IOException, UntrustedIdentityException { - return sendMessage(context, groupId, getDistributionId(groupId), null, allTargets, false, new CallSendOperation(message), null); + return sendMessage(context, groupId, getDistributionId(groupId), null, allTargets, false, false, new CallSendOperation(message), null); } /** - * Handles all of the logic of sending a story to a group. Will do sender key sends and legacy 1:1 sends as-needed, and give you back a list of + * Handles all of the logic of sending a story to a distribution list. Will do sender key sends and legacy 1:1 sends as-needed, and give you back a list of * {@link SendMessageResult}s just like we're used to. * * @param isRecipientUpdate True if you've already sent this message to some recipients in the past, otherwise false. @@ -175,6 +175,7 @@ public static List sendStoryMessage(@NonNull Context context, messageId, allTargets, isRecipientUpdate, + true, new StorySendOperation(messageId, null, sentTimestamp, message, manifest), null); } @@ -201,6 +202,7 @@ public static List sendGroupStoryMessage(@NonNull Context con messageId, allTargets, isRecipientUpdate, + true, new StorySendOperation(messageId, groupId, sentTimestamp, message, Collections.emptySet()), null); } @@ -219,6 +221,7 @@ private static List sendMessage(@NonNull Context context, @Nullable MessageId relatedMessageId, @NonNull List allTargets, boolean isRecipientUpdate, + boolean isStorySend, @NonNull SendOperation sendOperation, @Nullable CancelationSignal cancelationSignal) throws IOException, UntrustedIdentityException @@ -228,7 +231,7 @@ private static List sendMessage(@NonNull Context context, Set unregisteredTargets = allTargets.stream().filter(Recipient::isUnregistered).collect(Collectors.toSet()); List registeredTargets = allTargets.stream().filter(r -> !unregisteredTargets.contains(r)).collect(Collectors.toList()); - RecipientData recipients = new RecipientData(context, registeredTargets); + RecipientData recipients = new RecipientData(context, registeredTargets, isStorySend); Optional groupRecord = groupId != null ? SignalDatabase.groups().getGroup(groupId) : Optional.empty(); List senderKeyTargets = new LinkedList<>(); @@ -257,6 +260,11 @@ private static List sendMessage(@NonNull Context context, Log.i(TAG, "No DistributionId. Using legacy."); legacyTargets.addAll(senderKeyTargets); senderKeyTargets.clear(); + } else if (isStorySend) { + Log.i(TAG, "Sending a story. Using sender key for all " + allTargets.size() + " recipients."); + senderKeyTargets.clear(); + senderKeyTargets.addAll(registeredTargets); + legacyTargets.clear(); } else if (SignalStore.internalValues().removeSenderKeyMinimum()) { Log.i(TAG, "Sender key minimum removed. Using for " + senderKeyTargets.size() + " recipients."); } else if (senderKeyTargets.size() < 2) { @@ -681,7 +689,7 @@ public StorySendOperation(@NonNull MessageId relatedMessageId, @Nullable CancelationSignal cancelationSignal) throws IOException, UntrustedIdentityException { - return messageSender.sendStory(targets, access, isRecipientUpdate, message, getSentTimestamp(), manifest); + throw new UnsupportedOperationException("Stories can only be send via sender key!"); } @Override @@ -767,8 +775,8 @@ private static final class RecipientData { private final Map addressById; private final RecipientAccessList accessList; - RecipientData(@NonNull Context context, @NonNull List recipients) throws IOException { - this.accessById = UnidentifiedAccessUtil.getAccessMapFor(context, recipients); + RecipientData(@NonNull Context context, @NonNull List recipients, boolean isForStory) throws IOException { + this.accessById = UnidentifiedAccessUtil.getAccessMapFor(context, recipients, isForStory); this.addressById = mapAddresses(context, recipients); this.accessList = new RecipientAccessList(recipients); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/RestStrategy.java b/app/src/main/java/org/thoughtcrime/securesms/messages/RestStrategy.java index 99333ee060..ac5607c607 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/RestStrategy.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/RestStrategy.java @@ -10,6 +10,8 @@ import org.thoughtcrime.securesms.jobs.MarkerJob; import org.thoughtcrime.securesms.jobs.PushDecryptMessageJob; import org.thoughtcrime.securesms.jobs.PushProcessMessageJob; +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.stories.Stories; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import java.io.IOException; @@ -82,7 +84,7 @@ private static int enqueuePushDecryptJobs(IncomingMessageProcessor.Processor pro receiver.setSoTimeoutMillis(timeout); - receiver.retrieveMessages(envelope -> { + receiver.retrieveMessages(Stories.isFeatureEnabled(), envelope -> { Log.i(TAG, "Retrieved an envelope." + timeSuffix(startTime)); String jobId = processor.processEnvelope(envelope); diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsRepository.kt index 7e859cd6ae..de72a5a4c9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsRepository.kt @@ -5,6 +5,7 @@ import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.schedulers.Schedulers import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId @@ -25,6 +26,7 @@ class StoriesPrivacySettingsRepository { return Completable.fromAction { SignalStore.storyValues().isFeatureDisabled = !isEnabled Stories.onStorySettingsChanged(Recipient.self().id) + ApplicationDependencies.resetAllNetworkConnections() SignalDatabase.mms.getAllOutgoingStories(false, -1).use { reader -> reader.map { record -> record.id } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index 23559335b3..a9c1eed640 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -249,7 +249,10 @@ public final class FeatureFlags { */ private static final Map FLAG_CHANGE_LISTENERS = new HashMap() {{ put(MESSAGE_PROCESSOR_ALARM_INTERVAL, change -> MessageProcessReceiver.startOrUpdateAlarm(ApplicationDependencies.getApplication())); - put(STORIES, change -> ApplicationDependencies.getJobManager().startChain(new RefreshAttributesJob()).then(new RefreshOwnProfileJob()).enqueue()); + put(STORIES, change -> { + ApplicationDependencies.getJobManager().startChain(new RefreshAttributesJob()).then(new RefreshOwnProfileJob()).enqueue(); + ApplicationDependencies.resetAllNetworkConnections(); + }); put(GIFT_BADGE_RECEIVE_SUPPORT, change -> ApplicationDependencies.getJobManager().startChain(new RefreshAttributesJob()).then(new RefreshOwnProfileJob()).enqueue()); }}; diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SignalProxyUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/SignalProxyUtil.java index be8d0cc05f..f85c7dd88b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/SignalProxyUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SignalProxyUtil.java @@ -52,7 +52,7 @@ public static void startListeningToWebsocket() { public static void enableProxy(@NonNull SignalProxy proxy) { SignalStore.proxy().enableProxy(proxy); Conscrypt.setUseEngineSocketByDefault(true); - ApplicationDependencies.resetNetworkConnectionsAfterProxyChange(); + ApplicationDependencies.resetAllNetworkConnections(); startListeningToWebsocket(); } @@ -63,7 +63,7 @@ public static void enableProxy(@NonNull SignalProxy proxy) { public static void disableProxy() { SignalStore.proxy().disableProxy(); Conscrypt.setUseEngineSocketByDefault(false); - ApplicationDependencies.resetNetworkConnectionsAfterProxyChange(); + ApplicationDependencies.resetAllNetworkConnections(); startListeningToWebsocket(); } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java index 68d9443ff8..9199d5ffb1 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java @@ -195,15 +195,11 @@ public SignalServiceStickerManifest retrieveStickerManifest(byte[] packId, byte[ return new SignalServiceStickerManifest(pack.getTitle(), pack.getAuthor(), cover, stickers); } - public List retrieveMessages() throws IOException { - return retrieveMessages(new NullMessageReceivedCallback()); - } - - public List retrieveMessages(MessageReceivedCallback callback) + public List retrieveMessages(boolean allowStories, MessageReceivedCallback callback) throws IOException { List results = new LinkedList<>(); - SignalServiceMessagesResult messageResult = socket.getMessages(); + SignalServiceMessagesResult messageResult = socket.getMessages(allowStories); for (SignalServiceEnvelopeEntity entity : messageResult.getEnvelopes()) { SignalServiceEnvelope envelope; diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index 1c3d4bd967..93ac1606cf 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -221,7 +221,7 @@ public SendMessageResult sendReceipt(SignalServiceAddress recipient, EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.empty()); - return sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getWhen(), envelopeContent, false, null, false); + return sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getWhen(), envelopeContent, false, null, false, false); } /** @@ -237,7 +237,7 @@ public void sendRetryReceipt(SignalServiceAddress recipient, PlaintextContent content = new PlaintextContent(errorMessage); EnvelopeContent envelopeContent = EnvelopeContent.plaintext(content, groupId); - sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), envelopeContent, false, null, false); + sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), envelopeContent, false, null, false, false); } /** @@ -252,7 +252,7 @@ public void sendTyping(List recipients, Content content = createTypingContent(message); EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.empty()); - sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), envelopeContent, true, null, cancelationSignal, false); + sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), envelopeContent, true, null, cancelationSignal, false, false); } /** @@ -265,31 +265,12 @@ public void sendGroupTyping(DistributionId distributionId, throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException, InvalidRegistrationIdException { Content content = createTypingContent(message); - sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp(), content, ContentHint.IMPLICIT, message.getGroupId(), true, SenderKeyGroupEvents.EMPTY, false); - } - - public List sendStory(List recipients, - List> unidentifiedAccess, - boolean isRecipientUpdate, - SignalServiceStoryMessage message, - long timestamp, - Set manifest) - throws IOException, UntrustedIdentityException - { - Content content = createStoryContent(message); - EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.empty()); - List sendMessageResults = sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null, null, false); - - if (aciStore.isMultiDevice()) { - SignalServiceSyncMessage syncMessage = createSelfSendSyncMessageForStory(message, timestamp, isRecipientUpdate, manifest); - sendSyncMessage(syncMessage, Optional.empty()); - } - - return sendMessageResults; + sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp(), content, ContentHint.IMPLICIT, message.getGroupId(), true, SenderKeyGroupEvents.EMPTY, false, false); } /** - * Send a story using sender key. + * Send a story using sender key. Note: This is not just for group stories -- it's for any story. Just following the naming convention of making sender key + * method named "sendGroup*" */ public List sendGroupStory(DistributionId distributionId, Optional groupId, @@ -302,7 +283,7 @@ public List sendGroupStory(DistributionId throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException, InvalidRegistrationIdException { Content content = createStoryContent(message); - List sendMessageResults = sendGroupMessage(distributionId, recipients, unidentifiedAccess, timestamp, content, ContentHint.IMPLICIT, groupId, false, SenderKeyGroupEvents.EMPTY, false); + List sendMessageResults = sendGroupMessage(distributionId, recipients, unidentifiedAccess, timestamp, content, ContentHint.IMPLICIT, groupId, false, SenderKeyGroupEvents.EMPTY, false, true); if (aciStore.isMultiDevice()) { SignalServiceSyncMessage syncMessage = createSelfSendSyncMessageForStory(message, timestamp, isRecipientUpdate, manifest); @@ -328,7 +309,7 @@ public void sendCallMessage(SignalServiceAddress recipient, Content content = createCallContent(message); EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.DEFAULT, Optional.empty()); - sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), envelopeContent, false, null, message.isUrgent()); + sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), envelopeContent, false, null, message.isUrgent(), false); } public List sendCallMessage(List recipients, @@ -339,7 +320,7 @@ public List sendCallMessage(List recipi Content content = createCallContent(message); EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.DEFAULT, Optional.empty()); - return sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), envelopeContent, false, null, null, message.isUrgent()); + return sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), envelopeContent, false, null, null, message.isUrgent(), false); } public List sendCallMessage(DistributionId distributionId, @@ -349,7 +330,7 @@ public List sendCallMessage(DistributionId distributionId, throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException, InvalidRegistrationIdException { Content content = createCallContent(message); - return sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp().get(), content, ContentHint.IMPLICIT, message.getGroupId(), false, SenderKeyGroupEvents.EMPTY, message.isUrgent()); + return sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp().get(), content, ContentHint.IMPLICIT, message.getGroupId(), false, SenderKeyGroupEvents.EMPTY, message.isUrgent(), false); } /** @@ -399,7 +380,7 @@ public SendMessageResult sendDataMessage(SignalServiceAddress recipi sendEvents.onMessageEncrypted(); long timestamp = message.getTimestamp(); - SendMessageResult result = sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null, urgent); + SendMessageResult result = sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null, urgent, false); sendEvents.onMessageSent(); @@ -407,7 +388,7 @@ public SendMessageResult sendDataMessage(SignalServiceAddress recipi Content syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.of(recipient), timestamp, Collections.singletonList(result), false, Collections.emptySet()); EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty()); - sendMessage(localAddress, Optional.empty(), timestamp, syncMessageContent, false, null, false); + sendMessage(localAddress, Optional.empty(), timestamp, syncMessageContent, false, null, false, false); } sendEvents.onSyncMessageSent(); @@ -432,7 +413,8 @@ public List sendSenderKeyDistributionMessage(DistributionId List> unidentifiedAccess, SenderKeyDistributionMessage message, Optional groupId, - boolean urgent) + boolean urgent, + boolean story) throws IOException { ByteString distributionBytes = ByteString.copyFrom(message.serialize()); @@ -441,7 +423,7 @@ public List sendSenderKeyDistributionMessage(DistributionId long timestamp = System.currentTimeMillis(); Log.d(TAG, "[" + timestamp + "] Sending SKDM to " + recipients.size() + " recipients for DistributionId " + distributionId); - return sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null, null, urgent); + return sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null, null, urgent, story); } /** @@ -466,7 +448,7 @@ public SendMessageResult resendContent(SignalServiceAddress address, EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, contentHint, groupId); Optional access = unidentifiedAccess.isPresent() ? unidentifiedAccess.get().getTargetUnidentifiedAccess() : Optional.empty(); - return sendMessage(address, access, timestamp, envelopeContent, false, null, urgent); + return sendMessage(address, access, timestamp, envelopeContent, false, null, urgent, false); } /** @@ -486,7 +468,7 @@ public List sendGroupDataMessage(DistributionId d Content content = createMessageContent(message); Optional groupId = message.getGroupId(); - List results = sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp(), content, contentHint, groupId, false, sendEvents, urgent); + List results = sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp(), content, contentHint, groupId, false, sendEvents, urgent, false); sendEvents.onMessageSent(); @@ -494,7 +476,7 @@ public List sendGroupDataMessage(DistributionId d Content syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.empty(), message.getTimestamp(), results, isRecipientUpdate, Collections.emptySet()); EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty()); - sendMessage(localAddress, Optional.empty(), message.getTimestamp(), syncMessageContent, false, null, false); + sendMessage(localAddress, Optional.empty(), message.getTimestamp(), syncMessageContent, false, null, false, false); } sendEvents.onSyncMessageSent(); @@ -524,7 +506,7 @@ public List sendDataMessage(List Content content = createMessageContent(message); EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, contentHint, message.getGroupId()); long timestamp = message.getTimestamp(); - List results = sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, partialListener, cancelationSignal, urgent); + List results = sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, partialListener, cancelationSignal, urgent, false); boolean needsSyncInResults = false; sendEvents.onMessageSent(); @@ -545,7 +527,7 @@ public List sendDataMessage(List Content syncMessage = createMultiDeviceSentTranscriptContent(content, recipient, timestamp, results, isRecipientUpdate, Collections.emptySet()); EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty()); - sendMessage(localAddress, Optional.empty(), timestamp, syncMessageContent, false, null, false); + sendMessage(localAddress, Optional.empty(), timestamp, syncMessageContent, false, null, false, false); } sendEvents.onSyncMessageSent(); @@ -608,7 +590,7 @@ public SendMessageResult sendSyncMessage(SignalServiceSyncMessage message, Optio EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.empty()); - return sendMessage(localAddress, Optional.empty(), timestamp, envelopeContent, false, null, urgent); + return sendMessage(localAddress, Optional.empty(), timestamp, envelopeContent, false, null, urgent, false); } /** @@ -626,11 +608,7 @@ public SendMessageResult sendSyncMessage(SignalServiceSyncMessage message, Optio Content.Builder content = Content.newBuilder().setSyncMessage(syncMessage); EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content.build(), ContentHint.IMPLICIT, Optional.empty()); - return getEncryptedMessage(socket, localAddress, Optional.empty(), deviceId, envelopeContent); - } - - public void setSoTimeoutMillis(long soTimeoutMillis) { - socket.setSoTimeoutMillis(soTimeoutMillis); + return getEncryptedMessage(localAddress, Optional.empty(), deviceId, envelopeContent, false); } public void cancelInFlightRequests() { @@ -763,13 +741,13 @@ private SendMessageResult sendVerifiedSyncMessage(VerifiedMessage message) EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.empty()); - SendMessageResult result = sendMessage(message.getDestination(), Optional.empty(), message.getTimestamp(), envelopeContent, false, null, false); + SendMessageResult result = sendMessage(message.getDestination(), Optional.empty(), message.getTimestamp(), envelopeContent, false, null, false, false); if (result.getSuccess().isNeedsSync()) { Content syncMessage = createMultiDeviceVerifiedContent(message, nullMessage.toByteArray()); EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty()); - sendMessage(localAddress, Optional.empty(), message.getTimestamp(), syncMessageContent, false, null, false); + sendMessage(localAddress, Optional.empty(), message.getTimestamp(), syncMessageContent, false, null, false, false); } return result; @@ -793,7 +771,7 @@ public SendMessageResult sendNullMessage(SignalServiceAddress address, Optional< EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.empty()); - return sendMessage(address, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), envelopeContent, false, null, false); + return sendMessage(address, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), envelopeContent, false, null, false, false); } private SignalServiceProtos.PniSignatureMessage createPniSignatureMessage() { @@ -1678,7 +1656,8 @@ private List sendMessage(List r boolean online, PartialSendCompleteListener partialListener, CancelationSignal cancelationSignal, - boolean urgent) + boolean urgent, + boolean story) throws IOException { Log.d(TAG, "[" + timestamp + "] Sending to " + recipients.size() + " recipients."); @@ -1693,7 +1672,7 @@ private List sendMessage(List r SignalServiceAddress recipient = recipientIterator.next(); Optional access = unidentifiedAccessIterator.next(); futureResults.add(executor.submit(() -> { - SendMessageResult result = sendMessage(recipient, access, timestamp, content, online, cancelationSignal, urgent); + SendMessageResult result = sendMessage(recipient, access, timestamp, content, online, cancelationSignal, urgent, story); if (partialListener != null) { partialListener.onPartialSendComplete(result); } @@ -1761,7 +1740,8 @@ private SendMessageResult sendMessage(SignalServiceAddress recipient, EnvelopeContent content, boolean online, CancelationSignal cancelationSignal, - boolean urgent) + boolean urgent, + boolean story) throws UntrustedIdentityException, IOException { enforceMaxContentSize(content); @@ -1774,7 +1754,7 @@ private SendMessageResult sendMessage(SignalServiceAddress recipient, } try { - OutgoingPushMessageList messages = getEncryptedMessages(socket, recipient, unidentifiedAccess, timestamp, content, online, urgent); + OutgoingPushMessageList messages = getEncryptedMessages(recipient, unidentifiedAccess, timestamp, content, online, urgent, story); if (content.getContent().isPresent() && content.getContent().get().getSyncMessage() != null && content.getContent().get().getSyncMessage().hasSent()) { Log.d(TAG, "[sendMessage][" + timestamp + "] Sending a sent sync message to devices: " + messages.getDevices()); @@ -1788,7 +1768,7 @@ private SendMessageResult sendMessage(SignalServiceAddress recipient, if (!unidentifiedAccess.isPresent()) { try { - SendMessageResponse response = new MessagingService.SendResponseProcessor<>(messagingService.send(messages, Optional.empty()).blockingGet()).getResultOrThrow(); + SendMessageResponse response = new MessagingService.SendResponseProcessor<>(messagingService.send(messages, Optional.empty(), story).blockingGet()).getResultOrThrow(); return SendMessageResult.success(recipient, messages.getDevices(), response.sentUnidentified(), response.getNeedsSync() || aciStore.isMultiDevice(), System.currentTimeMillis() - startTime, content.getContent()); } catch (InvalidUnidentifiedAccessHeaderException | UnregisteredUserException | MismatchedDevicesException | StaleDevicesException e) { // Non-technical failures shouldn't be retried with socket @@ -1801,7 +1781,7 @@ private SendMessageResult sendMessage(SignalServiceAddress recipient, } } else if (unidentifiedAccess.isPresent()) { try { - SendMessageResponse response = new MessagingService.SendResponseProcessor<>(messagingService.send(messages, unidentifiedAccess).blockingGet()).getResultOrThrow(); + SendMessageResponse response = new MessagingService.SendResponseProcessor<>(messagingService.send(messages, unidentifiedAccess, story).blockingGet()).getResultOrThrow(); return SendMessageResult.success(recipient, messages.getDevices(), response.sentUnidentified(), response.getNeedsSync() || aciStore.isMultiDevice(), System.currentTimeMillis() - startTime, content.getContent()); } catch (InvalidUnidentifiedAccessHeaderException | UnregisteredUserException | MismatchedDevicesException | StaleDevicesException e) { // Non-technical failures shouldn't be retried with socket @@ -1821,7 +1801,7 @@ private SendMessageResult sendMessage(SignalServiceAddress recipient, throw new CancelationException(); } - SendMessageResponse response = socket.sendMessage(messages, unidentifiedAccess); + SendMessageResponse response = socket.sendMessage(messages, unidentifiedAccess, story); return SendMessageResult.success(recipient, messages.getDevices(), response.sentUnidentified(), response.getNeedsSync() || aciStore.isMultiDevice(), System.currentTimeMillis() - startTime, content.getContent()); @@ -1863,7 +1843,8 @@ private List sendGroupMessage(DistributionId dist Optional groupId, boolean online, SenderKeyGroupEvents sendEvents, - boolean urgent) + boolean urgent, + boolean story) throws IOException, UntrustedIdentityException, NoSessionException, InvalidKeyException, InvalidRegistrationIdException { if (recipients.isEmpty()) { @@ -1900,7 +1881,7 @@ private List sendGroupMessage(DistributionId dist }) .collect(Collectors.toList()); - List results = sendSenderKeyDistributionMessage(distributionId, needsSenderKey, access, message, groupId, urgent); + List results = sendSenderKeyDistributionMessage(distributionId, needsSenderKey, access, message, groupId, urgent, story); List successes = results.stream() .filter(SendMessageResult::isSuccess) @@ -1962,7 +1943,7 @@ private List sendGroupMessage(DistributionId dist try { try { - SendGroupMessageResponse response = new MessagingService.SendResponseProcessor<>(messagingService.sendToGroup(ciphertext, joinedUnidentifiedAccess, timestamp, online, urgent).blockingGet()).getResultOrThrow(); + SendGroupMessageResponse response = new MessagingService.SendResponseProcessor<>(messagingService.sendToGroup(ciphertext, joinedUnidentifiedAccess, timestamp, online, urgent, story).blockingGet()).getResultOrThrow(); return transformGroupResponseToMessageResults(targetInfo.devices, response, content); } catch (InvalidUnidentifiedAccessHeaderException | NotFoundException | GroupMismatchedDevicesException | GroupStaleDevicesException e) { // Non-technical failures shouldn't be retried with socket @@ -1973,7 +1954,7 @@ private List sendGroupMessage(DistributionId dist Log.w(TAG, "[sendGroupMessage][" + timestamp + "] Pipe failed, falling back... (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")"); } - SendGroupMessageResponse response = socket.sendGroupMessage(ciphertext, joinedUnidentifiedAccess, timestamp, online, urgent); + SendGroupMessageResponse response = socket.sendGroupMessage(ciphertext, joinedUnidentifiedAccess, timestamp, online, urgent, true); return transformGroupResponseToMessageResults(targetInfo.devices, response, content); } catch (GroupMismatchedDevicesException e) { Log.w(TAG, "[sendGroupMessage][" + timestamp + "] Handling mismatched devices. (" + e.getMessage() + ")"); @@ -2138,13 +2119,13 @@ private TextAttachment createTextAttachment(SignalServiceTextAttachment attachme return builder.build(); } - private OutgoingPushMessageList getEncryptedMessages(PushServiceSocket socket, - SignalServiceAddress recipient, + private OutgoingPushMessageList getEncryptedMessages(SignalServiceAddress recipient, Optional unidentifiedAccess, long timestamp, EnvelopeContent plaintext, boolean online, - boolean urgent) + boolean urgent, + boolean story) throws IOException, InvalidKeyException, UntrustedIdentityException { List messages = new LinkedList<>(); @@ -2161,18 +2142,18 @@ private OutgoingPushMessageList getEncryptedMessages(PushServiceSocket for (int deviceId : deviceIds) { if (deviceId == SignalServiceAddress.DEFAULT_DEVICE_ID || aciStore.containsSession(new SignalProtocolAddress(recipient.getIdentifier(), deviceId))) { - messages.add(getEncryptedMessage(socket, recipient, unidentifiedAccess, deviceId, plaintext)); + messages.add(getEncryptedMessage(recipient, unidentifiedAccess, deviceId, plaintext, story)); } } return new OutgoingPushMessageList(recipient.getIdentifier(), timestamp, messages, online, urgent); } - private OutgoingPushMessage getEncryptedMessage(PushServiceSocket socket, - SignalServiceAddress recipient, + private OutgoingPushMessage getEncryptedMessage(SignalServiceAddress recipient, Optional unidentifiedAccess, int deviceId, - EnvelopeContent plaintext) + EnvelopeContent plaintext, + boolean story) throws IOException, InvalidKeyException, UntrustedIdentityException { SignalProtocolAddress signalProtocolAddress = new SignalProtocolAddress(recipient.getIdentifier(), deviceId); @@ -2180,7 +2161,7 @@ private OutgoingPushMessage getEncryptedMessage(PushServiceSocket soc if (!aciStore.containsSession(signalProtocolAddress)) { try { - List preKeys = socket.getPreKeys(recipient, unidentifiedAccess, deviceId); + List preKeys = getPreKeys(recipient, unidentifiedAccess, deviceId, story); for (PreKeyBundle preKey : preKeys) { try { @@ -2207,6 +2188,19 @@ private OutgoingPushMessage getEncryptedMessage(PushServiceSocket soc } } + + private List getPreKeys(SignalServiceAddress recipient, Optional unidentifiedAccess, int deviceId, boolean story) throws IOException { + try { + return socket.getPreKeys(recipient, unidentifiedAccess, deviceId); + } catch (NonSuccessfulResponseCodeException e) { + if (e.getCode() == 401 && story) { + return socket.getPreKeys(recipient, Optional.empty(), deviceId); + } else { + throw e; + } + } + } + private void handleMismatchedDevices(PushServiceSocket socket, SignalServiceAddress recipient, MismatchedDevices mismatchedDevices) throws IOException, UntrustedIdentityException diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/MessagingService.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/MessagingService.java index c52355c0f3..9d4e63a145 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/MessagingService.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/MessagingService.java @@ -43,7 +43,7 @@ public MessagingService(SignalWebSocket signalWebSocket) { this.signalWebSocket = signalWebSocket; } - public Single> send(OutgoingPushMessageList list, Optional unidentifiedAccess) { + public Single> send(OutgoingPushMessageList list, Optional unidentifiedAccess, boolean story) { List headers = new LinkedList() {{ add("content-type:application/json"); }}; @@ -51,7 +51,7 @@ public Single> send(OutgoingPushMessageList WebSocketRequestMessage requestMessage = WebSocketRequestMessage.newBuilder() .setId(new SecureRandom().nextLong()) .setVerb("PUT") - .setPath(String.format("/v1/messages/%s", list.getDestination())) + .setPath(String.format("/v1/messages/%s?story=%s", list.getDestination(), story ? "true" : "false")) .addAllHeaders(headers) .setBody(ByteString.copyFrom(JsonUtil.toJson(list).getBytes())) .build(); @@ -72,13 +72,13 @@ public Single> send(OutgoingPushMessageList .onErrorReturn(ServiceResponse::forUnknownError); } - public Single> sendToGroup(byte[] body, byte[] joinedUnidentifiedAccess, long timestamp, boolean online, boolean urgent) { + public Single> sendToGroup(byte[] body, byte[] joinedUnidentifiedAccess, long timestamp, boolean online, boolean urgent, boolean story) { List headers = new LinkedList() {{ add("content-type:application/vnd.signal-messenger.mrm"); add("Unidentified-Access-Key:" + Base64.encodeBytes(joinedUnidentifiedAccess)); }}; - String path = String.format(Locale.US, "/v1/messages/multi_recipient?ts=%s&online=%s&urgent=%s", timestamp, online, urgent); + String path = String.format(Locale.US, "/v1/messages/multi_recipient?ts=%s&online=%s&urgent=%s&story=%s", timestamp, online, urgent, story); WebSocketRequestMessage requestMessage = WebSocketRequestMessage.newBuilder() .setId(new SecureRandom().nextLong()) diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index 5670ea80c7..43d1cdcd03 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -220,8 +220,8 @@ public class PushServiceSocket { private static final String DEVICE_PATH = "/v1/devices/%s"; private static final String DIRECTORY_AUTH_PATH = "/v1/directory/auth"; - private static final String MESSAGE_PATH = "/v1/messages/%s"; - private static final String GROUP_MESSAGE_PATH = "/v1/messages/multi_recipient?ts=%s&online=%s&urgent=%s"; + private static final String MESSAGE_PATH = "/v1/messages/%s?story=%s"; + private static final String GROUP_MESSAGE_PATH = "/v1/messages/multi_recipient?ts=%s&online=%s&urgent=%s&story=%s"; private static final String SENDER_ACK_MESSAGE_PATH = "/v1/messages/%s/%d"; private static final String UUID_ACK_MESSAGE_PATH = "/v1/messages/uuid/%s"; private static final String ATTACHMENT_V2_PATH = "/v2/attachments/form/upload"; @@ -486,12 +486,12 @@ public byte[] getUuidOnlySenderCertificate() throws IOException { return JsonUtil.fromJson(responseText, SenderCertificate.class).getCertificate(); } - public SendGroupMessageResponse sendGroupMessage(byte[] body, byte[] joinedUnidentifiedAccess, long timestamp, boolean online, boolean urgent) + public SendGroupMessageResponse sendGroupMessage(byte[] body, byte[] joinedUnidentifiedAccess, long timestamp, boolean online, boolean urgent, boolean story) throws IOException { ServiceConnectionHolder connectionHolder = (ServiceConnectionHolder) getRandom(serviceClients, random); - String path = String.format(Locale.US, GROUP_MESSAGE_PATH, timestamp, online, urgent); + String path = String.format(Locale.US, GROUP_MESSAGE_PATH, timestamp, online, urgent, story); Request.Builder requestBuilder = new Request.Builder(); requestBuilder.url(String.format("%s%s", connectionHolder.getUrl(), path)); @@ -544,11 +544,11 @@ public SendGroupMessageResponse sendGroupMessage(byte[] body, byte[] joinedUnide } } - public SendMessageResponse sendMessage(OutgoingPushMessageList bundle, Optional unidentifiedAccess) + public SendMessageResponse sendMessage(OutgoingPushMessageList bundle, Optional unidentifiedAccess, boolean story) throws IOException { try { - String responseText = makeServiceRequest(String.format(MESSAGE_PATH, bundle.getDestination()), "PUT", JsonUtil.toJson(bundle), NO_HEADERS, unidentifiedAccess); + String responseText = makeServiceRequest(String.format(MESSAGE_PATH, bundle.getDestination(), story ? "true" : "false"), "PUT", JsonUtil.toJson(bundle), NO_HEADERS, unidentifiedAccess); SendMessageResponse response = JsonUtil.fromJson(responseText, SendMessageResponse.class); response.setSentUnidentfied(unidentifiedAccess.isPresent()); @@ -559,8 +559,10 @@ public SendMessageResponse sendMessage(OutgoingPushMessageList bundle, Optional< } } - public SignalServiceMessagesResult getMessages() throws IOException { - try (Response response = makeServiceRequest(String.format(MESSAGE_PATH, ""), "GET", (RequestBody) null, NO_HEADERS, NO_HANDLER, Optional.empty())) { + public SignalServiceMessagesResult getMessages(boolean allowStories) throws IOException { + Map headers = Collections.singletonMap("X-Signal-Receive-Stories", allowStories ? "true" : "false"); + + try (Response response = makeServiceRequest(String.format(MESSAGE_PATH, ""), "GET", (RequestBody) null, headers, NO_HANDLER, Optional.empty())) { validateServiceResponse(response); List envelopes = readBodyJson(response.body(), SignalServiceEnvelopeEntityList.class).getMessages(); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/SendGroupMessageResponse.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/SendGroupMessageResponse.java index 0d7856880a..25110f3573 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/SendGroupMessageResponse.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/SendGroupMessageResponse.java @@ -18,9 +18,10 @@ public class SendGroupMessageResponse { public SendGroupMessageResponse() {} public Set getUnsentTargets() { - Set serviceIds = new HashSet<>(uuids404.length); + String[] uuids = uuids404 != null ? uuids404 : new String[0]; + Set serviceIds = new HashSet<>(uuids.length); - for (String raw : uuids404) { + for (String raw : uuids) { ServiceId parsed = ServiceId.parseOrNull(raw); if (parsed != null) { serviceIds.add(parsed); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/websocket/WebSocketConnection.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/websocket/WebSocketConnection.java index 2f722427f0..0b2e3509cf 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/websocket/WebSocketConnection.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/websocket/WebSocketConnection.java @@ -66,14 +66,15 @@ public class WebSocketConnection extends WebSocketListener { private final String name; private final String wsUri; - private final TrustStore trustStore; - private final Optional credentialsProvider; - private final String signalAgent; + private final TrustStore trustStore; + private final Optional credentialsProvider; + private final String signalAgent; private final HealthMonitor healthMonitor; private final List interceptors; private final Optional dns; private final Optional signalProxy; private final BehaviorSubject webSocketState; + private final boolean allowStories; private WebSocket client; @@ -81,8 +82,9 @@ public WebSocketConnection(String name, SignalServiceConfiguration serviceConfiguration, Optional credentialsProvider, String signalAgent, - HealthMonitor healthMonitor) { - this(name, serviceConfiguration, credentialsProvider, signalAgent, healthMonitor, ""); + HealthMonitor healthMonitor, + boolean allowStories) { + this(name, serviceConfiguration, credentialsProvider, signalAgent, healthMonitor, "", allowStories); } public WebSocketConnection(String name, @@ -90,7 +92,8 @@ public WebSocketConnection(String name, Optional credentialsProvider, String signalAgent, HealthMonitor healthMonitor, - String extraPathUri) + String extraPathUri, + boolean allowStories) { this.name = "[" + name + ":" + System.identityHashCode(this) + "]"; this.trustStore = serviceConfiguration.getSignalServiceUrls()[0].getTrustStore(); @@ -101,6 +104,7 @@ public WebSocketConnection(String name, this.signalProxy = serviceConfiguration.getSignalProxy(); this.healthMonitor = healthMonitor; this.webSocketState = BehaviorSubject.createDefault(WebSocketConnectionState.DISCONNECTED); + this.allowStories = allowStories; String uri = serviceConfiguration.getSignalServiceUrls()[0].getUrl().replace("https://", "wss://").replace("http://", "ws://"); @@ -156,6 +160,8 @@ public synchronized Observable connect() { requestBuilder.addHeader("X-Signal-Agent", signalAgent); } + requestBuilder.addHeader("X-Signal-Receive-Stories", allowStories ? "true" : "false"); + webSocketState.onNext(WebSocketConnectionState.CONNECTING); this.client = okHttpClient.newWebSocket(requestBuilder.build(), this); From 26709177d2d534409f0ed34001db0b0793b0e61a Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Wed, 5 Oct 2022 17:09:28 -0400 Subject: [PATCH 22/78] Fix out-of-sync local state after rejoining a group via invite link. --- .../ConversationParentFragment.java | 3 + .../securesms/database/GroupDatabase.java | 122 +++++++++-------- .../helpers/SignalDatabaseMigrations.kt | 7 +- ...GroupsLastForceUpdateTimestampMigration.kt | 13 ++ .../securesms/groups/GroupId.java | 8 +- .../securesms/groups/GroupManager.java | 11 ++ .../securesms/groups/GroupManagerV2.java | 26 ++-- .../v2/processing/GroupsV2StateProcessor.java | 51 ++++++- .../securesms/jobs/ForceUpdateGroupV2Job.java | 86 ++++++++++++ .../jobs/ForceUpdateGroupV2WorkerJob.java | 103 ++++++++++++++ .../securesms/jobs/JobManagerFactories.java | 4 +- .../securesms/database/GroupTestUtil.kt | 3 +- .../processing/GroupsV2StateProcessorTest.kt | 126 ++++++++++++++++++ 13 files changed, 487 insertions(+), 76 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V158_GroupsLastForceUpdateTimestampMigration.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/jobs/ForceUpdateGroupV2Job.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/jobs/ForceUpdateGroupV2WorkerJob.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java index 5514625b68..fe6a692fe7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java @@ -199,6 +199,7 @@ import org.thoughtcrime.securesms.insights.InsightsLauncher; import org.thoughtcrime.securesms.invites.InviteReminderModel; import org.thoughtcrime.securesms.invites.InviteReminderRepository; +import org.thoughtcrime.securesms.jobs.ForceUpdateGroupV2Job; import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob; import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob; import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob; @@ -594,6 +595,8 @@ public void onResume() { .then(GroupV2UpdateSelfProfileKeyJob.withoutLimits(groupId)) .enqueue(); + ForceUpdateGroupV2Job.enqueueIfNecessary(groupId); + if (viewModel.getArgs().isFirstTimeInSelfCreatedGroup()) { groupViewModel.inviteFriendsOneTimeIfJustSelfInGroup(getChildFragmentManager(), groupId); } 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 ed047dabda..2bc18c3907 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -69,24 +69,25 @@ public class GroupDatabase extends Database implements RecipientIdDatabaseRefere 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"; - public 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"; - private static final String DISTRIBUTION_ID = "distribution_id"; - private static final String SHOW_AS_STORY_STATE = "display_as_story"; + static final String TABLE_NAME = "groups"; + private static final String ID = "_id"; + static final String GROUP_ID = "group_id"; + public 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"; + private static final String DISTRIBUTION_ID = "distribution_id"; + private static final String SHOW_AS_STORY_STATE = "display_as_story"; + private static final String LAST_FORCE_UPDATE_TIMESTAMP = "last_force_update_timestamp"; /** Was temporarily used for PNP accept by pni but is no longer needed/updated */ @Deprecated @@ -101,27 +102,28 @@ public class GroupDatabase extends Database implements RecipientIdDatabaseRefere /** Serialized {@link DecryptedGroup} protobuf */ public 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, " + - UNMIGRATED_V1_MEMBERS + " TEXT DEFAULT NULL, " + - DISTRIBUTION_ID + " TEXT DEFAULT NULL, " + - SHOW_AS_STORY_STATE + " INTEGER DEFAULT 0, " + - AUTH_SERVICE_ID + " 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, " + + DISTRIBUTION_ID + " TEXT DEFAULT NULL, " + + SHOW_AS_STORY_STATE + " INTEGER DEFAULT 0, " + + AUTH_SERVICE_ID + " TEXT DEFAULT NULL, " + + LAST_FORCE_UPDATE_TIMESTAMP + " INTEGER DEFAULT 0);"; public static final String[] CREATE_INDEXS = { "CREATE UNIQUE INDEX IF NOT EXISTS group_id_index ON " + TABLE_NAME + " (" + GROUP_ID + ");", @@ -132,7 +134,7 @@ public class GroupDatabase extends Database implements RecipientIdDatabaseRefere private static final String[] GROUP_PROJECTION = { 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 + TIMESTAMP, ACTIVE, MMS, V2_MASTER_KEY, V2_REVISION, V2_DECRYPTED_GROUP, LAST_FORCE_UPDATE_TIMESTAMP }; static final List TYPED_GROUP_PROJECTION = Stream.of(GROUP_PROJECTION).map(columnName -> TABLE_NAME + "." + columnName).toList(); @@ -955,6 +957,12 @@ public void setActive(@NonNull GroupId groupId, boolean active) { database.update(TABLE_NAME, values, GROUP_ID + " = ?", new String[] {groupId.toString()}); } + public void setLastForceUpdateTimestamp(@NonNull GroupId groupId, long timestamp) { + ContentValues values = new ContentValues(); + values.put(LAST_FORCE_UPDATE_TIMESTAMP, timestamp); + getWritableDatabase().update(TABLE_NAME, values, GROUP_ID + " = ?", SqlUtil.buildArgs(groupId)); + } + @WorkerThread public boolean isCurrentMember(@NonNull GroupId.Push groupId, @NonNull RecipientId recipientId) { SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); @@ -1114,7 +1122,8 @@ public int getCount() { CursorUtil.requireBlob(cursor, V2_MASTER_KEY), CursorUtil.requireInt(cursor, V2_REVISION), CursorUtil.requireBlob(cursor, V2_DECRYPTED_GROUP), - CursorUtil.getString(cursor, DISTRIBUTION_ID).map(DistributionId::from).orElse(null)); + CursorUtil.getString(cursor, DISTRIBUTION_ID).map(DistributionId::from).orElse(null), + CursorUtil.requireLong(cursor, LAST_FORCE_UPDATE_TIMESTAMP)); } @Override @@ -1155,6 +1164,7 @@ public static class GroupRecord { private final boolean mms; @Nullable private final V2GroupProperties v2GroupProperties; private final DistributionId distributionId; + private final long lastForceUpdateTimestamp; public GroupRecord(@NonNull GroupId id, @NonNull RecipientId recipientId, @@ -1171,19 +1181,21 @@ public GroupRecord(@NonNull GroupId id, @Nullable byte[] groupMasterKeyBytes, int groupRevision, @Nullable byte[] decryptedGroupBytes, - @Nullable DistributionId distributionId) + @Nullable DistributionId distributionId, + long lastForceUpdateTimestamp) { - this.id = id; - this.recipientId = recipientId; - this.title = title; - this.avatarId = avatarId; - this.avatarKey = avatarKey; - this.avatarDigest = avatarDigest; - this.avatarContentType = avatarContentType; - this.relay = relay; - this.active = active; - this.mms = mms; - this.distributionId = distributionId; + this.id = id; + this.recipientId = recipientId; + this.title = title; + this.avatarId = avatarId; + this.avatarKey = avatarKey; + this.avatarDigest = avatarDigest; + this.avatarContentType = avatarContentType; + this.relay = relay; + this.active = active; + this.mms = mms; + this.distributionId = distributionId; + this.lastForceUpdateTimestamp = lastForceUpdateTimestamp; V2GroupProperties v2GroupProperties = null; if (groupMasterKeyBytes != null && decryptedGroupBytes != null) { @@ -1292,6 +1304,10 @@ public boolean isMms() { return distributionId; } + public long getLastForceUpdateTimestamp() { + return lastForceUpdateTimestamp; + } + public boolean isV1Group() { return !mms && !isV2Group(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt index 753887e22d..6f17717af0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt @@ -12,13 +12,14 @@ import org.thoughtcrime.securesms.database.helpers.migration.V154_PniSignaturesM import org.thoughtcrime.securesms.database.helpers.migration.V155_SmsExporterMigration import org.thoughtcrime.securesms.database.helpers.migration.V156_RecipientUnregisteredTimestampMigration import org.thoughtcrime.securesms.database.helpers.migration.V157_RecipeintHiddenMigration +import org.thoughtcrime.securesms.database.helpers.migration.V158_GroupsLastForceUpdateTimestampMigration /** * Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness. */ object SignalDatabaseMigrations { - const val DATABASE_VERSION = 157 + const val DATABASE_VERSION = 158 @JvmStatic fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { @@ -57,6 +58,10 @@ object SignalDatabaseMigrations { if (oldVersion < 157) { V157_RecipeintHiddenMigration.migrate(context, db, oldVersion, newVersion) } + + if (oldVersion < 158) { + V158_GroupsLastForceUpdateTimestampMigration.migrate(context, db, oldVersion, newVersion) + } } @JvmStatic diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V158_GroupsLastForceUpdateTimestampMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V158_GroupsLastForceUpdateTimestampMigration.kt new file mode 100644 index 0000000000..8cac5cf3ea --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V158_GroupsLastForceUpdateTimestampMigration.kt @@ -0,0 +1,13 @@ +package org.thoughtcrime.securesms.database.helpers.migration + +import android.app.Application +import net.zetetic.database.sqlcipher.SQLiteDatabase + +/** + * Track last time we did a forced sanity check for this group with the server. + */ +object V158_GroupsLastForceUpdateTimestampMigration : SignalDatabaseMigration { + override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + db.execSQL("ALTER TABLE groups ADD COLUMN last_force_update_timestamp INTEGER DEFAULT 0") + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupId.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupId.java index d269665e93..3d4320e090 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupId.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupId.java @@ -3,6 +3,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.signal.core.util.DatabaseId; import org.signal.core.util.Hex; import org.signal.libsignal.protocol.kdf.HKDFv3; import org.signal.libsignal.zkgroup.InvalidInputException; @@ -14,7 +15,7 @@ import java.io.IOException; import java.security.SecureRandom; -public abstract class GroupId { +public abstract class GroupId implements DatabaseId { private static final String ENCODED_SIGNAL_GROUP_V1_PREFIX = "__textsecure_group__!"; private static final String ENCODED_SIGNAL_GROUP_V2_PREFIX = "__signal_group__v2__!"; @@ -173,6 +174,11 @@ public int hashCode() { return encodedId; } + @Override + public @NonNull String serialize() { + return encodedId; + } + public abstract boolean isMms(); public abstract boolean isV1(); 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 6bf940b857..f24dd70fb4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java @@ -188,6 +188,17 @@ public static void updateGroupFromServer(@NonNull Context context, } } + @WorkerThread + public static void forceSanityUpdateFromServer(@NonNull Context context, + @NonNull GroupMasterKey groupMasterKey, + long timestamp) + throws GroupChangeBusyException, IOException, GroupNotAMemberException + { + try (GroupManagerV2.GroupUpdater updater = new GroupManagerV2(context).updater(groupMasterKey)) { + updater.forceSanityUpdateFromServer(timestamp); + } + } + @WorkerThread public static V2GroupServerStatus v2GroupStatus(@NonNull Context context, @NonNull ServiceId authServiceId, 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 d63949ed00..ff895cb31a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java @@ -798,6 +798,14 @@ void updateLocalToServerRevision(int revision, long timestamp, @Nullable byte[] .updateLocalGroupToRevision(revision, timestamp, getDecryptedGroupChange(signedGroupChange)); } + @WorkerThread + void forceSanityUpdateFromServer(long timestamp) + throws IOException, GroupNotAMemberException + { + new GroupsV2StateProcessor(context).forGroup(serviceIds, groupMasterKey) + .forceSanityUpdateFromServer(timestamp); + } + private DecryptedGroupChange getDecryptedGroupChange(@Nullable byte[] signedGroupChange) { if (signedGroupChange != null) { GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(GroupSecretParams.deriveFromMasterKey(groupMasterKey)); @@ -928,24 +936,6 @@ public GroupManager.GroupActionResult joinGroup(@NonNull DecryptedGroupJoinInfo if (group.isPresent()) { Log.i(TAG, "Group already present locally"); - - DecryptedGroup currentGroupState = group.get() - .requireV2GroupProperties() - .getDecryptedGroup(); - - DecryptedGroup updatedGroup = currentGroupState; - - try { - if (decryptedChange != null) { - updatedGroup = DecryptedGroupUtil.applyWithoutRevisionCheck(updatedGroup, decryptedChange); - } - updatedGroup = resetRevision(updatedGroup, currentGroupState.getRevision()); - } catch (NotAbleToApplyGroupV2ChangeException e) { - Log.w(TAG, e); - updatedGroup = decryptedGroup; - } - - groupDatabase.update(groupId, updatedGroup); } else { groupDatabase.create(groupMasterKey, decryptedGroup); Log.i(TAG, "Created local group with placeholder"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java index 156755bdab..c33164ae16 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java @@ -48,6 +48,7 @@ import org.thoughtcrime.securesms.sms.IncomingTextMessage; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupHistoryEntry; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; +import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct; import org.whispersystems.signalservice.api.groupsv2.GroupHistoryPage; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException; @@ -170,6 +171,52 @@ public static final class StateProcessorForGroup { this.profileAndMessageHelper = profileAndMessageHelper; } + @WorkerThread + public GroupUpdateResult forceSanityUpdateFromServer(long timestamp) + throws IOException, GroupNotAMemberException + { + Optional localRecord = groupDatabase.getGroup(groupId); + DecryptedGroup localState = localRecord.map(g -> g.requireV2GroupProperties().getDecryptedGroup()).orElse(null); + DecryptedGroup serverState; + + if (localState == null) { + info("No local state to force update"); + return new GroupUpdateResult(GroupState.GROUP_CONSISTENT_OR_AHEAD, null); + } + + try { + serverState = groupsV2Api.getGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(serviceIds, groupSecretParams)); + } catch (NotInGroupException | GroupNotFoundException e) { + throw new GroupNotAMemberException(e); + } catch (VerificationFailedException | InvalidGroupStateException e) { + throw new IOException(e); + } + + DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(localState, serverState); + GlobalGroupState inputGroupState = new GlobalGroupState(localState, Collections.singletonList(new ServerGroupLogEntry(serverState, decryptedGroupChange))); + AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(inputGroupState, serverState.getRevision()); + DecryptedGroup newLocalState = advanceGroupStateResult.getNewGlobalGroupState().getLocalState(); + + if (newLocalState == null || newLocalState == inputGroupState.getLocalState()) { + info("Local state and server state are equal"); + return new GroupUpdateResult(GroupState.GROUP_CONSISTENT_OR_AHEAD, null); + } else { + info("Local state (revision: " + localState.getRevision() + ") does not match server state (revision: " + serverState.getRevision() + "), updating"); + } + + updateLocalDatabaseGroupState(inputGroupState, newLocalState); + if (localState.getRevision() == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) { + info("Inserting single update message for restore placeholder"); + profileAndMessageHelper.insertUpdateMessages(timestamp, null, Collections.singleton(new LocalGroupLogEntry(newLocalState, null))); + } else { + info("Inserting force update messages"); + profileAndMessageHelper.insertUpdateMessages(timestamp, localState, advanceGroupStateResult.getProcessedLogEntries()); + } + profileAndMessageHelper.persistLearnedProfileKeys(inputGroupState); + + return new GroupUpdateResult(GroupState.GROUP_UPDATED, newLocalState); + } + /** * Using network where required, will attempt to bring the local copy of the group up to the revision specified. * @@ -560,10 +607,12 @@ static class ProfileAndMessageHelper { private final Context context; private final ServiceId serviceId; - private final GroupMasterKey masterKey; private final GroupId.V2 groupId; private final RecipientDatabase recipientDatabase; + @VisibleForTesting + GroupMasterKey masterKey; + ProfileAndMessageHelper(@NonNull Context context, @NonNull ServiceId serviceId, @NonNull GroupMasterKey masterKey, @NonNull GroupId.V2 groupId, @NonNull RecipientDatabase recipientDatabase) { this.context = context; this.serviceId = serviceId; diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ForceUpdateGroupV2Job.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ForceUpdateGroupV2Job.java new file mode 100644 index 0000000000..0b427c4ee3 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ForceUpdateGroupV2Job.java @@ -0,0 +1,86 @@ +package org.thoughtcrime.securesms.jobs; + +import androidx.annotation.NonNull; + +import org.signal.core.util.concurrent.SignalExecutors; +import org.thoughtcrime.securesms.database.GroupDatabase; +import org.thoughtcrime.securesms.database.SignalDatabase; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.groups.GroupId; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.impl.DecryptionsDrainedConstraint; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +/** + * Schedules a {@link ForceUpdateGroupV2WorkerJob} to happen after message queues are drained. + */ +public final class ForceUpdateGroupV2Job extends BaseJob { + + public static final String KEY = "ForceUpdateGroupV2Job"; + + private static final long FORCE_UPDATE_INTERVAL = TimeUnit.DAYS.toMillis(7); + private static final String KEY_GROUP_ID = "group_id"; + + private final GroupId.V2 groupId; + + public static void enqueueIfNecessary(@NonNull GroupId.V2 groupId) { + SignalExecutors.BOUNDED.execute(() -> { + Optional group = SignalDatabase.groups().getGroup(groupId); + if (group.isPresent() && + group.get().isV2Group() && + group.get().getLastForceUpdateTimestamp() + FORCE_UPDATE_INTERVAL < System.currentTimeMillis() + ) { + ApplicationDependencies.getJobManager().add(new ForceUpdateGroupV2Job(groupId)); + } + }); + } + + private ForceUpdateGroupV2Job(@NonNull GroupId.V2 groupId) { + this(new Parameters.Builder().setQueue("ForceUpdateGroupV2Job_" + groupId) + .setMaxInstancesForQueue(1) + .addConstraint(DecryptionsDrainedConstraint.KEY) + .setMaxAttempts(Parameters.UNLIMITED) + .build(), + groupId); + } + + private ForceUpdateGroupV2Job(@NonNull Parameters parameters, @NonNull GroupId.V2 groupId) { + super(parameters); + this.groupId = groupId; + } + + @Override + public @NonNull Data serialize() { + return new Data.Builder().putString(KEY_GROUP_ID, groupId.toString()).build(); + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; + } + + @Override + public void onRun() { + ApplicationDependencies.getJobManager().add(new ForceUpdateGroupV2WorkerJob(groupId)); + } + + @Override + public boolean onShouldRetry(@NonNull Exception e) { + return false; + } + + @Override + public void onFailure() { + } + + public static final class Factory implements Job.Factory { + + @Override + public @NonNull ForceUpdateGroupV2Job create(@NonNull Parameters parameters, @NonNull Data data) { + return new ForceUpdateGroupV2Job(parameters, GroupId.parseOrThrow(data.getString(KEY_GROUP_ID)).requireV2()); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ForceUpdateGroupV2WorkerJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ForceUpdateGroupV2WorkerJob.java new file mode 100644 index 0000000000..b84e5c291c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ForceUpdateGroupV2WorkerJob.java @@ -0,0 +1,103 @@ +package org.thoughtcrime.securesms.jobs; + +import androidx.annotation.NonNull; +import androidx.annotation.WorkerThread; + +import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.database.GroupDatabase; +import org.thoughtcrime.securesms.database.SignalDatabase; +import org.thoughtcrime.securesms.groups.GroupChangeBusyException; +import org.thoughtcrime.securesms.groups.GroupId; +import org.thoughtcrime.securesms.groups.GroupManager; +import org.thoughtcrime.securesms.groups.GroupNotAMemberException; +import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.whispersystems.signalservice.api.groupsv2.NoCredentialForRedemptionTimeException; +import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; + +import java.io.IOException; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +/** + * Scheduled by {@link ForceUpdateGroupV2Job} after message queues are drained. + * + * Forces a sanity check between local state and server state, and updates local state + * as necessary. + */ +final class ForceUpdateGroupV2WorkerJob extends BaseJob { + + public static final String KEY = "ForceUpdateGroupV2WorkerJob"; + + private static final String TAG = Log.tag(ForceUpdateGroupV2WorkerJob.class); + + private static final String KEY_GROUP_ID = "group_id"; + + private final GroupId.V2 groupId; + + ForceUpdateGroupV2WorkerJob(@NonNull GroupId.V2 groupId) { + this(new Parameters.Builder().setQueue(PushProcessMessageJob.getQueueName(Recipient.externalGroupExact(groupId).getId())) + .addConstraint(NetworkConstraint.KEY) + .setMaxAttempts(Parameters.UNLIMITED) + .build(), + groupId); + } + + private ForceUpdateGroupV2WorkerJob(@NonNull Parameters parameters, @NonNull GroupId.V2 groupId) { + super(parameters); + this.groupId = groupId; + } + + @Override + public @NonNull Data serialize() { + return new Data.Builder().putString(KEY_GROUP_ID, groupId.toString()) + .build(); + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; + } + + @Override + public void onRun() throws IOException, GroupNotAMemberException, GroupChangeBusyException { + Optional group = SignalDatabase.groups().getGroup(groupId); + + if (!group.isPresent()) { + Log.w(TAG, "Group not found"); + return; + } + + if (Recipient.externalGroupExact(groupId).isBlocked()) { + Log.i(TAG, "Not fetching group info for blocked group " + groupId); + return; + } + + GroupManager.forceSanityUpdateFromServer(context, group.get().requireV2GroupProperties().getGroupMasterKey(), System.currentTimeMillis()); + + SignalDatabase.groups().setLastForceUpdateTimestamp(group.get().getId(), System.currentTimeMillis()); + } + + @Override + public boolean onShouldRetry(@NonNull Exception e) { + return e instanceof PushNetworkException || + e instanceof NoCredentialForRedemptionTimeException || + e instanceof GroupChangeBusyException; + } + + @Override + public void onFailure() { + } + + public static final class Factory implements Job.Factory { + + @Override + public @NonNull ForceUpdateGroupV2WorkerJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new ForceUpdateGroupV2WorkerJob(parameters, + GroupId.parseOrThrow(data.getString(KEY_GROUP_ID)).requireV2()); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 03394d37f2..4549772b58 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -97,6 +97,8 @@ public static Map getJobFactories(@NonNull Application appl put(FcmRefreshJob.KEY, new FcmRefreshJob.Factory()); put(FetchRemoteMegaphoneImageJob.KEY, new FetchRemoteMegaphoneImageJob.Factory()); put(FontDownloaderJob.KEY, new FontDownloaderJob.Factory()); + put(ForceUpdateGroupV2Job.KEY, new ForceUpdateGroupV2Job.Factory()); + put(ForceUpdateGroupV2WorkerJob.KEY, new ForceUpdateGroupV2WorkerJob.Factory()); put(GiftSendJob.KEY, new GiftSendJob.Factory()); put(GroupV1MigrationJob.KEY, new GroupV1MigrationJob.Factory()); put(GroupCallUpdateSendJob.KEY, new GroupCallUpdateSendJob.Factory()); @@ -136,7 +138,7 @@ public static Map getJobFactories(@NonNull Application appl put(PaymentNotificationSendJob.KEY, new PaymentNotificationSendJob.Factory()); put(PaymentSendJob.KEY, new PaymentSendJob.Factory()); put(PaymentTransactionCheckJob.KEY, new PaymentTransactionCheckJob.Factory()); - put(PnpInitializeDevicesJob.KEY, new PnpInitializeDevicesJob.Factory()); + put(PnpInitializeDevicesJob.KEY, new PnpInitializeDevicesJob.Factory()); put(PreKeysSyncJob.KEY, new PreKeysSyncJob.Factory()); put(ProfileKeySendJob.KEY, new ProfileKeySendJob.Factory()); put(ProfileUploadJob.KEY, new ProfileUploadJob.Factory()); diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/GroupTestUtil.kt b/app/src/test/java/org/thoughtcrime/securesms/database/GroupTestUtil.kt index 83c84a711a..d9b06a25b9 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/GroupTestUtil.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/GroupTestUtil.kt @@ -190,7 +190,8 @@ fun groupRecord( masterKey.serialize(), decryptedGroup.revision, decryptedGroup.toByteArray(), - distributionId + distributionId, + System.currentTimeMillis() ) ) } diff --git a/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessorTest.kt b/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessorTest.kt index deca098ce8..f28e5a2523 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessorTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessorTest.kt @@ -3,19 +3,28 @@ package org.thoughtcrime.securesms.groups.v2.processing import android.app.Application import androidx.test.core.app.ApplicationProvider import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.both +import org.hamcrest.Matchers.hasItem +import org.hamcrest.Matchers.hasProperty import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.notNullValue import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.anyLong import org.mockito.ArgumentMatchers.eq import org.mockito.Mockito.any +import org.mockito.Mockito.doCallRealMethod import org.mockito.Mockito.doReturn import org.mockito.Mockito.isA import org.mockito.Mockito.mock import org.mockito.Mockito.reset import org.mockito.Mockito.verify +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doNothing import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config import org.signal.core.util.Hex.fromStringCondensed @@ -25,11 +34,13 @@ import org.signal.libsignal.zkgroup.groups.GroupMasterKey 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.storageservice.protos.groups.local.DecryptedString import org.signal.storageservice.protos.groups.local.DecryptedTimer import org.thoughtcrime.securesms.SignalStoreRule import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.database.GroupStateTestData import org.thoughtcrime.securesms.database.RecipientDatabase +import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context import org.thoughtcrime.securesms.database.model.databaseprotos.member import org.thoughtcrime.securesms.database.model.databaseprotos.requestingMember import org.thoughtcrime.securesms.database.setNewDescription @@ -104,6 +115,7 @@ class GroupsV2StateProcessorTest { } } doReturn(testPartial).`when`(groupsV2API).getPartialDecryptedGroup(any(), any()) + doReturn(serverState).`when`(groupsV2API).getGroup(any(), any()) } data.changeSet?.let { changeSet -> @@ -453,4 +465,118 @@ class GroupsV2StateProcessorTest { assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED)) assertThat("revision matches latest revision on server", result.latestServer!!.revision, `is`(101)) } + + /** + * If for some reason we missed a member being added in our local state, and then we preform a multi-revision update, + * we should now know about the member and add update messages to the chat. + */ + @Test + fun missedMemberAddResolvesWithMultipleRevisionUpdate() { + val secondOther = member(ServiceId.from(UUID.randomUUID())) + + val updateMessageContextCapture = ArgumentCaptor.forClass(DecryptedGroupV2Context::class.java) + profileAndMessageHelper.masterKey = masterKey + doCallRealMethod().`when`(profileAndMessageHelper).insertUpdateMessages(anyLong(), anyOrNull(), any()) + doNothing().`when`(profileAndMessageHelper).storeMessage(updateMessageContextCapture.capture(), anyLong()) + + given { + localState( + revision = 8, + title = "Whatever", + members = selfAndOthers + ) + serverState( + revision = 10, + title = "Changed", + members = selfAndOthers + secondOther + ) + changeSet { + changeLog(9) { + change { + setNewTitle("Mid-Change") + } + fullSnapshot( + title = "Mid-Change", + members = selfAndOthers + secondOther + ) + } + changeLog(10) { + change { + setNewTitle("Changed") + } + } + } + apiCallParameters(requestedRevision = 8, includeFirst = true) + } + + val result = processor.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, 0, null) + assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED)) + assertThat("members contains second other", result.latestServer!!.membersList, hasItem(secondOther)) + + val allUpdateMessageContexts = updateMessageContextCapture.allValues + assertThat("group update messages contains new member add", allUpdateMessageContexts.map { it.change.newMembersList }, hasItem(hasItem(secondOther))) + } + + /** + * If for some reason we missed a member being added in our local state, and then we preform a forced sanity update, + * we should now know about the member and any other changes, and add update messages to the chat. + */ + @Test + fun missedMemberAddResolvesWithForcedUpdate() { + val secondOther = member(ServiceId.from(UUID.randomUUID())) + + val updateMessageContextCapture = ArgumentCaptor.forClass(DecryptedGroupV2Context::class.java) + profileAndMessageHelper.masterKey = masterKey + doCallRealMethod().`when`(profileAndMessageHelper).insertUpdateMessages(anyLong(), anyOrNull(), any()) + doNothing().`when`(profileAndMessageHelper).storeMessage(updateMessageContextCapture.capture(), anyLong()) + + given { + localState( + revision = 10, + title = "Title", + members = selfAndOthers + ) + serverState( + revision = 10, + title = "Changed", + members = selfAndOthers + secondOther + ) + } + + val result = processor.forceSanityUpdateFromServer(0) + assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED)) + assertThat("members contains second other", result.latestServer!!.membersList, hasItem(secondOther)) + assertThat("title should be updated", result.latestServer!!.title, `is`("Changed")) + + val allUpdateMessageContexts = updateMessageContextCapture.allValues + assertThat("group update messages contains new member add", allUpdateMessageContexts.map { it.change.newMembersList }, hasItem(hasItem(secondOther))) + + assertThat( + "group update messages contains title change", + allUpdateMessageContexts.map { it.change.newTitle }, + hasItem(both(notNullValue()).and(hasProperty("value", `is`("Changed")))) + ) + } + + /** + * If we preform a forced sanity update, with no differences between local and server, then it should be no-op. + */ + @Test + fun noDifferencesNoOpsWithForcedUpdate() { + given { + localState( + revision = 10, + title = "Title", + members = selfAndOthers + ) + serverState( + revision = 10, + title = "Title", + members = selfAndOthers + ) + } + + val result = processor.forceSanityUpdateFromServer(0) + assertThat("local should be unchanged", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_CONSISTENT_OR_AHEAD)) + } } From 23ba5c874a7c2a97e20d65924f715820796d0089 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 5 Oct 2022 16:00:48 -0400 Subject: [PATCH 23/78] Improve styling of ChooseGroupStoryBottomSheet. --- .../components/settings/DSLSettingsText.kt | 1 + .../paged/ContactSearchConfiguration.kt | 1 + .../contacts/paged/ContactSearchData.kt | 2 +- .../contacts/paged/ContactSearchItems.kt | 14 ++++++- .../paged/ContactSearchPagedDataSource.kt | 2 +- .../v2/stories/ChooseGroupStoryBottomSheet.kt | 16 +++---- .../v2/stories/ChooseStoryTypeBottomSheet.kt | 24 +++++------ .../contact_selection_checkbox_dialog.xml | 2 +- .../contact_selection_checkbox_dialog.xml | 2 +- .../rounded_rectangle_secondary_22.xml | 5 +++ .../stories_choose_group_bottom_bar.xml | 42 +++++++------------ .../stories_choose_group_bottom_sheet.xml | 8 ++-- app/src/main/res/values/strings.xml | 5 +++ 13 files changed, 66 insertions(+), 58 deletions(-) create mode 100644 app/src/main/res/drawable/rounded_rectangle_secondary_22.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsText.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsText.kt index 6c70a78257..3ff3ba08e0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsText.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsText.kt @@ -66,6 +66,7 @@ sealed class DSLSettingsText { } object TitleLargeModifier : TextAppearanceModifier(R.style.Signal_Text_TitleLarge) + object TitleMediumModifier : TextAppearanceModifier(R.style.Signal_Text_TitleMedium) object Body1BoldModifier : TextAppearanceModifier(R.style.TextAppearance_Signal_Body1_Bold) open class TextAppearanceModifier(@StyleRes private val textAppearance: Int) : Modifier { diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchConfiguration.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchConfiguration.kt index 3a743af946..1061b55e39 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchConfiguration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchConfiguration.kt @@ -64,6 +64,7 @@ class ContactSearchConfiguration private constructor( val includeInactive: Boolean = false, val returnAsGroupStories: Boolean = false, val sortOrder: ContactSearchSortOrder = ContactSearchSortOrder.NATURAL, + val shortSummary: Boolean = false, override val includeHeader: Boolean, override val expandConfig: ExpandConfig? = null ) : Section(SectionKey.GROUPS) diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchData.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchData.kt index d835ad3c83..6ec9937934 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchData.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchData.kt @@ -23,7 +23,7 @@ sealed class ContactSearchData(val contactSearchKey: ContactSearchKey) { /** * A row displaying a known recipient. */ - data class KnownRecipient(val recipient: Recipient) : ContactSearchData(ContactSearchKey.RecipientSearchKey.KnownRecipient(recipient.id)) + data class KnownRecipient(val recipient: Recipient, val shortSummary: Boolean = false) : ContactSearchData(ContactSearchKey.RecipientSearchKey.KnownRecipient(recipient.id)) /** * A row containing a title for a given section diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchItems.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchItems.kt index cf60e759aa..bc640c45ca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchItems.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchItems.kt @@ -73,7 +73,7 @@ object ContactSearchItems { contactSearchData.filterNotNull().map { when (it) { is ContactSearchData.Story -> StoryModel(it, selection.contains(it.contactSearchKey), SignalStore.storyValues().userHasBeenNotifiedAboutStories) - is ContactSearchData.KnownRecipient -> RecipientModel(it, selection.contains(it.contactSearchKey)) + is ContactSearchData.KnownRecipient -> RecipientModel(it, selection.contains(it.contactSearchKey), it.shortSummary) is ContactSearchData.Expand -> ExpandModel(it) is ContactSearchData.Header -> HeaderModel(it) is ContactSearchData.TestRow -> error("This row exists for testing only.") @@ -207,7 +207,7 @@ object ContactSearchItems { /** * Recipient model */ - private class RecipientModel(val knownRecipient: ContactSearchData.KnownRecipient, val isSelected: Boolean) : MappingModel { + private class RecipientModel(val knownRecipient: ContactSearchData.KnownRecipient, val isSelected: Boolean, val shortSummary: Boolean) : MappingModel { override fun areItemsTheSame(newItem: RecipientModel): Boolean { return newItem.knownRecipient == knownRecipient @@ -230,6 +230,16 @@ object ContactSearchItems { override fun isSelected(model: RecipientModel): Boolean = model.isSelected override fun getData(model: RecipientModel): ContactSearchData.KnownRecipient = model.knownRecipient override fun getRecipient(model: RecipientModel): Recipient = model.knownRecipient.recipient + override fun bindNumberField(model: RecipientModel) { + val recipient = getRecipient(model) + + if (model.shortSummary && recipient.isGroup) { + val count = recipient.participantIds.size + number.setText(context.resources.getQuantityString(R.plurals.ContactSearchItems__group_d_members, count, count)) + } else { + super.bindNumberField(model) + } + } } /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSource.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSource.kt index 28c4287a94..fc2e9071d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSource.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSource.kt @@ -219,7 +219,7 @@ class ContactSearchPagedDataSource( if (section.returnAsGroupStories) { ContactSearchData.Story(contactSearchPagedDataSourceRepository.getRecipientFromGroupRecord(it), 0, DistributionListPrivacyMode.ALL) } else { - ContactSearchData.KnownRecipient(contactSearchPagedDataSourceRepository.getRecipientFromGroupRecord(it)) + ContactSearchData.KnownRecipient(contactSearchPagedDataSourceRepository.getRecipientFromGroupRecord(it), shortSummary = section.shortSummary) } } ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/ChooseGroupStoryBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/ChooseGroupStoryBottomSheet.kt index e91ebd9636..f8cbb1cc0c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/ChooseGroupStoryBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/ChooseGroupStoryBottomSheet.kt @@ -34,7 +34,6 @@ class ChooseGroupStoryBottomSheet : FixedRoundedCornerBottomSheetDialogFragment( private lateinit var confirmButton: View private lateinit var selectedList: RecyclerView - private lateinit var backgroundHelper: View private lateinit var divider: View private lateinit var mediator: ContactSearchMediator @@ -52,7 +51,6 @@ class ChooseGroupStoryBottomSheet : FixedRoundedCornerBottomSheetDialogFragment( confirmButton = bottomBar.findViewById(R.id.share_confirm) selectedList = bottomBar.findViewById(R.id.selected_list) - backgroundHelper = bottomBar.findViewById(R.id.background_helper) divider = bottomBar.findViewById(R.id.divider) val adapter = ShareSelectionAdapter() @@ -75,7 +73,7 @@ class ChooseGroupStoryBottomSheet : FixedRoundedCornerBottomSheetDialogFragment( addSection( ContactSearchConfiguration.Section.Groups( includeHeader = false, - returnAsGroupStories = true, + shortSummary = true, sortOrder = ContactSearchSortOrder.RECENCY ) ) @@ -86,7 +84,7 @@ class ChooseGroupStoryBottomSheet : FixedRoundedCornerBottomSheetDialogFragment( mediator.getSelectionState().observe(viewLifecycleOwner) { state -> adapter.submitList( - state.filterIsInstance(ContactSearchKey.RecipientSearchKey.Story::class.java) + state.filterIsInstance(ContactSearchKey.RecipientSearchKey.KnownRecipient::class.java) .map { it.recipientId } .mapIndexed { index, recipientId -> ShareSelectionMappingModel( @@ -118,9 +116,8 @@ class ChooseGroupStoryBottomSheet : FixedRoundedCornerBottomSheetDialogFragment( animatorSet?.cancel() animatorSet = AnimatorSet().apply { playTogether( - ObjectAnimator.ofFloat(confirmButton, View.ALPHA, 1f), + ObjectAnimator.ofFloat(confirmButton, View.TRANSLATION_Y, 0f), ObjectAnimator.ofFloat(selectedList, View.TRANSLATION_Y, 0f), - ObjectAnimator.ofFloat(backgroundHelper, View.TRANSLATION_Y, 0f), ObjectAnimator.ofFloat(divider, View.TRANSLATION_Y, 0f) ) start() @@ -128,14 +125,13 @@ class ChooseGroupStoryBottomSheet : FixedRoundedCornerBottomSheetDialogFragment( } private fun animateOutBottomBar() { - val translationY = DimensionUnit.DP.toPixels(48f) + val translationY = DimensionUnit.SP.toPixels(64f) animatorSet?.cancel() animatorSet = AnimatorSet().apply { playTogether( - ObjectAnimator.ofFloat(confirmButton, View.ALPHA, 0f), + ObjectAnimator.ofFloat(confirmButton, View.TRANSLATION_Y, translationY), ObjectAnimator.ofFloat(selectedList, View.TRANSLATION_Y, translationY), - ObjectAnimator.ofFloat(backgroundHelper, View.TRANSLATION_Y, translationY), ObjectAnimator.ofFloat(divider, View.TRANSLATION_Y, translationY) ) start() @@ -150,7 +146,7 @@ class ChooseGroupStoryBottomSheet : FixedRoundedCornerBottomSheetDialogFragment( RESULT_SET, ArrayList( mediator.getSelectedContacts() - .filterIsInstance(ContactSearchKey.RecipientSearchKey.Story::class.java) + .filterIsInstance(ContactSearchKey.RecipientSearchKey.KnownRecipient::class.java) .map { it.recipientId } ) ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/ChooseStoryTypeBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/ChooseStoryTypeBottomSheet.kt index f3e0a7cf33..f5b5e49f34 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/ChooseStoryTypeBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/ChooseStoryTypeBottomSheet.kt @@ -12,7 +12,7 @@ import org.thoughtcrime.securesms.components.settings.conversation.preferences.L import org.thoughtcrime.securesms.util.fragments.requireListener class ChooseStoryTypeBottomSheet : DSLSettingsBottomSheetFragment( - layoutId = R.layout.dsl_settings_bottom_sheet_no_handle + layoutId = R.layout.dsl_settings_bottom_sheet ) { override fun bindAdapter(adapter: DSLSettingsAdapter) { LargeIconClickPreference.register(adapter) @@ -24,7 +24,7 @@ class ChooseStoryTypeBottomSheet : DSLSettingsBottomSheetFragment( textPref( title = DSLSettingsText.from( stringId = R.string.ChooseStoryTypeBottomSheet__choose_your_story_type, - DSLSettingsText.CenterModifier, DSLSettingsText.Body1BoldModifier, DSLSettingsText.BoldModifier + DSLSettingsText.CenterModifier, DSLSettingsText.TitleMediumModifier ) ) @@ -37,11 +37,11 @@ class ChooseStoryTypeBottomSheet : DSLSettingsBottomSheetFragment( stringId = R.string.ChooseStoryTypeBottomSheet__visible_only_to ), icon = DSLSettingsIcon.from( - R.drawable.ic_plus_24, - R.color.signal_icon_tint_primary, - R.drawable.circle_tintable, - R.color.signal_button_secondary_ripple, - DimensionUnit.DP.toPixels(8f).toInt() + iconId = R.drawable.ic_plus_24, + iconTintId = R.color.signal_colorOnSurface, + backgroundId = R.drawable.circle_tintable, + backgroundTint = R.color.signal_colorSurface5, + insetPx = DimensionUnit.DP.toPixels(8f).toInt() ), onClick = { dismissAllowingStateLoss() @@ -59,11 +59,11 @@ class ChooseStoryTypeBottomSheet : DSLSettingsBottomSheetFragment( stringId = R.string.ChooseStoryTypeBottomSheet__share_to_an_existing_group ), icon = DSLSettingsIcon.from( - R.drawable.ic_group_outline_24, - R.color.signal_icon_tint_primary, - R.drawable.circle_tintable, - R.color.signal_button_secondary_ripple, - DimensionUnit.DP.toPixels(8f).toInt() + iconId = R.drawable.ic_group_outline_24, + iconTintId = R.color.signal_colorOnSurface, + backgroundId = R.drawable.circle_tintable, + backgroundTint = R.color.signal_colorSurface5, + insetPx = DimensionUnit.DP.toPixels(8f).toInt() ), onClick = { dismissAllowingStateLoss() diff --git a/app/src/main/res/drawable-night/contact_selection_checkbox_dialog.xml b/app/src/main/res/drawable-night/contact_selection_checkbox_dialog.xml index bc6f9d9588..feb423fdb4 100644 --- a/app/src/main/res/drawable-night/contact_selection_checkbox_dialog.xml +++ b/app/src/main/res/drawable-night/contact_selection_checkbox_dialog.xml @@ -15,7 +15,7 @@ - + diff --git a/app/src/main/res/drawable/contact_selection_checkbox_dialog.xml b/app/src/main/res/drawable/contact_selection_checkbox_dialog.xml index 43f5c486e0..ca684a5346 100644 --- a/app/src/main/res/drawable/contact_selection_checkbox_dialog.xml +++ b/app/src/main/res/drawable/contact_selection_checkbox_dialog.xml @@ -15,7 +15,7 @@ - + diff --git a/app/src/main/res/drawable/rounded_rectangle_secondary_22.xml b/app/src/main/res/drawable/rounded_rectangle_secondary_22.xml new file mode 100644 index 0000000000..213e46e417 --- /dev/null +++ b/app/src/main/res/drawable/rounded_rectangle_secondary_22.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/stories_choose_group_bottom_bar.xml b/app/src/main/res/layout/stories_choose_group_bottom_bar.xml index c29eaa0cca..9c1e40e3dc 100644 --- a/app/src/main/res/layout/stories_choose_group_bottom_bar.xml +++ b/app/src/main/res/layout/stories_choose_group_bottom_bar.xml @@ -10,23 +10,11 @@ + android:layout_height="4dp" + android:background="@drawable/bottom_toolbar_shadow" + android:translationY="64sp" + app:layout_constraintTop_toTopOf="parent" /> - - \ No newline at end of file diff --git a/app/src/main/res/layout/stories_choose_group_bottom_sheet.xml b/app/src/main/res/layout/stories_choose_group_bottom_sheet.xml index c268f77be7..cedb998b93 100644 --- a/app/src/main/res/layout/stories_choose_group_bottom_sheet.xml +++ b/app/src/main/res/layout/stories_choose_group_bottom_sheet.xml @@ -22,8 +22,7 @@ android:layout_marginTop="12dp" android:gravity="center" android:text="@string/ChooseGroupStoryBottomSheet__choose_groups" - android:textAppearance="@style/Signal.Text.Body" - android:textStyle="bold" /> + android:textAppearance="@style/Signal.Text.TitleMedium" /> + android:textColor="@color/signal_colorOnSurfaceVariant" + app:backgroundTint="@color/signal_colorSurface5" /> Group story · %1$d viewer Group story · %1$d viewers + + + %1$d member + %1$d members + %1$s · %2$d viewer From 44d407563633a4caaa286ecc48f51610ae13fe92 Mon Sep 17 00:00:00 2001 From: Jim Gustafson Date: Wed, 5 Oct 2022 14:36:15 -0700 Subject: [PATCH 24/78] Update to RingRTC v2.21.2 --- dependencies.gradle | 2 +- gradle/verification-metadata.xml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dependencies.gradle b/dependencies.gradle index 3489f8a78a..65a8af7345 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -89,7 +89,7 @@ dependencyResolutionManagement { alias('libsignal-android').to('org.signal', 'libsignal-android').versionRef('libsignal-client') alias('signal-aesgcmprovider').to('org.signal:aesgcmprovider:0.0.3') alias('signal-argon2').to('org.signal:argon2:13.1') - alias('signal-ringrtc').to('org.signal:ringrtc-android:2.21.1') + alias('signal-ringrtc').to('org.signal:ringrtc-android:2.21.2') alias('signal-android-database-sqlcipher').to('org.signal:android-database-sqlcipher:4.4.3-S8') // Third Party diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index e6fbf16fa0..6f35653c8c 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -3834,12 +3834,12 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - + + + - - + + From 293bc2da47acd7a519ec2afefa4e6c8abd7697a7 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 5 Oct 2022 17:37:47 -0400 Subject: [PATCH 25/78] Rotate the stories feature flag. --- .../main/java/org/thoughtcrime/securesms/util/FeatureFlags.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index a9c1eed640..554190ab1f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -82,7 +82,7 @@ public final class FeatureFlags { private static final String RETRY_RECEIPTS = "android.retryReceipts"; private static final String MAX_GROUP_CALL_RING_SIZE = "global.calling.maxGroupCallRingSize"; private static final String GROUP_CALL_RINGING = "android.calling.groupCallRinging"; - private static final String STORIES = "android.stories.2"; + private static final String STORIES = "android.stories.3"; private static final String STORIES_TEXT_FUNCTIONS = "android.stories.text.functions"; private static final String HARDWARE_AEC_BLOCKLIST_MODELS = "android.calling.hardwareAecBlockList"; private static final String SOFTWARE_AEC_BLOCKLIST_MODELS = "android.calling.softwareAecBlockList"; From 1fc119e027dbb2f4c18a947d2a735d88d126804c Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 5 Oct 2022 18:05:52 -0400 Subject: [PATCH 26/78] Fix lifespan of RefreshAttributesJob. --- .../org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java index 524301f081..1aec897d6b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java @@ -50,7 +50,7 @@ public RefreshAttributesJob(boolean forced) { .addConstraint(NetworkConstraint.KEY) .setQueue("RefreshAttributesJob") .setMaxInstancesForFactory(2) - .setLifespan(TimeUnit.DAYS.toDays(30)) + .setLifespan(TimeUnit.DAYS.toMillis(30)) .setMaxAttempts(Parameters.UNLIMITED) .build(), forced); From f9a4b7cf12cb97b80f80fa20ab7bb451c784e4aa Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 5 Oct 2022 17:43:54 -0400 Subject: [PATCH 27/78] Bump version to 5.52.0 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2b53d7d205..b06103cf0e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -57,8 +57,8 @@ ktlint { version = "0.43.2" } -def canonicalVersionCode = 1136 -def canonicalVersionName = "5.51.7" +def canonicalVersionCode = 1137 +def canonicalVersionName = "5.52.0" def postFixSize = 100 def abiPostFix = ['universal' : 0, From 9946da2cecb9b7b21a319888797b9f8dfee917d6 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 5 Oct 2022 19:49:29 -0400 Subject: [PATCH 28/78] Fix crash when fetching messages. --- .../signalservice/internal/push/PushServiceSocket.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index 43d1cdcd03..b59ac364ab 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -220,7 +220,7 @@ public class PushServiceSocket { private static final String DEVICE_PATH = "/v1/devices/%s"; private static final String DIRECTORY_AUTH_PATH = "/v1/directory/auth"; - private static final String MESSAGE_PATH = "/v1/messages/%s?story=%s"; + private static final String MESSAGE_PATH = "/v1/messages/%s"; private static final String GROUP_MESSAGE_PATH = "/v1/messages/multi_recipient?ts=%s&online=%s&urgent=%s&story=%s"; private static final String SENDER_ACK_MESSAGE_PATH = "/v1/messages/%s/%d"; private static final String UUID_ACK_MESSAGE_PATH = "/v1/messages/uuid/%s"; @@ -548,7 +548,7 @@ public SendMessageResponse sendMessage(OutgoingPushMessageList bundle, Optional< throws IOException { try { - String responseText = makeServiceRequest(String.format(MESSAGE_PATH, bundle.getDestination(), story ? "true" : "false"), "PUT", JsonUtil.toJson(bundle), NO_HEADERS, unidentifiedAccess); + String responseText = makeServiceRequest(String.format("/v1/messages/%s?story=%s", bundle.getDestination(), story ? "true" : "false"), "PUT", JsonUtil.toJson(bundle), NO_HEADERS, unidentifiedAccess); SendMessageResponse response = JsonUtil.fromJson(responseText, SendMessageResponse.class); response.setSentUnidentfied(unidentifiedAccess.isPresent()); From 1fe4c45c44b6a6ce84c4c56f89c75b923b50ce46 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 5 Oct 2022 19:49:51 -0400 Subject: [PATCH 29/78] Bump version to 5.52.1 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index b06103cf0e..fe87bf35da 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -57,8 +57,8 @@ ktlint { version = "0.43.2" } -def canonicalVersionCode = 1137 -def canonicalVersionName = "5.52.0" +def canonicalVersionCode = 1138 +def canonicalVersionName = "5.52.1" def postFixSize = 100 def abiPostFix = ['universal' : 0, From ec46d6039d1e2bf8c2a96a5871f50b5d96f83c09 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 6 Oct 2022 09:55:49 -0300 Subject: [PATCH 30/78] Fix crash when trying to share a text story. --- .../stories/dialogs/StoryContextMenu.kt | 25 +++- .../stories/dialogs/StoryContextMenuTest.kt | 130 ++++++++++++++++++ 2 files changed, 149 insertions(+), 6 deletions(-) create mode 100644 app/src/test/java/org/thoughtcrime/securesms/stories/dialogs/StoryContextMenuTest.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryContextMenu.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryContextMenu.kt index a0b356ac82..0dadf9eca3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryContextMenu.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryContextMenu.kt @@ -19,9 +19,11 @@ import org.thoughtcrime.securesms.components.menu.ActionItem import org.thoughtcrime.securesms.components.menu.SignalContextMenu import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost import org.thoughtcrime.securesms.stories.landing.StoriesLandingItem import org.thoughtcrime.securesms.stories.viewer.page.StoryPost import org.thoughtcrime.securesms.stories.viewer.page.StoryViewerPageState +import org.thoughtcrime.securesms.util.Base64 import org.thoughtcrime.securesms.util.DeleteDialog import org.thoughtcrime.securesms.util.SaveAttachmentTask @@ -61,12 +63,23 @@ object StoryContextMenu { } fun share(fragment: Fragment, messageRecord: MediaMmsMessageRecord) { - val attachment: Attachment = messageRecord.slideDeck.firstSlide!!.asAttachment() - val intent: Intent = ShareCompat.IntentBuilder(fragment.requireContext()) - .setStream(attachment.publicUri) - .setType(attachment.contentType) - .createChooserIntent() - .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + val intent = if (messageRecord.storyType.isTextStory) { + val textStoryBody = StoryTextPost.parseFrom(Base64.decode(messageRecord.body)).body + val linkUrl = messageRecord.linkPreviews.firstOrNull()?.url ?: "" + val shareText = "${textStoryBody} $linkUrl".trim() + + ShareCompat.IntentBuilder(fragment.requireContext()) + .setText(shareText) + .createChooserIntent() + } else { + val attachment: Attachment = messageRecord.slideDeck.firstSlide!!.asAttachment() + + ShareCompat.IntentBuilder(fragment.requireContext()) + .setStream(attachment.publicUri) + .setType(attachment.contentType) + .createChooserIntent() + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } try { fragment.startActivity(intent) diff --git a/app/src/test/java/org/thoughtcrime/securesms/stories/dialogs/StoryContextMenuTest.kt b/app/src/test/java/org/thoughtcrime/securesms/stories/dialogs/StoryContextMenuTest.kt new file mode 100644 index 0000000000..f2acfad664 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/stories/dialogs/StoryContextMenuTest.kt @@ -0,0 +1,130 @@ +package org.thoughtcrime.securesms.stories.dialogs + +import android.app.Application +import android.content.Context +import android.content.Intent +import androidx.fragment.app.Fragment +import androidx.test.core.app.ApplicationProvider +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.thoughtcrime.securesms.attachments.AttachmentId +import org.thoughtcrime.securesms.database.FakeMessageRecords +import org.thoughtcrime.securesms.database.model.StoryType +import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost +import org.thoughtcrime.securesms.linkpreview.LinkPreview +import org.thoughtcrime.securesms.mms.ImageSlide +import org.thoughtcrime.securesms.mms.PartAuthority +import org.thoughtcrime.securesms.mms.SlideDeck +import org.thoughtcrime.securesms.util.MediaUtil +import org.whispersystems.util.Base64 +import java.util.Optional + +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class StoryContextMenuTest { + + private val context: Context = ApplicationProvider.getApplicationContext() + private val intentCaptor = argumentCaptor() + private val fragment: Fragment = mock { + on { requireContext() } doReturn context + } + + @Test + fun `Given a story with an attachment, when I share, then I expect the correct stream, type, and flag`() { + // GIVEN + val attachmentId = AttachmentId(1, 2) + val storyRecord = FakeMessageRecords.buildMediaMmsMessageRecord( + storyType = StoryType.STORY_WITH_REPLIES, + slideDeck = SlideDeck().apply { + addSlide( + ImageSlide( + context, + FakeMessageRecords.buildDatabaseAttachment( + attachmentId = attachmentId + ) + ) + ) + } + ) + + // WHEN + StoryContextMenu.share(fragment, storyRecord) + + // THEN + verify(fragment).startActivity(intentCaptor.capture()) + val chooserIntent: Intent = intentCaptor.firstValue + val targetIntent: Intent = chooserIntent.getParcelableExtra(Intent.EXTRA_INTENT)!! + assertEquals(PartAuthority.getAttachmentPublicUri(PartAuthority.getAttachmentDataUri(attachmentId)), targetIntent.getParcelableExtra(Intent.EXTRA_STREAM)) + assertEquals(MediaUtil.IMAGE_JPEG, targetIntent.type) + assertTrue(Intent.FLAG_GRANT_READ_URI_PERMISSION and chooserIntent.flags == Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + + @Test + fun `Given a story with a text, when I share, then I expect the correct text`() { + // GIVEN + val expected = "Hello" + val storyRecord = FakeMessageRecords.buildMediaMmsMessageRecord( + storyType = StoryType.TEXT_STORY_WITH_REPLIES, + body = Base64.encodeBytes(StoryTextPost.newBuilder().setBody(expected).build().toByteArray()) + ) + + // WHEN + StoryContextMenu.share(fragment, storyRecord) + + // THEN + verify(fragment).startActivity(intentCaptor.capture()) + val chooserIntent: Intent = intentCaptor.firstValue + val targetIntent: Intent = chooserIntent.getParcelableExtra(Intent.EXTRA_INTENT)!! + assertEquals(expected, targetIntent.getStringExtra(Intent.EXTRA_TEXT)) + } + + @Test + fun `Given a story with a link, when I share, then I expect the correct text`() { + // GIVEN + val expected = "https://www.signal.org" + val storyRecord = FakeMessageRecords.buildMediaMmsMessageRecord( + storyType = StoryType.TEXT_STORY_WITH_REPLIES, + body = Base64.encodeBytes(StoryTextPost.newBuilder().build().toByteArray()), + linkPreviews = listOf(LinkPreview(expected, "", "", 0L, Optional.empty())) + ) + + // WHEN + StoryContextMenu.share(fragment, storyRecord) + + // THEN + verify(fragment).startActivity(intentCaptor.capture()) + val chooserIntent: Intent = intentCaptor.firstValue + val targetIntent: Intent = chooserIntent.getParcelableExtra(Intent.EXTRA_INTENT)!! + assertEquals(expected, targetIntent.getStringExtra(Intent.EXTRA_TEXT)) + } + + @Test + fun `Given a story with a text and a link, when I share, then I expect the correct text`() { +// GIVEN + val url = "https://www.signal.org" + val text = "hello" + val expected = "$text $url" + val storyRecord = FakeMessageRecords.buildMediaMmsMessageRecord( + storyType = StoryType.TEXT_STORY_WITH_REPLIES, + body = Base64.encodeBytes(StoryTextPost.newBuilder().setBody(text).build().toByteArray()), + linkPreviews = listOf(LinkPreview(url, "", "", 0L, Optional.empty())) + ) + + // WHEN + StoryContextMenu.share(fragment, storyRecord) + + // THEN + verify(fragment).startActivity(intentCaptor.capture()) + val chooserIntent: Intent = intentCaptor.firstValue + val targetIntent: Intent = chooserIntent.getParcelableExtra(Intent.EXTRA_INTENT)!! + assertEquals(expected, targetIntent.getStringExtra(Intent.EXTRA_TEXT)) + } +} \ No newline at end of file From 486e172aee5891040a3898c22298677d2fa0a544 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 6 Oct 2022 10:02:19 -0300 Subject: [PATCH 31/78] Fix crash when naturally finishing story set. --- .../securesms/stories/viewer/StoryViewerFragment.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerFragment.kt index 6c1201bd6e..e4d355b6fc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerFragment.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.stories.viewer import android.os.Bundle import android.view.View +import androidx.core.app.ActivityCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.viewpager2.widget.ViewPager2 @@ -75,7 +76,7 @@ class StoryViewerFragment : lifecycleDisposable.bindTo(viewLifecycleOwner) lifecycleDisposable += viewModel.state.observeOn(AndroidSchedulers.mainThread()).subscribe { state -> if (state.noPosts) { - requireActivity().finish() + ActivityCompat.finishAfterTransition(requireActivity()) } adapter.setPages(state.pages) @@ -86,7 +87,7 @@ class StoryViewerFragment : pagerOnPageSelectedLock = false if (state.page >= state.pages.size) { - requireActivity().onBackPressed() + ActivityCompat.finishAfterTransition(requireActivity()) } } From cc5aab6be35a481d2936998512f8a8b20836c2c9 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 6 Oct 2022 10:04:14 -0300 Subject: [PATCH 32/78] Fix display of story disable dialog. --- .../org/thoughtcrime/securesms/stories/dialogs/StoryDialogs.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryDialogs.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryDialogs.kt index 73843a2878..6bccdad5e5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryDialogs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryDialogs.kt @@ -38,6 +38,8 @@ object StoryDialogs { .setTitle(R.string.StoriesPrivacySettingsFragment__turn_off_stories_question) .setMessage(R.string.StoriesPrivacySettingsFragment__you_will_no_longer_be_able_to_share) .setPositiveButton(positiveButtonMessage) { _, _ -> onDisable() } + .setNegativeButton(android.R.string.cancel) { _, _ -> } + .show() } fun displayBetaDialog(context: Context, onConfirmed: () -> Unit) { From 35f1baf965fd73d1626028f7b1c9094c8e1f180b Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 6 Oct 2022 10:34:39 -0300 Subject: [PATCH 33/78] Add group story removal dialog. --- .../app/internal/StoryDialogLauncherFragment.kt | 9 +++++++++ .../securesms/stories/dialogs/StoryContextMenu.kt | 2 +- .../securesms/stories/dialogs/StoryDialogs.kt | 13 +++++++++++++ .../settings/group/GroupStorySettingsFragment.kt | 8 +++++++- app/src/main/res/values/strings.xml | 7 +++++++ .../stories/dialogs/StoryContextMenuTest.kt | 2 +- 6 files changed, 38 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/StoryDialogLauncherFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/StoryDialogLauncherFragment.kt index bc5dff2097..bd90db9c98 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/StoryDialogLauncherFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/StoryDialogLauncherFragment.kt @@ -16,6 +16,15 @@ class StoryDialogLauncherFragment : DSLSettingsFragment(titleId = R.string.prefe private fun getConfiguration(): DSLConfiguration { return configure { + clickPref( + title = DSLSettingsText.from(R.string.preferences__internal_remove_group_story), + onClick = { + StoryDialogs.removeGroupStory(requireContext(), "Family") { + Toast.makeText(requireContext(), R.string.preferences__internal_remove_group_story, Toast.LENGTH_SHORT).show() + } + } + ) + clickPref( title = DSLSettingsText.from(R.string.preferences__internal_retry_send), onClick = { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryContextMenu.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryContextMenu.kt index 0dadf9eca3..87f3850048 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryContextMenu.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryContextMenu.kt @@ -66,7 +66,7 @@ object StoryContextMenu { val intent = if (messageRecord.storyType.isTextStory) { val textStoryBody = StoryTextPost.parseFrom(Base64.decode(messageRecord.body)).body val linkUrl = messageRecord.linkPreviews.firstOrNull()?.url ?: "" - val shareText = "${textStoryBody} $linkUrl".trim() + val shareText = "$textStoryBody $linkUrl".trim() ShareCompat.IntentBuilder(fragment.requireContext()) .setText(shareText) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryDialogs.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryDialogs.kt index 6bccdad5e5..1844aa3b94 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryDialogs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryDialogs.kt @@ -10,6 +10,19 @@ import org.thoughtcrime.securesms.R object StoryDialogs { + fun removeGroupStory( + context: Context, + groupName: String, + onConfirmed: () -> Unit + ) { + MaterialAlertDialogBuilder(context) + .setTitle(R.string.StoryDialogs__remove_group_story) + .setMessage(context.getString(R.string.StoryDialogs__s_will_be_removed, groupName)) + .setPositiveButton(R.string.StoryDialogs__remove) { _, _ -> onConfirmed() } + .setNegativeButton(android.R.string.cancel) { _, _ -> } + .show() + } + fun deleteDistributionList( context: Context, distributionListName: String, diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/group/GroupStorySettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/group/GroupStorySettingsFragment.kt index f480d213ae..7b11dd9ab5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/group/GroupStorySettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/group/GroupStorySettingsFragment.kt @@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.conversation.ConversationIntents +import org.thoughtcrime.securesms.stories.dialogs.StoryDialogs import org.thoughtcrime.securesms.stories.settings.custom.PrivateStoryItem import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter @@ -94,7 +95,12 @@ class GroupStorySettingsFragment : DSLSettingsFragment(menuId = R.menu.story_gro DSLSettingsText.ColorModifier(ContextCompat.getColor(requireContext(), R.color.signal_colorError)) ), onClick = { - viewModel.doNotDisplayAsStory() + StoryDialogs.removeGroupStory( + requireContext(), + state.name + ) { + viewModel.doNotDisplayAsStory() + } } ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 99d4afa145..1c49ca9e85 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2761,6 +2761,7 @@ Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -4940,6 +4941,12 @@ Only share with… Done + + Remove group story? + + \"%1$s\" will be removed. + + Remove Delete private story? diff --git a/app/src/test/java/org/thoughtcrime/securesms/stories/dialogs/StoryContextMenuTest.kt b/app/src/test/java/org/thoughtcrime/securesms/stories/dialogs/StoryContextMenuTest.kt index f2acfad664..627f7bdefe 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/stories/dialogs/StoryContextMenuTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/stories/dialogs/StoryContextMenuTest.kt @@ -127,4 +127,4 @@ class StoryContextMenuTest { val targetIntent: Intent = chooserIntent.getParcelableExtra(Intent.EXTRA_INTENT)!! assertEquals(expected, targetIntent.getStringExtra(Intent.EXTRA_TEXT)) } -} \ No newline at end of file +} From 0a33574f1d8c4937a87a399bead96c0422ebe7e7 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 6 Oct 2022 10:44:32 -0300 Subject: [PATCH 34/78] Do not unarchive threads when story is received. --- .../securesms/database/MmsDatabase.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index 9a9dcd4514..dd6b2b5216 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -1875,7 +1875,18 @@ private Optional insertMessageInbox(IncomingMediaMessage retrieved return Optional.empty(); } - long messageId = insertMediaMessage(threadId, retrieved.getBody(), retrieved.getAttachments(), quoteAttachments, retrieved.getSharedContacts(), retrieved.getLinkPreviews(), retrieved.getMentions(), retrieved.getMessageRanges(), contentValues, null, true); + boolean updateThread = retrieved.getStoryType() == StoryType.NONE; + long messageId = insertMediaMessage(threadId, + retrieved.getBody(), + retrieved.getAttachments(), + quoteAttachments, + retrieved.getSharedContacts(), + retrieved.getLinkPreviews(), + retrieved.getMentions(), + retrieved.getMessageRanges(), + contentValues, + null, + updateThread); boolean isNotStoryGroupReply = retrieved.getParentStoryId() == null || !retrieved.getParentStoryId().isGroupReply(); if (!Types.isExpirationTimerUpdate(mailbox) && !retrieved.getStoryType().isStory() && isNotStoryGroupReply) { From 95801dbdc7b0bc44d01e18921e689a946013cd2f Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 6 Oct 2022 10:49:56 -0300 Subject: [PATCH 35/78] Remove long-press action from my story items. --- .../thoughtcrime/securesms/stories/my/MyStoriesFragment.kt | 7 ------- .../org/thoughtcrime/securesms/stories/my/MyStoriesItem.kt | 2 -- 2 files changed, 9 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesFragment.kt index b5c0a43424..7613294e95 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesFragment.kt @@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.stories.my import android.net.Uri import android.view.View -import android.widget.Toast import androidx.activity.OnBackPressedCallback import androidx.core.app.ActivityOptionsCompat import androidx.core.view.ViewCompat @@ -24,7 +23,6 @@ import org.thoughtcrime.securesms.stories.dialogs.StoryContextMenu import org.thoughtcrime.securesms.stories.dialogs.StoryDialogs import org.thoughtcrime.securesms.stories.viewer.StoryViewerActivity import org.thoughtcrime.securesms.util.LifecycleDisposable -import org.thoughtcrime.securesms.util.Util import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter import org.thoughtcrime.securesms.util.visible @@ -80,11 +78,6 @@ class MyStoriesFragment : DSLSettingsFragment( onClick = { it, preview -> openStoryViewer(it, preview, false) }, - onLongClick = { - Util.copyToClipboard(requireContext(), it.distributionStory.messageRecord.timestamp.toString()) - Toast.makeText(requireContext(), R.string.MyStoriesFragment__copied_sent_timestamp_to_clipboard, Toast.LENGTH_SHORT).show() - true - }, onSaveClick = { StoryContextMenu.save(requireContext(), it.distributionStory.messageRecord) }, diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesItem.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesItem.kt index 0e6d7240fd..fff8be46c1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesItem.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesItem.kt @@ -38,7 +38,6 @@ object MyStoriesItem { class Model( val distributionStory: ConversationMessage, val onClick: (Model, View) -> Unit, - val onLongClick: (Model) -> Boolean, val onSaveClick: (Model) -> Unit, val onDeleteClick: (Model) -> Unit, val onForwardClick: (Model) -> Unit, @@ -106,7 +105,6 @@ object MyStoriesItem { override fun bind(model: Model) { storyPreview.isClickable = false itemView.setOnClickListener { model.onClick(model, storyPreview) } - itemView.setOnLongClickListener { model.onLongClick(model) } downloadTarget.setOnClickListener { model.onSaveClick(model) } moreTarget.setOnClickListener { showContextMenu(model) } presentDateOrStatus(model) From f3fabcbe6af976ecf5d56ea52ff80c235f00b288 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 6 Oct 2022 10:53:03 -0300 Subject: [PATCH 36/78] Fix issue where onboarding text could be cut off. --- app/src/main/res/layout/story_first_time_navigation_view.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/res/layout/story_first_time_navigation_view.xml b/app/src/main/res/layout/story_first_time_navigation_view.xml index 6768fbb65c..93f60dcf3c 100644 --- a/app/src/main/res/layout/story_first_time_navigation_view.xml +++ b/app/src/main/res/layout/story_first_time_navigation_view.xml @@ -52,6 +52,7 @@ android:text="@string/StoryFirstTimeNavigationView__tap_to_advance" android:textAppearance="@style/Signal.Text.BodyLarge" android:textColor="@color/core_white" + app:layout_constrainedWidth="true" app:layout_constraintBottom_toBottomOf="@id/edu_tap_icon" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/edu_tap_icon" @@ -80,6 +81,7 @@ android:text="@string/StoryFirstTimeNavigationView__swipe_up_to_skip" android:textAppearance="@style/Signal.Text.BodyLarge" android:textColor="@color/core_white" + app:layout_constrainedWidth="true" app:layout_constraintBottom_toBottomOf="@id/edu_swipe_up_icon" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/edu_swipe_up_icon" @@ -94,6 +96,7 @@ android:importantForAccessibility="no" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/edu_swipe_right_label" + app:layout_constraintHorizontal_bias="0" app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/edu_swipe_up_icon" @@ -107,6 +110,7 @@ android:text="@string/StoryFirstTimeNavigationView__swipe_right_to_exit" android:textAppearance="@style/Signal.Text.BodyLarge" android:textColor="@color/core_white" + app:layout_constrainedWidth="true" app:layout_constraintBottom_toBottomOf="@id/edu_swipe_right_icon" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/edu_swipe_right_icon" From da9dcc794f4de8b3cdeadb2be247c3f51948bf06 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 6 Oct 2022 10:57:22 -0300 Subject: [PATCH 37/78] Close search if open on back pressed in stories landing page. --- .../stories/landing/StoriesLandingFragment.kt | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt index 6f988c9030..577c144bd7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt @@ -167,7 +167,9 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l viewLifecycleOwner, object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { - tabsViewModel.onChatsSelected() + if (!closeSearchIfOpen()) { + tabsViewModel.onChatsSelected() + } } } ) @@ -350,4 +352,25 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l viewModel.isTransitioningToAnotherScreen = true startActivity(intent, options) } + + private fun isSearchOpen(): Boolean { + return isSearchVisible() + } + + private fun isSearchVisible(): Boolean { + return requreSearchBinder().getSearchToolbar().resolved() && requreSearchBinder().getSearchToolbar().get().getVisibility() == View.VISIBLE + } + + private fun closeSearchIfOpen(): Boolean { + if (isSearchOpen()) { + requreSearchBinder().getSearchToolbar().get().collapse() + requreSearchBinder().onSearchClosed() + return true + } + return false + } + + private fun requreSearchBinder(): SearchBinder { + return requireListener() + } } From 0b978dd9d7e48b658b9ce0c95fa4c86211625c54 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 6 Oct 2022 11:06:13 -0300 Subject: [PATCH 38/78] Update private story creation screens to match material3 spec. --- .../select/BaseStoryRecipientSelectionFragment.kt | 2 +- .../main/res/drawable/rounded_rectangle_variant_22.xml | 5 +++++ .../stories_base_recipient_selection_fragment.xml | 9 +++++---- .../res/layout/stories_edit_story_name_fragment.xml | 10 ++++++---- 4 files changed, 17 insertions(+), 9 deletions(-) create mode 100644 app/src/main/res/drawable/rounded_rectangle_variant_22.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/select/BaseStoryRecipientSelectionFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/select/BaseStoryRecipientSelectionFragment.kt index af56b1d688..eb121c8944 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/select/BaseStoryRecipientSelectionFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/select/BaseStoryRecipientSelectionFragment.kt @@ -147,7 +147,7 @@ abstract class BaseStoryRecipientSelectionFragment : Fragment(R.layout.stories_b canSelectSelf = false, currentSelection = emptyList(), displaySelectionCount = false, - displayChips = false, + displayChips = true, checkboxResource = checkboxResource ) diff --git a/app/src/main/res/drawable/rounded_rectangle_variant_22.xml b/app/src/main/res/drawable/rounded_rectangle_variant_22.xml new file mode 100644 index 0000000000..bfb8b1f886 --- /dev/null +++ b/app/src/main/res/drawable/rounded_rectangle_variant_22.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/stories_base_recipient_selection_fragment.xml b/app/src/main/res/layout/stories_base_recipient_selection_fragment.xml index bad2a17c19..be1b262828 100644 --- a/app/src/main/res/layout/stories_base_recipient_selection_fragment.xml +++ b/app/src/main/res/layout/stories_base_recipient_selection_fragment.xml @@ -1,10 +1,10 @@ + android:layout_height="match_parent" + tools:viewBindingIgnore="true"> + app:navigationIcon="@drawable/ic_arrow_left_24" + app:titleTextAppearance="@style/Signal.Text.TitleLarge" /> + android:layout_height="match_parent" + tools:viewBindingIgnore="true"> + app:title="@string/EditPrivateStoryNameFragment__edit_story_name" + app:titleTextAppearance="@style/Signal.Text.TitleLarge" /> Date: Thu, 6 Oct 2022 11:08:02 -0300 Subject: [PATCH 39/78] Update colors on create button in private story creation flow. --- .../res/layout/stories_create_with_recipients_fragment.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout/stories_create_with_recipients_fragment.xml b/app/src/main/res/layout/stories_create_with_recipients_fragment.xml index c269ca52c7..48d4c57522 100644 --- a/app/src/main/res/layout/stories_create_with_recipients_fragment.xml +++ b/app/src/main/res/layout/stories_create_with_recipients_fragment.xml @@ -68,7 +68,8 @@ android:layout_marginEnd="16dp" android:layout_marginBottom="16dp" android:text="@string/CreateStoryWithViewersFragment__create" - android:textColor="@color/white" + android:textColor="@color/signal_colorOnPrimaryContainer" + app:backgroundTint="@color/signal_colorPrimaryContainer" app:cornerRadius="80dp" app:elevation="4dp" app:layout_constraintBottom_toBottomOf="parent" From 14e8f5cf980172f629e57f267f8fbef862e75852 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 6 Oct 2022 11:03:32 -0400 Subject: [PATCH 40/78] Fix sending group stories when you're the only group member. --- .../securesms/messages/GroupSendUtil.java | 10 +++++++++- .../api/SignalServiceMessageSender.java | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java b/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java index 95c27e41a7..b8b7bca668 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java @@ -484,6 +484,7 @@ private DataSendOperation(@NonNull SignalServiceDataMessage message, @NonNull Co @Nullable CancelationSignal cancelationSignal) throws IOException, UntrustedIdentityException { + // PniSignatures are only needed for 1:1 messages, but some message jobs use the GroupSendUtil methods to send 1:1 if (targets.size() == 1 && relatedMessageId == null) { Recipient targetRecipient = targetRecipients.get(0); SendMessageResult result = messageSender.sendDataMessage(targets.get(0), access.get(0), contentHint, message, SignalServiceMessageSender.IndividualSendEvents.EMPTY, urgent, targetRecipient.needsPniSignature()); @@ -689,7 +690,14 @@ public StorySendOperation(@NonNull MessageId relatedMessageId, @Nullable CancelationSignal cancelationSignal) throws IOException, UntrustedIdentityException { - throw new UnsupportedOperationException("Stories can only be send via sender key!"); + // We only allow legacy sends if you're sending to an empty group and just need to send a sync message. + if (targets.isEmpty()) { + Log.w(TAG, "Only sending a sync message."); + messageSender.sendStorySyncMessage(message, getSentTimestamp(), isRecipientUpdate, manifest); + return Collections.emptyList(); + } else { + throw new UnsupportedOperationException("Stories can only be send via sender key!"); + } } @Override diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index 93ac1606cf..a8a86a4b66 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -268,6 +268,20 @@ public void sendGroupTyping(DistributionId distributionId, sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp(), content, ContentHint.IMPLICIT, message.getGroupId(), true, SenderKeyGroupEvents.EMPTY, false, false); } + /** + * Only sends sync message for a story. Useful if you're sending to a group with no one else in it -- meaning you don't need to send a story, but you do need + * to send it to your linked devices. + */ + public void sendStorySyncMessage(SignalServiceStoryMessage message, + long timestamp, + boolean isRecipientUpdate, + Set manifest) + throws IOException, UntrustedIdentityException + { + SignalServiceSyncMessage syncMessage = createSelfSendSyncMessageForStory(message, timestamp, isRecipientUpdate, manifest); + sendSyncMessage(syncMessage, Optional.empty()); + } + /** * Send a story using sender key. Note: This is not just for group stories -- it's for any story. Just following the naming convention of making sender key * method named "sendGroup*" From 0d94794eceb71cc87707d08fac576744a43bcd08 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 6 Oct 2022 12:53:34 -0300 Subject: [PATCH 41/78] Fix issue where quote view would display base64 encoded text story. --- .../thoughtcrime/securesms/components/QuoteView.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java b/app/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java index c063596779..0f3b5b9563 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java @@ -305,12 +305,11 @@ private void setQuoteText(@Nullable CharSequence body, missingStoryReaction.setVisibility(View.GONE); } - boolean isTextStory = !attachments.containsMediaSlide() && isStoryReply(); - + StoryTextPostModel textPostModel = isStoryReply() ? getStoryTextPost(body) : null; if (!TextUtils.isEmpty(body) || !attachments.containsMediaSlide()) { - if (isTextStory && body != null) { + if (textPostModel != null) { try { - bodyView.setText(getStoryTextPost(body).getText()); + bodyView.setText(textPostModel.getText()); } catch (Exception e) { Log.w(TAG, "Could not parse body of text post.", e); bodyView.setText(""); @@ -365,8 +364,8 @@ private void setQuoteAttachment(@NonNull GlideRequests glideRequests, @NonNull C mainView.setMinimumHeight(isStoryReply() && originalMissing ? 0 : thumbHeight); thumbnailView.setPadding(0, 0, 0, 0); - if (!attachments.containsMediaSlide() && isStoryReply()) { - StoryTextPostModel model = getStoryTextPost(body); + StoryTextPostModel model = isStoryReply() ? getStoryTextPost(body) : null; + if (model != null) { attachmentVideoOverlayView.setVisibility(GONE); attachmentContainerView.setVisibility(GONE); thumbnailView.setVisibility(VISIBLE); From c0e11fbd23ffa062c8253a78ff51a48e8c7c6a07 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 6 Oct 2022 11:58:23 -0400 Subject: [PATCH 42/78] Updated language translations. --- app/src/main/res/values-af/strings.xml | 50 +++++++++- app/src/main/res/values-ar/strings.xml | 54 ++++++++++- app/src/main/res/values-az/strings.xml | 50 +++++++++- app/src/main/res/values-bg/strings.xml | 50 +++++++++- app/src/main/res/values-bn/strings.xml | 50 +++++++++- app/src/main/res/values-bs/strings.xml | 52 +++++++++- app/src/main/res/values-ca/strings.xml | 50 +++++++++- app/src/main/res/values-cs/strings.xml | 52 +++++++++- app/src/main/res/values-da/strings.xml | 50 +++++++++- app/src/main/res/values-de/strings.xml | 50 +++++++++- app/src/main/res/values-el/strings.xml | 50 +++++++++- app/src/main/res/values-es/strings.xml | 50 +++++++++- app/src/main/res/values-et/strings.xml | 50 +++++++++- app/src/main/res/values-eu/strings.xml | 50 +++++++++- app/src/main/res/values-fa/strings.xml | 50 +++++++++- app/src/main/res/values-fi/strings.xml | 50 +++++++++- app/src/main/res/values-fr/strings.xml | 50 +++++++++- app/src/main/res/values-ga/strings.xml | 53 ++++++++++- app/src/main/res/values-gl/strings.xml | 50 +++++++++- app/src/main/res/values-gu/strings.xml | 50 +++++++++- app/src/main/res/values-hi/strings.xml | 50 +++++++++- app/src/main/res/values-hr/strings.xml | 52 +++++++++- app/src/main/res/values-hu/strings.xml | 50 +++++++++- app/src/main/res/values-in/strings.xml | 49 +++++++++- app/src/main/res/values-it/strings.xml | 50 +++++++++- app/src/main/res/values-iw/strings.xml | 52 +++++++++- app/src/main/res/values-ja/strings.xml | 49 +++++++++- app/src/main/res/values-ka/strings.xml | 50 +++++++++- app/src/main/res/values-kk/strings.xml | 50 +++++++++- app/src/main/res/values-km/strings.xml | 49 +++++++++- app/src/main/res/values-kn/strings.xml | 50 +++++++++- app/src/main/res/values-ko/strings.xml | 49 +++++++++- app/src/main/res/values-ky/strings.xml | 49 +++++++++- app/src/main/res/values-lt/strings.xml | 52 +++++++++- app/src/main/res/values-lv/strings.xml | 51 +++++++++- app/src/main/res/values-mk/strings.xml | 50 +++++++++- app/src/main/res/values-ml/strings.xml | 50 +++++++++- app/src/main/res/values-mr/strings.xml | 106 +++++++++++++++------ app/src/main/res/values-ms/strings.xml | 49 +++++++++- app/src/main/res/values-my/strings.xml | 49 +++++++++- app/src/main/res/values-nb/strings.xml | 50 +++++++++- app/src/main/res/values-nl/strings.xml | 50 +++++++++- app/src/main/res/values-pa/strings.xml | 50 +++++++++- app/src/main/res/values-pl/strings.xml | 52 +++++++++- app/src/main/res/values-pt-rBR/strings.xml | 54 ++++++++++- app/src/main/res/values-pt/strings.xml | 50 +++++++++- app/src/main/res/values-ro/strings.xml | 51 +++++++++- app/src/main/res/values-ru/strings.xml | 52 +++++++++- app/src/main/res/values-sk/strings.xml | 52 +++++++++- app/src/main/res/values-sl/strings.xml | 52 +++++++++- app/src/main/res/values-sq/strings.xml | 50 +++++++++- app/src/main/res/values-sr/strings.xml | 50 +++++++++- app/src/main/res/values-sv/strings.xml | 50 +++++++++- app/src/main/res/values-sw/strings.xml | 50 +++++++++- app/src/main/res/values-ta/strings.xml | 50 +++++++++- app/src/main/res/values-te/strings.xml | 50 +++++++++- app/src/main/res/values-th/strings.xml | 49 +++++++++- app/src/main/res/values-tl/strings.xml | 50 +++++++++- app/src/main/res/values-tr/strings.xml | 50 +++++++++- app/src/main/res/values-uk/strings.xml | 52 +++++++++- app/src/main/res/values-ur/strings.xml | 50 +++++++++- app/src/main/res/values-vi/strings.xml | 49 +++++++++- app/src/main/res/values-zh-rCN/strings.xml | 49 +++++++++- app/src/main/res/values-zh-rHK/strings.xml | 49 +++++++++- app/src/main/res/values-zh-rTW/strings.xml | 49 +++++++++- 65 files changed, 3232 insertions(+), 95 deletions(-) diff --git a/app/src/main/res/values-af/strings.xml b/app/src/main/res/values-af/strings.xml index b1bc424752..fe9c85107a 100644 --- a/app/src/main/res/values-af/strings.xml +++ b/app/src/main/res/values-af/strings.xml @@ -1960,6 +1960,7 @@ %1$s aan jou Media nie meer beskikbaar nie. Kan nie \'n toepassing vind wat hierdie media kan deel nie. + Close %1$d nuwe boodskappe in %2$d gesprekke @@ -2748,10 +2749,14 @@ Pasmaak-opsie + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ Nie nou nie + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Voeg fondse by Jou beursie-adres @@ -4911,6 +4936,22 @@ Deel slegs met… Klaar + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Voeg by jou storie by? @@ -4923,6 +4964,8 @@ Storie kon nie gestuur word nie. Kontroleer jou verbinding en probeer weer. Stuur + + Turn off and delete Deel & Kyk na stories @@ -5071,6 +5114,11 @@ Groepstorie · %1$d kyker Groepstorie · %1$d kykers + + + %1$d member + %1$d members + %1$s · %2$d kyker @@ -5206,7 +5254,7 @@ Wil jy stories afskakel? - Jy sal nie meer stories kan deel of kyk nie. Enige stories wat jy onlangs gestuur het, sal steeds vir andere sigbaar wees totdat dit verval. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Storieprivaatheid diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index d962725371..1508620357 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -2256,6 +2256,7 @@ %1$s إليك هذه الوسيط لم يعد متاحا. لم يعثر على تطبيق قادر على فتح هذا الملف. + Close %1$d رسائل جديدة في %2$d محادثات @@ -3092,10 +3093,14 @@ تخصيص الخيارات + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -3248,6 +3253,26 @@ ليس الآن + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + إضافة مبالغ عنوان محفظتك @@ -5347,6 +5372,22 @@ مشاركة فقط مع… تمّ + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. أأضيف للقصة ؟ @@ -5359,6 +5400,8 @@ لقد تعذر إرسال القصة. يُرجى التحقق من اتصالك ثم المحاولة مرة أخرى. أرسلْ + + Turn off and delete مشاركة أو إظهار القصص @@ -5523,6 +5566,15 @@ قصة جماعية · %1$d مشاهدين قصة جماعية · %1$d مشاهدين / مشاهد + + + %1$d members + %1$d member + %1$d members + %1$d members + %1$d members + %1$d members + %1$s · %2$d مشاهدين @@ -5678,7 +5730,7 @@ إيقاف تشغيل القِصص؟ - لن تتمكن بعد الآن من مشاركة القِصص أو عرضها. ستظل أي قِصص أرسلتها مؤخرًا ظاهرة للآخرين حتى تنتهي صلاحيتها. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. إعدادات خصوصية القِصة diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index e2fed93a40..a307084b26 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -1960,6 +1960,7 @@ %1$s sizə göndərildi Media artıq mövcud deyil. Bu medianı paylaşmaq üçün bir tətbiq tapıla bilmir. + Close %2$d danışıqdan %1$d yeni mesaj @@ -2748,10 +2749,14 @@ Seçimi özəlləşdir + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ İndi yox + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Pul əlavə et Pulqabı ünvanınız @@ -4911,6 +4936,22 @@ Yalnız bunlarla paylaşın… Bitdi + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Bir hekayə əlavə edilsin? @@ -4923,6 +4964,8 @@ Hekayə göndərilə bilmədi. Bağlantınızı yoxlayıb yenidən sınayın. Göndər + + Turn off and delete Hekayələrə bax & onları paylaş @@ -5071,6 +5114,11 @@ Qrup hekayəsi · %1$d baxış Qrup hekayəsi · %1$d baxış + + + %1$d member + %1$d members + %1$s · %2$d baxış @@ -5206,7 +5254,7 @@ Hekayələr xüsusiyyəti söndürülsün? - Artıq hekayələrə baxa və ya onları paylaşa bilməyəcəksiniz. İstifadəçilər son göndərdiyiniz bütün hekayələri aktivlik müddəti bitənədək görə bilər. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Hekayənin məxfiliyi diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 2e44ff7bac..7636e8fce5 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -1960,6 +1960,7 @@ От %1$s към вас Тази мултимедия вече не е налична. Не може да бъде намерено приложение, което да може да споделя тази мултимедия. + Close %1$d нови съобщения в %2$d чата @@ -2748,10 +2749,14 @@ Опция за персонализиране + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ Не сега + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Добавяне на средства Адресът на вашия портфейл @@ -4911,6 +4936,22 @@ Споделяне само с… Готово + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Добавяне към историята? @@ -4923,6 +4964,8 @@ Историята не можа да бъде изпратена. Проверете връзката си и опитайте отново. Изпращане + + Turn off and delete Споделяне и преглед на истории @@ -5071,6 +5114,11 @@ Групова история · %1$d зрител Групова история · %1$d зрители + + + %1$d member + %1$d members + %1$s · %2$d зрител @@ -5206,7 +5254,7 @@ Изключване на истории? - Повече няма да можете да споделяте или виждате истории. Историите, които сте изпратили наскоро, все още ще бъдат видими за другите, докато не изтекат. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Поверителност на история diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index eb36a8d48e..4dd7f5a9f8 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -1960,6 +1960,7 @@ %1$s-এর কাছ থেকে আপনার কাছে মিডিয়াটি আর উপলভ্য নেই। এই মিডিয়া শেয়ার করার জন্য কোনও অ্যাপ পাওয়া যায়নি। + Close %1$dটি নতুন বার্তা%2$dটি কথোপকথন @@ -2748,10 +2749,14 @@ বিকল্পগুলি কাস্টমাইজ করুন + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ এখন নয় + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + ফান্ড যোগ করুন আপনার ওয়ালেট ঠিকানা @@ -4911,6 +4936,22 @@ শুধু এর/এদের সাথে শেয়ার করুন… শেষ + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. স্টোরি যোগ করবেন? @@ -4923,6 +4964,8 @@ স্টোরি পাঠানো যায়নি। আপনার সংযোগ ঠিক আছে কিনা দেখুন এবং আবার চেষ্টা করুন। পাঠান + + Turn off and delete &amp শেয়ার করুন; স্টোরি দেখুন @@ -5071,6 +5114,11 @@ গ্ৰুপ স্টোরি · %1$d জন দর্শক গ্ৰুপ স্টোরি · %1$d জন দর্শক + + + %1$d member + %1$d members + %1$s · %2$d দর্শক @@ -5206,7 +5254,7 @@ স্টোরি বন্ধ করবেন? - আপনি আর স্টোরি শেয়ার করতে কিংবা দেখতে পারবেন না। আপনার সম্প্রতি পাঠানো যেকোনো স্টোরি-র সময়কাল শেষ না হওয়া পর্যন্ত অন্যরা দেখতে পাবেন। + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. স্টোরির গোপনীয়তা diff --git a/app/src/main/res/values-bs/strings.xml b/app/src/main/res/values-bs/strings.xml index fbe90533fa..bb2dc828cb 100644 --- a/app/src/main/res/values-bs/strings.xml +++ b/app/src/main/res/values-bs/strings.xml @@ -2108,6 +2108,7 @@ %1$s za Vas Datoteka više nije dostupna. Nije pronađena aplikacija koja može podijeliti ovu datoteku. + Close %1$d novih poruka u %2$d konverzacija @@ -2920,10 +2921,14 @@ Prilagodi + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -3076,6 +3081,26 @@ Ne sada + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Dodaj sredstva Adresa Vašeg novčanika @@ -5129,6 +5154,22 @@ Dijeli samo sa… Uredu + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Dodati u priču? @@ -5141,6 +5182,8 @@ Nije bilo moguće poslati priču. Provjerite svoju konekciju i pokušajte ponovo. Šalji + + Turn off and delete Podijeli i pogledaj priče @@ -5297,6 +5340,13 @@ Grupna priča · %1$d gledaoca Grupna priča · %1$d gledaoca + + + %1$d member + %1$d members + %1$d members + %1$d members + %1$s · %2$d gledalac @@ -5442,7 +5492,7 @@ Isključi priče? - Više nećete moći dijeliti ni pregledati priče. Druge osobe će i dalje moći pregledati priče koje ste nedavno poslali sve dok ne isteknu. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Privatnost priče diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 00630fda90..0ff8d3bd72 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -1960,6 +1960,7 @@ %1$s a vós El contingut ja no està disponible. No es pot trobar cap aplicació que pugui compartir aquest contingut. + Close %1$d missatges nous a %2$d converses @@ -2748,10 +2749,14 @@ Opció de personalització + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ Ara no + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Afegeix fons La vostra adreça de cartera @@ -4911,6 +4936,22 @@ Compartir només amb… Fet + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Ho voleu afegir a la història? @@ -4923,6 +4964,8 @@ No s\'ha pogut enviar la història. Comproveu la connexió i torneu-ho a provar. Envia + + Turn off and delete Compartiu i vegeu històries @@ -5071,6 +5114,11 @@ Història de grup · %1$d espectador Història de grup · %1$d espectadors + + + %1$d member + %1$d members + %1$s · %2$d espectador @@ -5206,7 +5254,7 @@ Desactivar històries? - Ja no podràs compartir o visualitzar històries. Qualsevol història que hagis enviat recentment continuarà sent visible per als altres usuaris fins que expiri. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Privacitat de la història diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 33a9e87e55..6c09639e80 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -2108,6 +2108,7 @@ %1$s pro vás Média již nejsou k dispozici. Nebyla nalezena žádná aplikace pro sdílení tohoto média. + Close %1$d nových zpráv v %2$d konverzacích @@ -2920,10 +2921,14 @@ Nastavit předvolbu + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -3076,6 +3081,26 @@ Nyní ne + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Přidat prostředky Adresa vaší peněženky @@ -5129,6 +5154,22 @@ Sdílet jen s… Hotovo + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Přidat příběh? @@ -5141,6 +5182,8 @@ Příběh se nepodařilo odeslat. Zkontrolujte připojení a zkuste to znovu. Odeslat + + Turn off and delete Sdílení & zobrazení příběhů @@ -5297,6 +5340,13 @@ Skupinový příběh · %1$d sledujících Skupinový příběh · %1$d zobrazení + + + %1$d member + %1$d members + %1$d members + %1$d members + %1$s . %2$d sledující @@ -5442,7 +5492,7 @@ Vypnout příběhy? - Příběhy již nebudete moci sdílet ani prohlížet. Všechny příběhy, které jste nedávno odeslali, budou pro ostatní stále viditelné, dokud nevyprší jejich platnost. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Soukromí příběhů diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 59d6c2267c..f720e47c76 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -1960,6 +1960,7 @@ %1$s til dig Mediefil er ikke længere tilgængelig. Kan ikke finde en app, som kan dele denne mediefil. + Close %1$d nye beskeder i %2$d samtaler @@ -2748,10 +2749,14 @@ Tilpas mulighed + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ Ikke nu + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Tilføj midler Din wallet-adresse @@ -4911,6 +4936,22 @@ Del kun med… Udført + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Føj til historie? @@ -4923,6 +4964,8 @@ Historie kunne ikke sendes. Tjek din forbindelse og prøv igen. Send + + Turn off and delete Del & Se historier @@ -5071,6 +5114,11 @@ Gruppehistorie · %1$d seer Gruppehistorie · %1$d seere + + + %1$d member + %1$d members + %1$s · %2$d seer @@ -5206,7 +5254,7 @@ Slå historier fra? - Du vil ikke længere kunne dele eller se historier. Alle historier, som du har sendt for nyligt, vil stadig være synlige, indtil de udløber. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Privatlivsindstillinger for historier diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index c5fc84c191..833c29c904 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1960,6 +1960,7 @@ %1$s an dich Medieninhalte nicht mehr verfügbar. Keine App zum Teilen dieser Medieninhalte gefunden. + Close %1$d neue Nachrichten in %2$d Unterhaltungen @@ -2748,10 +2749,14 @@ Option anpassen + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ Jetzt nicht + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Guthaben hinzufügen Deine Wallet-Adresse @@ -4911,6 +4936,22 @@ Nur teilen mit … Fertig + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Zu Story hinzufügen? @@ -4923,6 +4964,8 @@ Story konnte nicht versendet werden. Überprüfe deine Internetverbindung und versuche es erneut. Senden + + Turn off and delete Stories teilen & betrachten @@ -5071,6 +5114,11 @@ Gruppen-Story · %1$d Ansichten Gruppen-Story · %1$d Ansichten + + + %1$d member + %1$d members + %1$s · %2$d Betrachter @@ -5206,7 +5254,7 @@ Storys ausschalten? - Du kannst dann keine Storys mehr teilen oder ansehen. Alle Storys, die du kürzlich verschickt hast, sind weiterhin für andere sichtbar, bis sie ablaufen. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Story-Datenschutz diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 5c28d17911..d9fd45a2e7 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -1960,6 +1960,7 @@ %1$s προς εσένα Το πολυμέσο δεν είναι πια διαθέσιμο. Δεν βρέθηκε εφαρμογή που να μπορεί να διαμοιραστεί αυτό το πολυμέσο. + Close %1$d νέα μηνύματα σε %2$d συνομιλίες @@ -2748,10 +2749,14 @@ Προσαρμογή ρύθμισης + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ Όχι τώρα + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Προσθήκη χρημάτων Η διεύθυνση πορτοφολιού σας @@ -4911,6 +4936,22 @@ Κοινοποίηση μόνο σε… Τέλος + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Προσθήκη στην ιστορία; @@ -4923,6 +4964,8 @@ Η ιστορία δεν μπόρεσε να σταλθεί. Έλεγξε τη σύνδεσή σου και προσπάθησε ξανά. Αποστολή + + Turn off and delete Διαμοιρασμός & Προβολή Ιστοριών @@ -5071,6 +5114,11 @@ Ομαδική ιστορία · %1$d προβολή Ομαδική ιστορία · %1$d προβολές + + + %1$d member + %1$d members + %1$s · %2$d θεατής @@ -5206,7 +5254,7 @@ Απενεργοποίηση ιστοριών; - Δεν θα μπορείς πλέον να μοιράζεσαι ή να βλέπεις ιστορίες. Οι ιστορίες που έχεις στείλει πρόσφατα θα παραμείνουν ορατές σε άλλους μέχρι να εξαφανιστούν. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Απόρρητο ιστορίας diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 992ad978b6..0e9a1d1584 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1960,6 +1960,7 @@ De %1$s para ti El adjunto original ya no está disponible. Fallo al encontrar la aplicación para mostrar este adjunto. + Close %1$d mensajes nuevos en %2$d chats @@ -2748,10 +2749,14 @@ Personalizar opción + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ Ahora no + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Añadir fondos Dirección de tu cartera @@ -4911,6 +4936,22 @@ Compartir solo con… Hecho + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. ¿Añadir a la historia? @@ -4923,6 +4964,8 @@ Fallo al enviar la historia. Comprueba tu conexión e inténtalo de nuevo. Enviar + + Turn off and delete Compartir y ver historias @@ -5071,6 +5114,11 @@ Historia de grupo · %1$d vista Historia de grupo · %1$d vistas + + + %1$d member + %1$d members + %1$s · %2$d vista @@ -5206,7 +5254,7 @@ ¿Desactivar historias? - Ya no podrás compartir o visualizar historias. Cualquier historia que hayas enviado recientemente continuará siendo visible por otros usuarios hasta que expire. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Privacidad de la historia diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 47499136a3..23f7808adf 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -1960,6 +1960,7 @@ Kasutajalt %1$s sinule Meedia ei ole enam saadaval. Ei leia rakendust, mis oleks võimeline seda meediafaili jagama. + Close %1$d uut sõnumit %2$d vestluses @@ -2748,10 +2749,14 @@ Kohanda valikut + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ Mitte praegu + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Lisa vahendeid Sinu rahakoti aadress @@ -4911,6 +4936,22 @@ Jaga ainult nendega … Tehtud + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Kas lisada loosse? @@ -4923,6 +4964,8 @@ Lugu ei õnnestunud saata. Kontrolli oma internetiühendust ja proovi uuesti. Saada + + Turn off and delete Jaga & Vaata lugusid @@ -5071,6 +5114,11 @@ Grupi lugu · %1$d vaataja Grupi lugu · %1$d vaataja + + + %1$d member + %1$d members + %1$s · %2$d vaataja @@ -5206,7 +5254,7 @@ Kas lülitada lood välja? - Sa ei saa enam lugusid jagada ega vaadata. Sinu hiljuti jagatud lood on teistele endiselt nähtavad, kuni need aeguvad. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Loo privaatsus diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 0209843fe6..91d4fe5b5c 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -1960,6 +1960,7 @@ %1$s(e)k zuri Medioa jada ez dago eskuragarri. Ez da topatu multimedia eduki hau partekatzeko gai den aplikaziorik. + Close %1$d mezu berri %2$d solasalditan @@ -2748,10 +2749,14 @@ Pertsonalizatu aukera + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ Orain ez + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Gehitu funtsak Zure diru-zorroaren helbidea @@ -4911,6 +4936,22 @@ Partekatu hauekin bakarrik… Eginda + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Gehitu storie-ra? @@ -4923,6 +4964,8 @@ Ezin izan da storie-a bidali. Egiaztatu konexioa eta saiatu berriro. Bidali + + Turn off and delete Partekatu & Ikusi storie-ak @@ -5071,6 +5114,11 @@ Taldeko Istorioa · ikusle %1$d Taldeko Istorioa · %1$d ikusle + + + %1$d member + %1$d members + %1$s · %2$dikusle @@ -5206,7 +5254,7 @@ Istorioak desaktibatu nahi dituzu? - Aurrerantzean, ezingo duzu istoriorik partekatu edo ikusi. Duela gutxi argitaratutako istorioak iraungi arte ikusi ahalko dituzte beste erabiltzaileek. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Istorioen pribatutasuna diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index e3af6ff38d..503762b3c8 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -1960,6 +1960,7 @@ %1$s به شما رسانه دیگر در دسترس نیست. برنامه‌ای برای اشتراک‌گذاری این رسانه پیدا نشد. + Close %1$d پیام جدید در %2$d مکالمه @@ -2748,10 +2749,14 @@ گزینهٔ سفارشی‌سازی + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ حالا نه + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + افزودن اعتبار نشانی کیف پول شما @@ -4911,6 +4936,22 @@ تنها اشتراک‌گذاری شود با… تمام + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. افزودن به استوری؟ @@ -4923,6 +4964,8 @@ استوری فرستاده نمی‌شود. اتصال خود را بررسی نموده و دوباره سعی کنید. ارسال + + Turn off and delete اشتراک‌گذاری و نمایش استوری‌ها @@ -5071,6 +5114,11 @@ استوری گروهی · %1$d بازدید کننده استوری گروهی · %1$d بازدید کننده + + + %1$d member + %1$d members + %1$s · %2$d بازدیدکننده @@ -5206,7 +5254,7 @@ استوری‌ها خاموش شود؟ - دیگر نخواهید توانست استوری‌ها را به اشتراک بگذارید یا مشاهده کنید. استوری‌هایی که اخیراً ارسال کرده‌اید، تا زمانی که منقضی شوند همچنان توسط دیگران قابل مشاهده خواهند بود. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. حریم شخصی استوری diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index bdfa343378..233168507b 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -1960,6 +1960,7 @@ %1$s sinulle Media ei ole enää saatavilla. Mikään sovellus ei tue tämän median jakamista. + Close %1$d uutta viestiä %2$d keskustelussa @@ -2748,10 +2749,14 @@ Mukautettu vaihtoehto + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ Ei nyt + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Lisää varoja Lompakko-osoite @@ -4911,6 +4936,22 @@ Jaa vain seuraaville… Valmis + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Haluatko lisätä tarinaan? @@ -4923,6 +4964,8 @@ Tarinaa ei voitu lähettää. Tarkista yhteytesi ja yritä uudelleen. Lähetä + + Turn off and delete Jaa ja katso tarinoita @@ -5071,6 +5114,11 @@ Ryhmätarina · %1$d katsoja Ryhmätarina · %1$d katsojaa + + + %1$d member + %1$d members + %1$s · %2$d katsoja @@ -5206,7 +5254,7 @@ Poistetaanko tarinat käytöstä? - Et voi enää jakaa tai nähdä tarinoita. Viimeksi julkaisemasi tarinat näkyvät edelleen muille, kunnes ne vanhenevat. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Tarinoiden yksityisyys diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 28ec377d92..893e7ccffc 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1960,6 +1960,7 @@ %1$s à vous Le média n’est plus disponible. Impossible de trouver une appli qui peut partager ce média. + Close %1$d nouveaux messages dans %2$d conversations @@ -2748,10 +2749,14 @@ Personnaliser l’option + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ Pas maintenant + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Ajouter des fonds L’adresse de votre portefeuille @@ -4911,6 +4936,22 @@ Partager uniquement avec… Terminé + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Ajouter à l’histoire ? @@ -4923,6 +4964,8 @@ L\'histoire n’a pas pu être envoyée. Veuillez vérifier votre connexion et réessayer. Envoyer + + Turn off and delete Partager et voir les histoires @@ -5071,6 +5114,11 @@ Story de groupe · %1$d spectateur Story de groupe · %1$d spectateurs + + + %1$d member + %1$d members + %1$s · %2$d spectateur @@ -5206,7 +5254,7 @@ Désactiver les stories ? - Vous ne pourrez plus partager ni consulter de stories. Celles que vous avez récemment partagées restent visibles par vos contacts jusqu\'à leur expiration. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Confidentialité de la story diff --git a/app/src/main/res/values-ga/strings.xml b/app/src/main/res/values-ga/strings.xml index 8b0070d18e..98937f83ca 100644 --- a/app/src/main/res/values-ga/strings.xml +++ b/app/src/main/res/values-ga/strings.xml @@ -2182,6 +2182,7 @@ %1$s to you Níl an meán sin ar fáil a thuilleadh. Can\'t find an app able to share this media. + Close %1$d dteachtaireacht nua i %2$d gcomhrá @@ -3006,10 +3007,14 @@ Customize option + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -3162,6 +3167,26 @@ Ná bac leis anois + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Add Funds Your Wallet Address @@ -5238,6 +5263,22 @@ Only share with… Déanta + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Add to story? @@ -5250,6 +5291,8 @@ Story could not be sent. Check your connection and try again. Seol + + Turn off and delete Share & View Stories @@ -5410,6 +5453,14 @@ Group story · %1$d viewers Group story · %1$d viewers + + + %1$d member + %1$d members + %1$d members + %1$d members + %1$d members + %1$s · %2$d viewer @@ -5560,7 +5611,7 @@ An bhfuil fonn ort scéalta a chasadh as? - Ní bheidh tú in ann scéalta a chomhroinnt ná féachaint orthu a thuilleadh. Beidh aon scéalta a sheol tú le déanaí infheicthe ag daoine eile go dtí go n-imeoidh siad as feidhm. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Story Privacy diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 5bc71c3f77..9815c88d8e 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -1960,6 +1960,7 @@ %1$s a ti Contido multimedia xa non dispoñible. Non se atopa unha aplicación con que compartir este contido multimedia. + Close %1$d novas mensaxes en %2$d conversas @@ -2748,10 +2749,14 @@ Personalizar opción + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ Agora non + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Engadir fondos Enderezo da túa carteira @@ -4911,6 +4936,22 @@ Compartir só con… Feito + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Engadir á historia? @@ -4923,6 +4964,8 @@ Erro ao enviar a historia. Comproba a túa conexión e inténtao de novo. Enviar + + Turn off and delete Compartir & Ver historias @@ -5071,6 +5114,11 @@ Historia grupal · %1$d espectador Historia grupal · %1$d espectadores + + + %1$d member + %1$d members + %1$s · %2$d espectador @@ -5206,7 +5254,7 @@ Desactivar historias? - Non poderás ver nin compartir as historias. Calquera historia que enviases recentemente seguirá a ser visible ata que caduque. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Privacidade da historia diff --git a/app/src/main/res/values-gu/strings.xml b/app/src/main/res/values-gu/strings.xml index aca2e4b98a..b7baff39be 100644 --- a/app/src/main/res/values-gu/strings.xml +++ b/app/src/main/res/values-gu/strings.xml @@ -1960,6 +1960,7 @@ %1$sએ તમને મીડિયા હવે ઉપલબ્ધ નથી. આ મીડિયાને શેર કરવા માટે સક્ષમ એપ્લિકેશન શોધી શકાતી નથી. + Close %1$d%2$d સંવાદ માં નવા મેસેજ @@ -2748,10 +2749,14 @@ કસ્ટમાઇઝ વિકલ્પ + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ અત્યારે નહીં + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + ફંડ ઉમેરો તમારું વૉલેટ સરનામું @@ -4911,6 +4936,22 @@ ફક્ત આમની સાથે શેર કરો… થઈ ગયું + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. સ્ટોરીમાં ઉમેરવું છે? @@ -4923,6 +4964,8 @@ સ્ટોરી મોકલી શકાઈ નહીં. તમારું કનેક્શન તપાસો અને ફરી પ્રયાસ કરો. મોકલો + + Turn off and delete સ્ટોરી શેર કરો અને જુઓ @@ -5071,6 +5114,11 @@ ગ્રૂપ સ્ટોરી · %1$d દર્શક ગ્રુપ સ્ટોરી · %1$d વ્યૂઅર્સ + + + %1$d member + %1$d members + %1$s · %2$d દર્શક @@ -5206,7 +5254,7 @@ સ્ટોરી બંધ કરવી છે? - તમે હવે સ્ટોરી જોઈ અથવા શેર કરી શકશો નહીં. તમે તાજેતરમાં મોકલેલી કોઈ પણ સ્ટોરી હજી પણ તેનો સમય સમાપ્ત ન થાય ત્યાં સુધી અન્યોને દેખાશે. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. સ્ટોરી ગોપનીયતા diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index c650dc2ffc..a13814807c 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -1960,6 +1960,7 @@ %1$s से आपको मीडिया अब उपलब्ध नहीं है। इस मीडिया को शेयर करने के लिए कोई ऐप नहीं मिल रही। + Close %2$d संवाद में %1$d नए मेसेज @@ -2748,10 +2749,14 @@ अनुकूलित करने का विकल्प + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ अभी नहीं + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + धन जोड़ें आपका वॉलेट पता @@ -4911,6 +4936,22 @@ केवल इनके साथ शेयर करें… पूर्ण + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. स्टोरी में जोड़ना है? @@ -4923,6 +4964,8 @@ स्टोरी नहीं भेजी जा सकी। अपना कनेक्शन जाँचें और फिर से प्रयास करें। भेजें + + Turn off and delete शेयर करें और स्टोरीज़ देखें @@ -5071,6 +5114,11 @@ ग्रुप स्टोरी · %1$d व्यूअर ग्रुप स्टोरी · %1$d वयूअर + + + %1$d member + %1$d members + %1$s · %2$d व्यूअर @@ -5206,7 +5254,7 @@ स्टोरीज़ बंद करनी हैं? - आप स्टोरीज़ न तो शेयर कर पाएंगे, न ही उन्हें देख पाएंगे। फिर भी हाल ही में भेजी गई आपकी कोई भी स्टोरी, समाप्त होने तक दूसरों को दिखेंगी। + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. स्टोरी की निजता diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 44313b8aaf..8e21fefa7c 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -2108,6 +2108,7 @@ Poslao korisnik %1$s Medijski zapis više nije dostupan. Nije moguće pronaći aplikaciju za dijeljenje ovog medijskog zapisa. + Close %1$d novih poruka u %2$d razgovora @@ -2920,10 +2921,14 @@ Opcija prilagođavanja + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -3076,6 +3081,26 @@ Ne sada + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Dodaj sredstva Vaša adresa novčanika @@ -5129,6 +5154,22 @@ Podijeli samo s… Gotovo + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Dodati u priču? @@ -5141,6 +5182,8 @@ Nije moguće poslati priču. Provjerite internetsku vezu i pokušajte ponovo. Pošalji + + Turn off and delete Dijelite i gledajte priče @@ -5297,6 +5340,13 @@ Grupna priča · %1$d gledatelja Grupna priča · %1$d gledatelja + + + %1$d member + %1$d members + %1$d members + %1$d members + %1$s · %2$d gledatelj @@ -5442,7 +5492,7 @@ Isključiti priče? - Više nećete moći dijeliti niti pregledavati priče. Sve priče koje ste nedavno poslali bit će vidljive drugima dok ne isteknu. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Privatnost priče diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index f43babfe03..3a1cba8243 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -1960,6 +1960,7 @@ Feladó: %1$s A médiafájl már nem érhető el Nem található alkalmazás ezen médiafájl megnyitásához. + Close %1$d új üzenet %2$d beszélgetésben @@ -2748,10 +2749,14 @@ Beállítás testreszabása + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ Később + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Feltöltés A tárcád címe @@ -4911,6 +4936,22 @@ Megosztás kizárólag a következőkkel… Befejezés + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Hozzáadod a történethez? @@ -4923,6 +4964,8 @@ A történetet nem sikerült elküldeni. Ellenőrizd a hálózati kapcsolatot és próbáld újra! Küldés + + Turn off and delete Történetek megosztása és megtekintése @@ -5071,6 +5114,11 @@ Csoport Történet · %1$d néző Csoport Történet · %1$d néző + + + %1$d member + %1$d members + %1$s · %2$d megtekintő @@ -5206,7 +5254,7 @@ Történetek kikapcsolása? - Többé nem tudsz majd Történeteket megosztani vagy megtekinteni. A közelmúltban elküldött Történeteket mások továbbra is láthatják, amíg le nem járnak. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Történet adatvédelmi beállításai diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 9f22639106..64f72f0d15 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -1886,6 +1886,7 @@ %1$s ke Anda Media tidak tersedia. Tidak dapat menemukan aplikasi untuk berbagi media ini. + Close %1$d pesan baru dalam %2$d percakapan @@ -2662,10 +2663,14 @@ Sesuaikan opsi + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2818,6 +2823,26 @@ Jangan Sekarang + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Tambahkan dana Alamat Dompet Anda @@ -4802,6 +4827,22 @@ Hanya bagikan dengan… Selesai + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Tambahkan ke cerita? @@ -4814,6 +4855,8 @@ Cerita tidak dapat dikirim. Cek koneksi Anda dan coba lagi. Kirim + + Turn off and delete Bagikan & Tampilakan Cerita @@ -4958,6 +5001,10 @@ Cerita grup · %1$d penonton + + + %1$d members + %1$s · %2$d penonton @@ -5088,7 +5135,7 @@ Nonaktifkan cerita? - Anda tidak akan bisa lagi membagikan atau melihat cerita. Cerita apa pun yang baru-baru ini Anda kirim masih bisa terlihat oleh orang lain hingga cerita tersebut kedaluwarsa. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Privasi cerita diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 527d9c3fc9..32cd5ea2a6 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1960,6 +1960,7 @@ %1$s a te Media non più disponibile. Impossibile trovare un\'app per condividere questo media. + Close %1$d nuovi messaggi in %2$d conversazioni @@ -2748,10 +2749,14 @@ Personalizza opzione + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ Non ora + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Aggiungi fondi L\'indirizzo del tuo portafoglio @@ -4911,6 +4936,22 @@ Condividi solo con… Fatto + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Aggiungere alla storia? @@ -4923,6 +4964,8 @@ La storia non può essere inviata. Controlla la tua connessione e riprova. Invia + + Turn off and delete Condividi & Visualizza storie @@ -5071,6 +5114,11 @@ Storia di gruppo · %1$d persona Storia di gruppo · %1$d persone + + + %1$d member + %1$d members + %1$s · %2$d persona @@ -5206,7 +5254,7 @@ Vuoi disattivare le Storie? - Non potrai né condividere le tue Storie, né vedere quelle di altre persone. Qualsiasi Storia tu abbia già pubblicato sarà visibile fino a 24 ore dalla pubblicazione e poi scomparirà. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Privacy delle Storie diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index bd43486987..702e196a18 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -2108,6 +2108,7 @@ %1$s אליך מדיה אינה זמינה יותר. לא ניתן למצוא יישום שמסוגל לשתף מדיה זו. + Close %1$d הודעות חדשות ב־%2$d שיחות @@ -2920,10 +2921,14 @@ התאם אישית אפשרות + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -3076,6 +3081,26 @@ לא עכשיו + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + הוסף כספים כתובת הארנק שלך @@ -5129,6 +5154,22 @@ שיתוף רק עם… סיים + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. להוסיף אל סיפור? @@ -5141,6 +5182,8 @@ סיפור לא היה יכול להישלח. בדוק את החיבור שלך ונסה שוב. שלח + + Turn off and delete שתף וצפה בסיפורים @@ -5297,6 +5340,13 @@ סטורי קבוצתי · %1$d צופים סטורי קבוצתי · %1$d צופים + + + %1$d member + %1$d members + %1$d members + %1$d members + %1$s· צופה %2$d @@ -5442,7 +5492,7 @@ לכבות את סטוריז? - לא תהיה לך אפשרות לשתף או לצפות בסטוריז יותר. סטוריז ששלחת לאחרונה עדיין יהיו זמינים לצפיה לאחרים עד שהתוקף שלהם יפוג. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. פרטיות של סטורי diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 9ce07a0536..9cc20c6db7 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1886,6 +1886,7 @@ %1$s からあなた メディアが存在しません。 このメディアを共有できるアプリが見つかりません。 + Close %2$d個のチャットに%1$d件の新着メッセージ @@ -2662,10 +2663,14 @@ オプションのカスタマイズ + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2818,6 +2823,26 @@ 今はしない + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + 入金 あなたのウォレットアドレス @@ -4802,6 +4827,22 @@ 共有するのは… 完了 + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. ストーリーに追加しますか? @@ -4814,6 +4855,8 @@ ストーリーを送信できませんでした。接続を確認し、再度試してください。 送信する + + Turn off and delete ストーリーの共有と閲覧 @@ -4958,6 +5001,10 @@ グループストーリー · 閲覧 %1$d人 + + + %1$d members + %1$s• %2$d 人の閲覧者 @@ -5088,7 +5135,7 @@ ストーリーを非表示にしますか? - ストーリーの共有や閲覧ができなくなります。最近送信したストーリーは、有効期限が切れるまでは他のユーザーに表示されます。 + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. ストーリープライバシー diff --git a/app/src/main/res/values-ka/strings.xml b/app/src/main/res/values-ka/strings.xml index 3620325e68..9f3069962f 100644 --- a/app/src/main/res/values-ka/strings.xml +++ b/app/src/main/res/values-ka/strings.xml @@ -1960,6 +1960,7 @@ %1$s შენ მედია-ფაილი აღარაა ხელმისაწვდომი. ვერ მოიძებნა აპი, რომელსაც შეუძლია ამ მედია-ფაილის გაზიარება. + Close %1$d ახალი შეტყობინება %2$d მიმოწერაში @@ -2748,10 +2749,14 @@ ვარიანტის მორგება + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ ახლა არა + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + თანხის დამატება შენი საფულის მისამართი @@ -4911,6 +4936,22 @@ გაუზიარე მხოლოდ… შესრულებულია + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. დავამატოთ Story-ში? @@ -4923,6 +4964,8 @@ Story-ი ვერ გაიგზავნა. შეამოწმე შენი ინტერნეტ-კავშირი და ისევ სცადე. გაგზავნა + + Turn off and delete გააზიარე & ნახე Stories-ები @@ -5071,6 +5114,11 @@ ჯგუფის Story-ი · %1$d მნახველი ჯგუფის Story-ი · %1$d მნახველი + + + %1$d member + %1$d members + %1$s · %2$d მნახველი @@ -5206,7 +5254,7 @@ გამოვრთოთ Stories-ები? - Stories-ების გაზიარებას ან ნახვას ვეღარ შეძლებ. შენ მიერ ახლახან გაგზავნილი ნებისმიერი Story-ი კვლავ ხილული იქნება სხვებისთვის, სანამ ვადა არ ამოიწურება. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Story-ის კონფიდენციალურობა diff --git a/app/src/main/res/values-kk/strings.xml b/app/src/main/res/values-kk/strings.xml index b02d285db3..64cd256ece 100644 --- a/app/src/main/res/values-kk/strings.xml +++ b/app/src/main/res/values-kk/strings.xml @@ -1960,6 +1960,7 @@ %1$s – сіз Мультимедиа қолжетімді емес. Бұл мультимедиа файлын бөлісе алатын қолданба табылмады. + Close %2$d әңгімеде %1$d жаңа хат бар @@ -2748,10 +2749,14 @@ Бейімдеу опциясы + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ Қазір емес + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Қаражат қосу Әмияныңыздың мекенжайы @@ -4911,6 +4936,22 @@ Тек мына адамдармен бөлісу… Дайын + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Сторис қосу керек пе? @@ -4923,6 +4964,8 @@ Стористі жіберу мүмкін болмады. Байланысты тексеріп, қайталап көріңіз. Жіберу + + Turn off and delete Стористерді бөлісу және көру @@ -5071,6 +5114,11 @@ Топтық стористе · %1$d көрермен Топтық стористе · %1$d көрермен + + + %1$d member + %1$d members + %1$s · %2$d көрермен @@ -5206,7 +5254,7 @@ Стористерді өшіру керек пе? - Бұдан былай стористерді бөлісе немесе көре алмайсыз. Соңғы жіберген стористеріңіздің мерзімі өткенге дейін, оларды басқа адамдар көре алады. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Сторис құпиялылығы diff --git a/app/src/main/res/values-km/strings.xml b/app/src/main/res/values-km/strings.xml index b0ab1a5e9a..bc6f7e792b 100644 --- a/app/src/main/res/values-km/strings.xml +++ b/app/src/main/res/values-km/strings.xml @@ -1886,6 +1886,7 @@ %1$s ទៅអ្នក ឯកសារមេឌៀលែងមានទៀតហើយ។ មិនអាចស្វែងរកកម្មវិធីដែលអាចចែករំលែកឯកសារមេឌៀនេះ។ + Close %1$d សារថ្មីក្នុង %2$d ការសន្ទនា @@ -2662,10 +2663,14 @@ ជម្រើសធ្វើផ្សេង + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2818,6 +2823,26 @@ កុំទាន់ + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + បន្ថែមថវិកា អាសយដ្ឋានកាបូបរបស់អ្នក @@ -4802,6 +4827,22 @@ គ្រាន់តែចែករំលែកជាមួយ… បញ្ចប់ + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. បញ្ចូលទៅក្នុងរឿងរ៉ាវឬទេ? @@ -4814,6 +4855,8 @@ មិនអាចផ្ញើរឿងរ៉ាវទេ។ សូមពិនិត្យមើលសេវាអ៊ីនធឺណិតរបស់អ្នក ហើយព្យាយាមម្តងទៀត។ ផ្ញើ + + Turn off and delete ចែករំលែក និងមើលរឿងរ៉ាវ @@ -4958,6 +5001,10 @@ រឿងរ៉ាវក្រុម · អ្នកមើល %1$d នាក់ + + + %1$d members + %1$s · អ្នកមើល %2$d នាក់ @@ -5088,7 +5135,7 @@ បិទរឿងរ៉ាវឬ? - អ្នកនឹងមិនអាចចែករំលែក ឬមើលរឿងរ៉ាវទៀតទេ។ រឿងរ៉ាវទាំងឡាយដែលអ្នកបានផ្ញើនាពេលថ្មីៗនេះនឹងនៅតែអាចមើលឃើញដោយអ្នកដទៃ រហូតដល់ពួកវាផុតកំណត់។ + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. ឯកជនភាពនៃរឿងរ៉ាវ diff --git a/app/src/main/res/values-kn/strings.xml b/app/src/main/res/values-kn/strings.xml index 1ddcaeec70..ffcce25b6d 100644 --- a/app/src/main/res/values-kn/strings.xml +++ b/app/src/main/res/values-kn/strings.xml @@ -1960,6 +1960,7 @@ %1$s ಅವರಿಂದ ನಿಮಗೆ ಮೀಡಿಯಾ ಇನ್ನು ಲಭ್ಯವಿಲ್ಲ. ಈ ಮೀಡಿಯಾ ಹಂಚಿಕೊಳ್ಳಲು ಒಂದು ಆಪ್ ಕಂಡುಕೊಳ್ಳಲಾಗುತ್ತಿಲ್ಲ. + Close %2$dಸಂಭಾಷಣೆಗಳಲ್ಲಿ %1$d ಹೊಸ ಸಂದೇಶಗಳು @@ -2748,10 +2749,14 @@ ಆಯ್ಕೆಯನ್ನು ಅಗತ್ಯಾನುಗುಣಗೊಳಿಸಿರಿ + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ ಈಗಲ್ಲ + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + ಫಂಡ್‌ಗಳನ್ನು ಸೇರಿಸಿ ನಿಮ್ಮ ವಾಲೆಟ್ ವಿಳಾಸ @@ -4911,6 +4936,22 @@ ಇವರೊಂದಿಗೆ ಮಾತ್ರ ಹಂಚಿಕೊಳ್ಳಿ… ಮುಗಿದಿದೆ + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. ಸ್ಟೋರಿಗೆ ಸೇರಿಸುವುದೇ? @@ -4923,6 +4964,8 @@ ಸ್ಟೋರಿ ಕಳುಹಿಸಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ. ನಿಮ್ಮ ಸಂಪರ್ಕ ಪರಿಶೀಲಿಸಿ ಹಾಗೂ ಇನ್ನೊಮ್ಮೆ ಪ್ರಯತ್ನಿಸಿ. ಕಳುಹಿಸು + + Turn off and delete ಸ್ಟೋರೀಸ್ ಹಂಚಿಕೊಳ್ಳಿ & ವೀಕ್ಷಿಸಿ @@ -5071,6 +5114,11 @@ Group story · %1$d viewer ಗ್ರೂಪ್ ಸ್ಟೋರಿ · %1$d ವೀಕ್ಷಕರು + + + %1$d member + %1$d members + %1$s · %2$d viewer @@ -5206,7 +5254,7 @@ ಸ್ಟೋರೀಸ್ ಆಫ್ ಮಾಡಬಹುದೇ? - ನಿಮಗಿನ್ನು ಸ್ಟೋರೀಸ್ ಹಂಚಿಕೊಳ್ಳಲು ಅಥವಾ ವೀಕ್ಷಿಸಲು ಸಾಧ್ಯವಾಗುವುದಿಲ್ಲ. ನೀವು ಇತ್ತೀಚೆಗೆ ಕಳುಹಿಸಿದ ಯಾವುದೇ ಸ್ಟೋರೀಗಳ‌ ಅವಧಿ ಮುಗಿಯುವ ತನಕ ಇತತರತು ಅದನ್ನು ವೀಕ್ಷಿಸಬಹುದಾಗಿದೆ. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. ಸ್ಟೋರಿ ಗೌಪ್ಯತೆ diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 2ca45554d9..f56e77f122 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -1886,6 +1886,7 @@ 보낸 사람: %1$s 님, 받는 사람: 나 미디어를 더 이상 이용할 수 없습니다. 미디어를 공유할 수 있는 앱을 찾을 수 없습니다. + Close 대화 %2$d개 내 새 메시지 %1$d개 @@ -2662,10 +2663,14 @@ 옵션 설정하기 + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2818,6 +2823,26 @@ 나중에 + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + 자금 추가 지갑 주소 @@ -4802,6 +4827,22 @@ 다음 사용자와만 공유… 확인 + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. 스토리에 추가할까요? @@ -4814,6 +4855,8 @@ 스토리를 보낼 수 없습니다. 연결을 확인하고 다시 시도하세요. 보내기 + + Turn off and delete 스토리 공유 및 보기 @@ -4958,6 +5001,10 @@ 그룹 스토리 · 볼 수 있는 사람 %1$d명 + + + %1$d members + %1$s · 볼 수 있는 사람 %2$d명 @@ -5088,7 +5135,7 @@ 스토리를 끌까요? - 더 이상 스토리를 공유하거나 볼 수 없게 됩니다. 최근 공유한 스토리는 24시간 후 자동 삭제될 때까지 스토리를 받은 모든 사람에게 표시됩니다. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. 스토리 개인정보보호 diff --git a/app/src/main/res/values-ky/strings.xml b/app/src/main/res/values-ky/strings.xml index 11f4c644dc..1c217bcc77 100644 --- a/app/src/main/res/values-ky/strings.xml +++ b/app/src/main/res/values-ky/strings.xml @@ -1886,6 +1886,7 @@ %1$s сизге Мындай медиафайл жок. Ушул медиафайлды бөлүшө турган колдонмо табылган жок. + Close %2$d сүйлөшүүдө %1$d жаңы билдирүү @@ -2662,10 +2663,14 @@ Ыңгайлаштыруу опциясы + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2818,6 +2823,26 @@ Азыр эмес + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Каражат кошуу Капчыгыңыздын дареги @@ -4802,6 +4827,22 @@ Ушул адамдарга гана көрүнсүн… Бүттү + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Окуяга кошулсунбу? @@ -4814,6 +4855,8 @@ Окуя жөнөтүлбөй койду. Туташууңузду текшерип, кайра аракет кылыңыз. Жөнөтүү + + Turn off and delete Окуяларды бөлүшүү жана көрүү @@ -4958,6 +5001,10 @@ Топтук окуя · %1$d жолу көрүлдү + + + %1$d members + %1$s · %2$d жолу көрүлдү @@ -5088,7 +5135,7 @@ Окуяларды өчүрөсүзбү? - Окуяларды бөлүшө да, көрө да албай каласыз. Буга чейин жөнөткөн соңку окуялар мөөнөтү бүткөнгө чейин башкаларга көрүнө берет. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Окуянын купуялыгы diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index e28641b43d..1e0bea6ca9 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -2108,6 +2108,7 @@ %1$s siuntė jums Medija daugiau nebeprieinama. Nepavyksta rasti programėlės, galinčios bendrinti šią mediją. + Close %1$d naujų(-os) žinutės(-ių) %2$d pokalbyje(-iuose) @@ -2920,10 +2921,14 @@ Tinkinti parinktį + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -3076,6 +3081,26 @@ Ne dabar + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Pridėti lėšų Jūsų piniginės adresas @@ -5129,6 +5154,22 @@ Bendrinti tik su… Atlikta + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Pridėti į istoriją? @@ -5141,6 +5182,8 @@ Nepavyko išsiųsti istorijos. Patikrinkite interneto ryšį ir bandykite dar kartą. Siųsti + + Turn off and delete Bendrinti ir žiūrėti istorijas @@ -5297,6 +5340,13 @@ Grupės istorija · žiūrovų: %1$d Grupės istorija · žiūrovų: %1$d + + + %1$d member + %1$d members + %1$d members + %1$d members + %1$s · Peržiūrėjo: %2$d @@ -5442,7 +5492,7 @@ Išjungti Istorijas? - Nebegalėsi nei bendrinti, nei žiūrėti istorijų. Visos neseniai tavo nusiųstos istorijos tebebus matomos kitiems tol, kol galios. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Istorijų privatumas diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index b08d293d30..a1c037d5a4 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -2034,6 +2034,7 @@ %1$s jums Mediju fails vairs nav pieejams Nevar atrast lietotni, kas varētu kopīgot šo multivides saturu. + Close %1$d jaunas ziņas %2$dsarunās @@ -2834,10 +2835,14 @@ Pielāgot opciju + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2990,6 +2995,26 @@ Ne tagad + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Papildināt līdzekļus Jūsu maciņa adrese @@ -5020,6 +5045,22 @@ Kopīgot tikai ar… Darīts + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Pievienot stāstam? @@ -5032,6 +5073,8 @@ Stāstu nevarēja nosūtīt. Pārbaudiet jūsu pieslēgumu un mēģiniet vēlreiz. Sūtīt + + Turn off and delete Kopīgot & skatīt stāstus @@ -5184,6 +5227,12 @@ Grupas stāsts · %1$d skatītājs Grupas stāsts · %1$d skatītāji + + + %1$d members + %1$d member + %1$d members + %1$s · %2$d skatītāji @@ -5324,7 +5373,7 @@ Vai izslēgt stāstus? - Jūs nevarēsiet kopīgot vai skatīt stāstus. Citi lietotāji varēs redzēt visus jūsu nesen nosūtītos stāstus līdz to termiņa beigām. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Stāsta privātums diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index f2cf9ed48a..a4facc2bd6 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -1960,6 +1960,7 @@ %1$s до Вас Медијата повеќе не е достапна. Не е пронајдена апликација која може да го сподели овој тип на медија. + Close %1$d нови пораки во %2$d разговори @@ -2748,10 +2749,14 @@ Сопствена опција + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ Не сега + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Додај средства Адреса на Вашиот паричник @@ -4911,6 +4936,22 @@ Споделете само со… Готово + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Да се додаде во приказната? @@ -4923,6 +4964,8 @@ Приказната не може да се испрати. Проверете ја Вашата интернет конекција и обидете се повторно. Испрати + + Turn off and delete Сподели и гледај приказни @@ -5071,6 +5114,11 @@ Групна приказна · %1$d гледач Групна приказна · %1$d гледачи + + + %1$d member + %1$d members + %1$s · %2$d гледач @@ -5206,7 +5254,7 @@ Дали сакате да ги исклучите приказните? - Повеќе нема да можете да споделувате или гледате приказни. Приказните кои сте ги испратиле неодамна ќе им бидат видливи на останатите сè додека не истечат. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Приватност на приказна diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 7596bd0dd8..bab7d4c4a9 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -1960,6 +1960,7 @@ %1$s എന്നയാൾ നിങ്ങൾക്ക് അയച്ചത് മീഡിയ ഇനി ലഭ്യമല്ല. ഈ മീഡിയ പങ്കിടാൻ കഴിയുന്ന ഒരു അപ്ലിക്കേഷൻ കണ്ടെത്താനായില്ല. + Close %2$d സംഭാഷണങ്ങളിൽ %1$d പുതിയ സന്ദേശങ്ങൾ @@ -2748,10 +2749,14 @@ ഓപ്ഷൻ ഇച്ഛാനുസൃതമാക്കുക + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ ഇപ്പോൾ വേണ്ട + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + ഫണ്ടുകൾ ചേർക്കുക നിങ്ങളുടെ വാലറ്റ് വിലാസം @@ -4911,6 +4936,22 @@ ഇനിപ്പറയുന്നവരുമായി മാത്രം പങ്കിടുക… ചെയ്‌തു + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. സ്റ്റോറിയിലേക്ക് ചേർക്കണോ? @@ -4923,6 +4964,8 @@ സ്റ്റോറി അയയ്ക്കാനായില്ല. നിങ്ങളുടെ കണക്ഷൻ പരിശോധിച്ച ശേഷം വീണ്ടും ശ്രമിക്കുക. അയയ്‌ക്കുക + + Turn off and delete സ്റ്റോറീസ് പങ്കിടുകയും കാണുകയും ചെയ്യുക @@ -5071,6 +5114,11 @@ ഗ്രൂപ്പ് സ്റ്റോറി · %1$d ആൾ കണ്ടു ഗ്രൂപ്പ് സ്റ്റോറി · %1$d കാഴ്‌ചക്കാർ + + + %1$d member + %1$d members + %1$s · %2$d ആൾ കണ്ടു @@ -5206,7 +5254,7 @@ സ്റ്റോറികൾ ഓഫാക്കണോ? - നിങ്ങൾക്ക് ഇനിമുതൽ സ്റ്റോറികൾ പങ്കിടാനോ കാണാനോ കഴിയില്ല. നിങ്ങൾ അടുത്തിടെ അയച്ച എല്ലാ സ്റ്റോറികളും കാലഹരണപ്പെടുന്നതുവരെ മറ്റുള്ളവർക്ക് തുടർന്നും ദൃശ്യമാകും. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. സ്റ്റോറി സ്വകാര്യത diff --git a/app/src/main/res/values-mr/strings.xml b/app/src/main/res/values-mr/strings.xml index 82468b898d..6b259b9772 100644 --- a/app/src/main/res/values-mr/strings.xml +++ b/app/src/main/res/values-mr/strings.xml @@ -358,8 +358,8 @@ संचयनमध्ये जतन करायचे? - हा मिडिया संचयनामध्ये जतन केल्याने आपल्या डिव्हाईस वरील इतर ॲप्सना त्यात प्रवेश करण्याची अनुमती मिळेल.\n\nसुरू ठेवायचे? - सर्व %1$d मिडिया संचयन मध्ये जतन केल्याने आपल्या डिव्हाईस वरील इतर ॲप्सना त्यात प्रवेश करण्याची अनुमती मिळेल.\n\nसुरू ठेवायचे? + हा मिडिया संग्रहणामध्ये जतन केल्याने आपल्या डिव्हाईस वरील इतर ॲप्सना त्यात प्रवेश करण्याची अनुमती मिळेल.\n\nसुरू ठेवायचे? + सर्व %1$d मिडिया संग्रहणामध्ये जतन केल्याने आपल्या डिव्हाईस वरील इतर ॲप्सना त्यात प्रवेश करण्याची अनुमती मिळेल.\n\nसुरू ठेवायचे? संचयनमध्ये संलग्न जतन करण्यात त्रुटी! @@ -367,12 +367,12 @@ संचयनमध्ये लिहिण्यात अक्षम! - संलग्न जतन करत आहे + संलग्नक जतन करत आहे %1$d संलग्नके जतन करत आहे - संलग्न संचयनमध्ये जतन करत आहे… - %1$d संलग्नके संचयनामध्ये जतन करत आहे… + संलग्नक संग्रहणामध्ये जतन करत आहे… + %1$d संलग्नके संग्रहणामध्ये जतन करत आहे… प्रलंबित… डेटा (Signal) @@ -418,14 +418,14 @@ निवडलेले संभाषणे हटवायचे? - हे निवडलेले संभाषणे कायमचे हटवेल. - हे निवडलेले %1$d संभाषणे कायमचे हटवेल. + हे निवडलेले संभाषण कायमचे हटवेल. + हे निवडलेली %1$d संभाषणे कायमची हटवेल. हटवत आहे निवडलेले संभाषण हटवत आहे… - संभाषण आर्काईव्ह केले - %1$d संभाषणे आर्काईव्ह केली + संभाषण संग्रहित केले + %1$d संभाषणे संग्रहित केली अनडू करा @@ -887,7 +887,7 @@ - %1$s ने व्यक्ती आमंत्रित केली + %1$s ने 1 व्यक्तीला आमंत्रित केले %1$s ने %2$d व्यक्तींना आमंत्रित केले @@ -926,8 +926,8 @@ आपण %1$s ला पाठवलेले आमंत्रण रद्द करू इच्छिता? - %1$s द्वारा पाठवलेले आमंत्रण आपण मागे घेऊ इच्छिता? - %1$s द्वारा पाठवलेली %2$d आमंत्रणे मागे घेऊ इच्छिता? + आपण %1$s द्वारे पाठवलेले आमंत्रण मागे घेऊ इच्छिता? + %1$s द्वारे पाठवलेली %2$d आमंत्रणे मागे घेऊ इच्छिता? @@ -1035,8 +1035,8 @@ निवडलेले आयटम हटवायचे? - हे निवडलेली फाईल कायमची हटवेल. या आयटम सोबत संबंधित कुठलाही संदेश मजकूर देखील हटविला जाईल. - हे निवडलेल्या सर्व %1$d फायली कायमच्या हटवेल. या आयटम सोबत संबंधित कुठलाही संदेश मजकूर देखील हटविला जाईल. + हे निवडलेली फाईल कायमची हटवेल. या घटकाशी संबंधित कुठलाही संदेश मजकूर देखील हटविला जाईल. + हे निवडलेल्या सर्व %1$d फायली कायमच्या हटवेल. या घटकांशी संबंधित कुठलाही संदेश मजकूर देखील हटविला जाईल. हटवत आहे संदेश हटवत आहे… @@ -1168,13 +1168,13 @@ आपण नवीन गटात जोडले जाऊ शकले नाही आणि त्यात सामील होण्यासाठी आपणास आमंत्रित केले गेले आहे. चॅट सत्र ताजेतवाने झाले - सदस्य नवीन गटात जोडला जाऊ शकत नाही आणि त्यात सामील होण्यासाठी आमंत्रित केले गेले आहे. - %1$s सदस्य नवीन गटात जोडले जाऊ शकले नाही आणि त्यात सामील होण्यासाठी आमंत्रित केले गेले आहे. + नवीन गटात सदस्य जोडला जाऊ शकत नाही आणि त्याला सामील होण्यासाठी आमंत्रित केले गेले आहे. + %1$s सदस्य नवीन गटात जोडले जाऊ शकत नाहीत आणि त्यांना सामील होण्यासाठी आमंत्रित केले गेले आहे. - सदस्य नवीन गटात जोडला जाऊ शकत नाही आणि त्यातून तो काढला गेला आहे. - %1$s सदस्य नवीन गटात जोडले जाऊ शकले नाही आणि त्यातून ते काढले गेले आहेत. + सदस्य नवीन गटात जोडला जाऊ शकत नाही आणि त्यातून त्याला काढून टाकले आहे. + %1$s सदस्य नवीन गटात जोडले जाऊ शकत नाहीत आणि त्यातून त्यांना काढून टाकले आहे. @@ -1219,23 +1219,23 @@ आपण %1$s ला गटात आमंत्रित केले. %1$s ने आपल्याला गटात आमंत्रित केले. - %1$s ने 1 व्यक्तींला गटात आमंत्रित केले. - %1$s ने %2$d व्यक्तींना गटात आमंत्रित केले. + %1$s ने 1 व्यक्तीला ग्रुपमध्ये आमंत्रित केले. + %1$s ने %2$d व्यक्तींना ग्रुपमध्ये आमंत्रित केले. आपल्याला गटात निमंत्रित केले गेले होते. - 1 व्यक्ती गटात आमंत्रित केली गेली होती. - %1$d व्यक्तीना गटात आमंत्रित केले गेले होते. + 1 व्यक्तीला ग्रुपमध्ये आमंत्रित केले गेले होते. + %1$d व्यक्तींना गटात आमंत्रित केले गेले होते. - आपण गटाचे आमंत्रण मागे घेतले. - आपण गटाची %1$d आमंत्रणे मागे घेतली. + आपण ग्रुपचे आमंत्रण मागे घेतले. + आपण ग्रुपची %1$d आमंत्रणे मागे घेतली. - %1$s ने गटाचे आमंत्रण मागे घेतले. - %1$s ने गटाची %2$d आमंत्रणे मागे घेतली. + %1$s ने ग्रुपचे आमंत्रण मागे घेतले. + %1$s ने ग्रुपची %2$d आमंत्रणे मागे घेतली. कुणीतरी गटाचे आमंत्रण रद्द केले. आपण गटाचे आमंत्रण रद्द केले. @@ -1316,8 +1316,8 @@ %1$s ने गट लिंक द्वारे सामिल होण्यासाठी विनंती केली. - %1$s ने गट लिंकद्वारे सामील होण्यासाठी विनंती केली आणि त्यांची विनंती रद्द केली. - %1$s ने गट लिंकद्वारे सामील होण्यासाठी विनंती केली आणि त्यांच्या %2$d विनंत्या रद्द केल्या. + %1$s ने विनंती केली आणि ग्रुप लिंकद्वारे सामील होण्यासाठी त्यांची विनंती रद्द केली. + %1$s ने विनंती केली आणि ग्रुप लिंकद्वारे सामील होण्याच्या %2$d विनंत्या रद्द केल्या. @@ -1960,6 +1960,7 @@ %1$s कडून आपणाला मिडिया आता उपलब्ध नाही. हे मिडिया शेअर करण्यासाठी सक्षम असे अॅप सापडू शकले नाही. + Close %2$d संभाषणांमध्ये %1$d नवीन संदेश @@ -2748,10 +2749,14 @@ पर्याय सानुकूलित करा + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ आता नाही + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + फंड जोडा आपला वॉलेट पत्ता @@ -4911,6 +4936,22 @@ फक्त …सोबत शेअर करा ठीक + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. स्टोरीत जोडायचे? @@ -4923,6 +4964,8 @@ स्टोरी पाठवता आली नाही. आपले कनेक्शन तपासा आणि पुन्हा प्रयत्न करा. पाठवा + + Turn off and delete स्टोरीज पहा & शेअर करा @@ -5071,6 +5114,11 @@ ग्रुप स्टोरी · %1$d दर्शक ग्रुप स्टोरी · %1$d दर्शक + + + %1$d member + %1$d members + %1$s · %2$d दर्शक @@ -5206,7 +5254,7 @@ स्टोरीज बंद करा? - आपण यापुढे स्टोरीज पाहू किंवा शेअर करु शकणार नाही. आपण अलिकडेच पाठविलेल्या स्टोरीज कालबाह्य होत नाहीत तोपर्यंत इतरांना अद्याप दृश्यमान असतील. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. स्टोरी गोपनीयता diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index 87d50c6c52..b6cfb64133 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -1886,6 +1886,7 @@ %1$s kepada anda Media tidak lagi tersedia. Tidak menemui aplikasi yang dapat kongsikan media ini. + Close %1$d mesej baru dalam %2$d perbualan @@ -2662,10 +2663,14 @@ Sesuaikan pilihan + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2818,6 +2823,26 @@ Bukan Sekarang + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Tambah dana Alamat Dompet Anda @@ -4802,6 +4827,22 @@ Hanya berkongsi dengan… Selesai + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Tambah ke cerita? @@ -4814,6 +4855,8 @@ Cerita tidak boleh dihantar. Periksa sambungan anda dan cuba lagi. Hantar + + Turn off and delete Kongsi & Lihat Cerita @@ -4958,6 +5001,10 @@ Cerita kumpulan . %1$d tontonan + + + %1$d members + %1$s . %2$d penonton @@ -5088,7 +5135,7 @@ Matikan cerita? - Anda tidak lagi boleh berkongsi atau melihat cerita. Sebarang cerita yang anda hantar baru-baru ini masih boleh dilihat oleh orang lain sehingga ia tamat tempoh. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Privasi cerita diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index b4ae1332b2..34fcb71426 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -1886,6 +1886,7 @@ %1$s ထံမှ မီဒီယာမရှိတော့ပါ ဤမီဒီယာကို ဝေမျှနိုင်သော အပ္ပလီကေးရှင်း မတွေ့ရှိပါ။ + Close စကားဝိုင်း %2$d တွင် စာပေါင်း %1$d ရှိသည်။ @@ -2662,10 +2663,14 @@ ရွေးချယ်မှုကို စိတ်ကြိုက်ပြင်ဆင်မယ် + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2818,6 +2823,26 @@ ယခု မလုပ်ပါ + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + ငွေဖြည့်မယ် သင့် ပိုက်ဆံအိတ် Wallet လိပ်စာ @@ -4802,6 +4827,22 @@ ဖော်ပြထားသူများထံသာ ဝေမျှရန်… ပြီးပါပြီ + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. စတိုရီတွင် ပေါင်းထည့်မည်လား။ @@ -4814,6 +4855,8 @@ စတိုရီကို ပို့၍ မရနိုင်ပါ။ သင့်ချိတ်ဆက်မှုကို စစ်ဆေးပြီး ထပ်ကြိုးစားပါ။ ပေးပို့မယ် + + Turn off and delete စတိုရီများကို မျှဝေကြည့်ရှုရန် @@ -4958,6 +5001,10 @@ အဖွဲ့ စတိုရီ · ကြည့်ရှုသူ · %1$d ဦး + + + %1$d members + %1$s · ကြည့်ရှုသူ %2$d ဦး @@ -5088,7 +5135,7 @@ စတိုရီများကို ပိတ်မည်လား။ - စတိုရီများ ဝေမျှခြင်း သို့မဟုတ် ကြည့်ခြင်းများ လုပ်နိုင်တော့မည် မဟုတ်ပါ။ မကြာသေးမီက ပို့ထားသော စတိုရီများကို သက်တမ်းမကုန်မချင်း အခြားသူများက ဆက်လက်မြင်နိုင်ပါမည်။ + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. စတိုရီ ကိုယ်ပိုင်အချက်အလက် diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 31ffc0a2f4..d40f254e11 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -1960,6 +1960,7 @@ Fra %1$s til deg Media er ikke lenger tilgjengelig. Kan ikke finne app som er i stand til å dele dette mediet. + Close %1$d nye meldinger i %2$d samtaler @@ -2748,10 +2749,14 @@ Tilpass opsjon + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ Ikke nå + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Legg til penger Lommebokadressen din @@ -4911,6 +4936,22 @@ Del kun med … Ferdig + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Legge til i story? @@ -4923,6 +4964,8 @@ Storyen kunne ikke sendes. Sjekk internettilkoblingen din og prøv igjen. Send + + Turn off and delete Del og se storyer @@ -5071,6 +5114,11 @@ Gruppestory · Sett av %1$d Gruppestory · Sett av %1$d + + + %1$d member + %1$d members + %1$s · Sett av %2$d @@ -5206,7 +5254,7 @@ Vil du skru av stories? - Du vil ikke lenger kunne se og dele stories. Storiesene du har delt vil fortsatt være synlige for kontaktene dine til de utløper. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Personvern for stories diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index eea4015f4a..3afdc89614 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -1960,6 +1960,7 @@ %1$s naar jou Media niet langer beschikbaar. Geen app gevonden waarmee dit bestand met anderen gedeeld kan worden. + Close %1$d nieuwe berichten in %2$d gesprekken @@ -2748,10 +2749,14 @@ Instelling personaliseren + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ Niet nu + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Krediet toevoegen Het adres van je portemonnee @@ -4911,6 +4936,22 @@ Deel alleen met… Klaar + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Toevoegen aan verhaal? @@ -4923,6 +4964,8 @@ Je verhaal kan niet worden geüpload. Ga na dat je apparaat met het internet is verbonden en probeer het opnieuw. Uploaden + + Turn off and delete Verhaal weergeven & doorsturen @@ -5071,6 +5114,11 @@ Groepsverhaal · door %1$d persoon gezien Groepsverhaal · door %1$d personen gezien + + + %1$d member + %1$d members + %1$s · %2$d keer bekeken. @@ -5206,7 +5254,7 @@ Verhalen uitzetten? - Je zult niet langer verhalen kunnen delen of bekijken. Verhalen die je recent verzonden hebt zullen nog steeds zichtbaar zijn tot deze verlopen. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Verhaalprivacy diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 549be8183d..f2b27870b9 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -1960,6 +1960,7 @@ %1$s ਨੇ ਤੁਹਾਨੂੰ ਮੀਡੀਆ ਹੁਣ ਉਪਲਬਧ ਨਹੀਂ ਹੈ। ਇਸ ਮੀਡੀਆ ਨੂੰ ਸਾਂਝਾ ਕਰਨ ਦੇ ਯੋਗ ਕੋਈ ਐਪ ਨਹੀਂ ਹੈ। + Close %2$d ਗੱਲਬਾਤਾਂ ਵਿੱਚ %1$d ਨਵੇਂ ਸੁਨੇਹੇ @@ -2748,10 +2749,14 @@ ਕਸਟਮਾਈਜ਼ ਚੋਣ + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ ਹਾਲੇ ਨਹੀਂ + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + ਫੰਡ ਸ਼ਾਮਲ ਕਰੋ ਤੁਹਾਡੇ ਵਾਲਟ ਦਾ ਸਿਰਨਾਵਾਂ @@ -4911,6 +4936,22 @@ ਸਿਰਫ਼ ਇਹਨਾਂ ਨਾਲ ਸਾਂਝਾ ਕਰੋ… ਮੁਕੰਮਲ + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. ਕੀ ਤੁਸੀਂ ਸਟੋਰੀ ਵਿੱਚ ਸ਼ਾਮਲ ਕਰਨਾ ਚਾਹੁੰਦੇ ਹੋ? @@ -4923,6 +4964,8 @@ ਸਟੋਰੀ ਭੇਜ ਨਹੀਂ ਸਕੇ। ਆਪਣੇ ਕਨੈਕਸ਼ਨ ਦੀ ਜਾਂਚ ਕਰੋ ਅਤੇ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ। ਭੇਜੋ + + Turn off and delete ਸਟੋਰੀਆਂ ਨੂੰ ਸਾਂਝਾ ਕਰੋ ਅਤੇ ਦੇਖੋ @@ -5071,6 +5114,11 @@ ਗਰੁੱਪ ਸਟੋਰੀ · %1$d ਦਰਸ਼ਕ ਗਰੁੱਪ ਸਟੋਰੀ · %1$d ਦਰਸ਼ਕ + + + %1$d member + %1$d members + %1$s · %2$d ਦਰਸ਼ਕ @@ -5206,7 +5254,7 @@ ਕੀ ਸਟੋਰੀਆਂ ਨੂੰ ਬੰਦ ਕਰਨਾ ਹੈ? - ਹੁਣ ਤੁਸੀਂ ਸਟੋਰੀਆਂ ਨੂੰ ਨਾ ਹੀ ਸਾਂਝਾ ਕਰ ਸਕੋਗੇ ਅਤੇ ਨਾ ਹੀ ਦੇਖ ਸਕੋਗੇ। ਤੁਹਾਡੇ ਵੱਲੋਂ ਹਾਲ ਹੀ ਵਿੱਚ ਭੇਜੀਆਂ ਗਈਆਂ ਸਟੋਰੀਆਂ ਦੀ ਮਿਆਦ ਪੁੱਗਣ ਤੱਕ ਉਹ ਦੂਜਿਆਂ ਨੂੰ ਦਿਖਾਈ ਦਿੰਦੀਆਂ ਰਹਿਣਗੀਆਂ। + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. ਸਟੋਰੀ ਦੀ ਪਰਦੇਦਾਰੀ diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 1f2d01578d..24485e3bb2 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -2108,6 +2108,7 @@ %1$s do Ciebie Multimedia nie są już dostępne. Nie można znaleźć aplikacji, aby udostępnić te multimedia. + Close %1$d nowych wiadomości w %2$d rozmowach @@ -2920,10 +2921,14 @@ Dostosuj opcję + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -3076,6 +3081,26 @@ Nie teraz + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Dodaj środki Adres Twojego portfela @@ -5129,6 +5154,22 @@ Udostępnij tylko… Gotowe + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Dodać do historii? @@ -5141,6 +5182,8 @@ Nie udało się wysłać historii. Sprawdź połączenie z internetem i spróbuj ponownie. Wyślij + + Turn off and delete Udostępniaj i oglądaj historie @@ -5297,6 +5340,13 @@ Historia grupowa · %1$d odbiorców Relacja grupowa · %1$d odbiorców + + + %1$d member + %1$d members + %1$d members + %1$d members + %1$s · %2$d widz @@ -5442,7 +5492,7 @@ Wyłączyć relacje? - Stracisz możliwość udostępniania i wyświetlania relacji. Relacje przesłane przez Ciebie w ostatnich godzinach pozostaną widoczne dla innych użytkowników do momentu ich wygaśnięcia. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Prywatność relacji diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index baa7fea890..6596da1fe9 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -441,8 +441,8 @@ Não lidas - Fixada - Fixadas + Fixar + Fixar Desafixada @@ -1960,6 +1960,7 @@ %1$s para você A mídia não está mais disponível. Não foi possível encontrar um aplicativo capaz de compartilhar esta mídia. + Close %1$d mensagens novas em %2$d conversas @@ -2748,10 +2749,14 @@ Opção para personalizar + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ Agora não + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Adicionar fundos Endereço da sua carteira @@ -4911,6 +4936,22 @@ Compartilhar apenas com… Pronto + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Adicionar ao story? @@ -4923,6 +4964,8 @@ O Story não pôde ser enviado. Verifique sua conexão e tente novamente. Enviar + + Turn off and delete Compartilhar & ver Stories @@ -5071,6 +5114,11 @@ Story do grupo · %1$d visualizador Story do grupo · %1$d visualizadores + + + %1$d member + %1$d members + %1$s · %2$d visualizador @@ -5206,7 +5254,7 @@ Deseja desativar os stories? - Você não poderá mais compartilhar ou visualizar stories. Todos os stories que você enviou recentemente ainda estarão visíveis para outras pessoas até que expirem. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Privacidade do story diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index d7a655d793..5062999435 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -1960,6 +1960,7 @@ De %1$s para si Multimédia atualmente indisponível. Não foi possível encontrar uma aplicação capaz de partilhar este média. + Close %1$d novas mensagens em %2$d conversas @@ -2748,10 +2749,14 @@ Personalizar opção + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ Agora não + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Adicionar fundos O endereço da sua carteira @@ -4911,6 +4936,22 @@ Partilhar apenas com… Concluir + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Adicionar à história? @@ -4923,6 +4964,8 @@ A história não pode ser enviada. Verifique a sua ligação e tente novamente. Enviar + + Turn off and delete Partilhar e ver histórias @@ -5071,6 +5114,11 @@ História de grupo · %1$d visualizador História de grupo · %1$d visualizadores + + + %1$d member + %1$d members + %1$s · %2$d visualizador @@ -5206,7 +5254,7 @@ Desativar histórias? - Já não poderá partilhar ou ver histórias. Quaisquer histórias que tenha recentemente enviado ainda estarão visíveis até expirarem. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Privacidade da história diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 379e0aeed7..98f50cd561 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -2034,6 +2034,7 @@ %1$s către tine Media nu mai este disponibilă. Nu pot găsi o aplicație pentru a distribui acest tip media. + Close %1$d mesaje noi în %2$d conversații @@ -2834,10 +2835,14 @@ Personalizează opțiunea + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2990,6 +2995,26 @@ Nu Acum + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Adaugă fonduri Adresa Portofelului Tău @@ -5020,6 +5045,22 @@ Distribuie doar către… Gata + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Adaugi la poveste? @@ -5032,6 +5073,8 @@ Povestea nu a putut fi trimisă. Verifică-ți conexiunea și încearcă din nou. Trimite + + Turn off and delete Distribuie & vezi povești @@ -5184,6 +5227,12 @@ Poveste de grup · %1$d vizualizatori Poveste de grup · %1$d de vizualizatori + + + %1$d member + %1$d members + %1$d members + %1$s · %2$d vizualizator @@ -5324,7 +5373,7 @@ Dezactivezi poveștile? - Nu vei mai putea să distribui sau să vezi povești. Orice povești ai trimis recent vor fi încă vizibile pentru alte persoane până ce expiră. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Confidențialitatea poveștilor diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 2600347f4c..56afaa9b71 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -2108,6 +2108,7 @@ %1$s отправил(-а) вам Медиафайл больше не доступен. Не найдено приложение, в котором можно поделиться этим медиафайлом. + Close %1$d новых сообщений в %2$d разговорах @@ -2920,10 +2921,14 @@ Настроить опцию + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -3076,6 +3081,26 @@ Не сейчас + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Пополнить Адрес вашего кошелька @@ -5129,6 +5154,22 @@ Поделиться только с… Готово + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Добавить в историю? @@ -5141,6 +5182,8 @@ Не удалось отправить историю. Проверьте ваше подключение к интернету и попробуйте ещё раз. Отправить + + Turn off and delete Делиться историями и просматривать их @@ -5297,6 +5340,13 @@ История группы · %1$d зрителей История группы · %1$d зрителя + + + %1$d member + %1$d members + %1$d members + %1$d members + %1$s · %2$d зритель @@ -5442,7 +5492,7 @@ Отключить истории? - Вы больше не сможете делиться историями или просматривать их. Все недавно отправленные вами истории будут по-прежнему видны другим пользователям до истечения срока их действия. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Настройки приватности историй diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index cdf13788e7..e094aa86bf 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -2108,6 +2108,7 @@ %1$s vám Médiá už nie sú dostupné. Nepodarilo sa nájsť aplikáciu schopnú zdieľať tento typ súboru. + Close %1$d nové správy v %2$d konverzáciách @@ -2920,10 +2921,14 @@ Prispôsobiť nastavenie + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -3076,6 +3081,26 @@ Teraz nie + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Pridať prostriedky Adresa vašej peňaženky @@ -5129,6 +5154,22 @@ Zdieľať iba s… Hotovo + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Pridať do príbehu? @@ -5141,6 +5182,8 @@ Príbeh sa nepodarilo odoslať. Skontrolujte pripojenie a skúste to znova. Poslať + + Turn off and delete Zdieľajte a zobrazujte príbehy @@ -5297,6 +5340,13 @@ Skupinový príbeh · %1$d sledovateľov Skupinový príbeh · %1$d sledovateľov + + + %1$d member + %1$d members + %1$d members + %1$d members + %1$s · %2$d sledovateľ @@ -5442,7 +5492,7 @@ Vypnúť príbehy? - Už nebudete môcť zdieľať ani prezerať príbehy. Všetky príbehy, ktoré ste nedávno uverejnili, sa budú naďalej zobrazovať ostatným, až kým nevyprší ich platnosť. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Súkromie príbehu diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 73106c4358..0862357903 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -2108,6 +2108,7 @@ %1$s za vas Medijsko sporočilo ni več na voljo. Ne najdem ustrezne aplikacije za delitev tega medija. + Close Novih sporočil: %1$d, pogovorov: %2$d @@ -2920,10 +2921,14 @@ Nastavitve po meri + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -3076,6 +3081,26 @@ Ne zdaj + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Dodaj sredstva Naslov vaše denarnice @@ -5129,6 +5154,22 @@ Deli le s/z … OK + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Želite dodati k zgodbi? @@ -5141,6 +5182,8 @@ Zgodba ni mogla biti poslana. Preverite svojo povezavo in poskusite znova. Pošlji + + Turn off and delete Deljenje & Ogled zgodb @@ -5297,6 +5340,13 @@ Skupinska zgodba · %1$d gledalci_ke Skupinska zgodba · %1$d gledalcev_k + + + %1$d member + %1$d members + %1$d members + %1$d members + %1$s · %2$d gledalec @@ -5442,7 +5492,7 @@ Želite izklopiti Zgodbe? - Zgodb ne boste mogli več deliti ali si jih ogledovati. Vse Zgodbe, ki ste jih nedavno poslali, bodo še vedno vidne drugim, dokler ne potečejo. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Zasebnost Zgodb diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml index d133035b80..f8af5be838 100644 --- a/app/src/main/res/values-sq/strings.xml +++ b/app/src/main/res/values-sq/strings.xml @@ -1960,6 +1960,7 @@ %1$s për ju Media s\\’është më e passhme. S\\’gjendet dot një aplikacion për të ndarë me të tjerët këtë media. + Close %1$d mesazhe të reja në %2$d biseda @@ -2748,10 +2749,14 @@ Përshtatë një mundësi + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ Jo tani + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Shtoni fonde Portofol @@ -4911,6 +4936,22 @@ Shpërndaj vetëm me… U bë + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Të shtohet te histori? @@ -4923,6 +4964,8 @@ S\\’u dërgua dot histori. Kontrolloni lidhjen tuaj në internet dhe riprovoni. Dërgoje + + Turn off and delete Ndajeni me të tjerë & Shihni Histori @@ -5071,6 +5114,11 @@ Histori grupi · %1$d parës Postim i përkohshëm grupi · %1$d shikues + + + %1$d member + %1$d members + %1$s %2$d shikues @@ -5206,7 +5254,7 @@ Do t\'i çaktivizosh postimet e përkohshme? - Nuk mund të ndash apo shikosh më postimet e përkohshme. Çdo postim i përkohshëm që ke dërguar së fundi do të shihet ende nga të tjerët, derisa t\'i mbarojë koha. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Privatësia e postimit të përkohshëm diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 9f3a98029b..8ad906ae28 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -1960,6 +1960,7 @@ %1$s за вас Медијуч више није доступан. Нема апликације која може да подели овај медијум. + Close %1$d нових порука у %2$d преписки @@ -2748,10 +2749,14 @@ Прилагодите опцију + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ Ne sada + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Додај средства Адреса Вашег новчаника @@ -4911,6 +4936,22 @@ Samo podeli sa… Готово + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Додати причу? @@ -4923,6 +4964,8 @@ Није могуће послати причу. Проверите своју везу и покушајте поново. Пошаљи + + Turn off and delete Делити & Преглед приче @@ -5071,6 +5114,11 @@ Group story · %1$d viewer Group story · %1$d viewers + + + %1$d member + %1$d members + %1$s · %2$d viewer @@ -5206,7 +5254,7 @@ Želite li da isključite priče? - Više nećete moći da delite ili vidite priče. Priče koje ste nedavno poslali će i dalje biti vidljive drugima sve dok ne isteknu. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Privatnost priča diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index f1a1846960..97c5a46274 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -1960,6 +1960,7 @@ %1$s till dig Media är inte längre tillgängligt. Det går inte att hitta en app som kan dela detta medium. + Close %1$d nya meddelanden i %2$d konversationer @@ -2748,10 +2749,14 @@ Anpassa alternativet + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ Inte nu + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Lägg till pengar Din plånboksadress @@ -4911,6 +4936,22 @@ Dela endast med … Klar + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Lägg till i berättelse? @@ -4923,6 +4964,8 @@ Berättelse kunde inte skickas. Kontrollera din anslutning och försök igen. Skicka + + Turn off and delete Dela & visa berättelser @@ -5071,6 +5114,11 @@ Gruppberättelse · %1$d tittare Gruppberättelse · %1$d tittare + + + %1$d member + %1$d members + %1$s · %2$d tittare @@ -5206,7 +5254,7 @@ Stänga av berättelser? - Du kommer inte längre att kunna dela eller visa berättelser. Alla berättelser som du nyligen har skickat kommer att fortsätta kunna visas tills tiden går ut för den. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Berättelseintegritet diff --git a/app/src/main/res/values-sw/strings.xml b/app/src/main/res/values-sw/strings.xml index d139e5796f..d3ccfc5eea 100644 --- a/app/src/main/res/values-sw/strings.xml +++ b/app/src/main/res/values-sw/strings.xml @@ -1960,6 +1960,7 @@ %1$s kwako Media haipatikani tena. Hatujapata programu inayoweza kushiriki media hii. + Close %1$d ujumbe mpya kwa %2$d mazungumzo @@ -2748,10 +2749,14 @@ Badilisha chaguo likufae + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ Sio kwa Sasa + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Ongeza fedha Anwani ya Mkoba Wako @@ -4911,6 +4936,22 @@ Shirikisha pekee na… Imekamilika + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Ongeza kwenye stori? @@ -4923,6 +4964,8 @@ Stori haingeweza kutumwa. Tazama muunganisho wa mtandao wako kisha ujaribu tena. Tuma + + Turn off and delete Shirikisha $amp; Tazama Stori @@ -5071,6 +5114,11 @@ Stori ya kikundi · mtazamaji %1$d Stori ya kikundi · watazamaji %1$d + + + %1$d member + %1$d members + Mtazamaji %1$s . %2$d @@ -5206,7 +5254,7 @@ Zima stori? - Hutaweza tena kushiriki au kuangalia stori. Stori zozote ulizotuma hivi karibuni bado zitaonekana na wengine mpaka muda wake utakapoisha. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Faragha ya stori diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index f4b014aa3b..3bac6ede24 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -1960,6 +1960,7 @@ %1$s இடமிருந்து உங்களுக்கு இந்த ஊடகம் சேமிப்பகத்தில் இல்லை. இந்த மீடியாவைப் பகிரக்கூடிய பயன்பாட்டைக் கண்டுபிடிக்க முடியவில்லை. + Close %2$d உரையாடல்களில் %1$d புதிய செய்திகள் @@ -2748,10 +2749,14 @@ விருப்ப அமைப்பைத் தனிப்பயனாக்கவும் + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ இப்போது வேண்டாம் + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + கூட்டு நிதி உங்கள் Wallet முகவரி @@ -4911,6 +4936,22 @@ இவர்களுடன் மட்டும் பகிரவும்… முடிந்தது + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. ஸ்டோரியில் சேர்ப்பதா? @@ -4923,6 +4964,8 @@ ஸ்டோரியை அனுப்ப முடியவில்லை. உங்கள் இணைப்பைச் சரிபார்த்து மீண்டும் முயற்சிக்கவும். அனுப்புக + + Turn off and delete பகிரவும் &ஆம்ப்; ஸ்டோரீக்களை காணவும் @@ -5071,6 +5114,11 @@ குழு ஸ்டோரி · %1$d பார்வையாளர் குழு ஸ்டோரி · %1$d பார்வையாளர்கள் + + + %1$d member + %1$d members + %1$s · %2$d பார்வையாளர் @@ -5206,7 +5254,7 @@ ஸ்டோரீக்களை முடக்குவதா? - இனி உங்களால் ஸ்டோரீக்களைப் பகிரவோ பார்க்கவோ முடியாது. நீங்கள் சமீபத்தில் அனுப்பிய ஸ்டோரீக்கள் அனைத்தும் காலாவதியாகும் வரை பிறரால் பார்க்கப்படும். + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. ஸ்டோரி தனியுரிமை diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index 70b9ab0a2e..fc9881305a 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -1960,6 +1960,7 @@ %1$s నుంచి మీకు మీడియా ఇకపై అందుబాటులో లేదు. ఈ మీడియాను భాగస్వామ్యం చేయగల అనువర్తనాన్ని కనుగొనలేకపోయాము. + Close %2$d సంభాషణలొ కొత్త %1$d సందేశాలు @@ -2748,10 +2749,14 @@ అనుకూలీకరించు ఎంపిక + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ ఇప్పుడు కాదు + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + ఫండ్స్ జోడించండి మీ వాలెట్ చిరునామా @@ -4911,6 +4936,22 @@ కేవలం వీరితో పంచుకోండి… పూర్తయింది + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. స్టోరీని జోడించాలా? @@ -4923,6 +4964,8 @@ స్టోరీని పంపలేరు. మీ కనెక్షన్ చెక్ చేసి, మళ్లీ ప్రయత్నించండి. పంపు + + Turn off and delete పంచుకోండి & స్టోరీలను వీక్షించండి @@ -5071,6 +5114,11 @@ గ్రూపు స్టోరీ· %1$d వీక్షకుడు గ్రూప్ కథ · %1$d వీక్షకులు + + + %1$d member + %1$d members + %1$s · %2$d వీక్షకుడు @@ -5206,7 +5254,7 @@ కథలను ఆఫ్ చేసేదా? - మీరు కథలను ఇక ఏమాత్రం వీక్షించలేరు లేదా పంచుకోలేరు. మీరు ఇటీవల పంపిన కథలు ఏవైనా, అవి గడువు ముగిసే వరకు ఇతరులకు ఇంకనూ కనిపిస్తాయి. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. కథ గోప్యత diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index bb0ff00145..ed6a96566c 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -1886,6 +1886,7 @@ %1$s ถึงคุณ สื่อตัวนี้ไม่มีอยู่แล้ว ไม่พบแอปที่แบ่งปันสื่อนี้ได้ + Close มี %1$d ข้อความใหม่ใน %2$d การสนทนา @@ -2662,10 +2663,14 @@ กำหนดการตั้งค่า + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2818,6 +2823,26 @@ ไม่ใช่ตอนนี้ + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + เพิ่มเงิน ที่อยู่กระเป๋าเงินของคุณ @@ -4802,6 +4827,22 @@ แชร์ให้เฉพาะ… เสร็จสิ้น + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. เพิ่มไปยังสตอรี่หรือไม่ @@ -4814,6 +4855,8 @@ ไม่สามารถส่งสตอรี่ได้ ตรวจสอบการเชื่อมต่อแล้วลองอีกครั้ง ส่ง + + Turn off and delete แชร์ & ดูสตอรี่ @@ -4958,6 +5001,10 @@ สตอรี่กลุ่ม · ผู้ชม %1$d คน + + + %1$d members + %1$s · ผู้ชม %2$d คน @@ -5088,7 +5135,7 @@ ปิดสตอรี่ใช่ไหม - คุณจะไม่สามารถแชร์หรือดูสตอรี่ได้อีกต่อไป สตอรี่ที่คุณเพิ่งส่งจะยังปรากฏให้เห็นตามปกติจนกว่าสตอรี่นั้นจะหมดอายุ + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. ความเป็นส่วนตัวของสตอรี่ diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml index ff37f24ac8..3fbfb45633 100644 --- a/app/src/main/res/values-tl/strings.xml +++ b/app/src/main/res/values-tl/strings.xml @@ -1960,6 +1960,7 @@ %1$s to you Hindi na available ang media. Walang makitang app na pwedeng mag-share ng media na ito. + Close %1$d bagong mensahe sa %2$d pag-uusap @@ -2748,10 +2749,14 @@ I-customize ang option + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ Hindi na Muna + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Mag-add ng funds Your Wallet Address @@ -4911,6 +4936,22 @@ Only share with… Tapos na + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. I-add ito sa story? @@ -4923,6 +4964,8 @@ Hindi ma-send ang story. I-check ang iyong internet connection at subukan ulit. Ipadala + + Turn off and delete Pag-share at Pag-view ng Stories @@ -5071,6 +5114,11 @@ Group story · %1$d viewer Group story · %1$d viewers + + + %1$d member + %1$d members + %1$s · %2$d viewer @@ -5206,7 +5254,7 @@ Gusto mo bang i-off ang stories? - Hindi ka na makakapag-share o makakakita ng stories. Makikita pa rin ng iba ang anumang stories na na-send mo kamakailan hanggang sa mag-expire ang mga ito. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Pagkapribado ng story diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 0aa0014fdd..224390853f 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -1960,6 +1960,7 @@ %1$s\'den sana İçerik artık mevcut değil. Bu içeriği paylaşabilen bir uygulama bulunamadı. + Close %2$d konuşmadan %1$d yeni ileti @@ -2748,10 +2749,14 @@ Seçeneği düzenle + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ Şimdi Değil + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Para ekle Cüzdan Adresiniz @@ -4911,6 +4936,22 @@ Sadece ….. ile paylaş Tamam + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Hikayeye ekle? @@ -4923,6 +4964,8 @@ Hikaye gönderilemedi. Bağlantınızı kontrol edip tekrar deneyiniz. Gönder + + Turn off and delete Paylaş & Hikayeleri Görüntüle @@ -5071,6 +5114,11 @@ Grup hikayesi · %1$d görüntüleyen Grup hikayesi · %1$d görüntüleyen + + + %1$d member + %1$d members + %1$s · %2$d görüntüleyen @@ -5206,7 +5254,7 @@ Hikayeler kapatılsın mı? - Artık hikayeleri paylaşamayacak veya görüntüleyemeyeceksin. Yakın zamanda paylaştığın tüm hikayeler, süresi dolana kadar başkaları tarafından görüntülenmeye devam edecek. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Hikaye gizliliği diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index c8cbfad496..b8000dd1c6 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -2108,6 +2108,7 @@ %1$s надіслав вам Мультимедійний більше не доступний. Неможливо знайти програму для того щоб поділитись цим медіафайлом. + Close %1$d нових повідомлень у %2$d розмовах @@ -2920,10 +2921,14 @@ Налаштувати опцію + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -3076,6 +3081,26 @@ Не зараз + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Додати кошти Адреса вашого гаманця @@ -5129,6 +5154,22 @@ Ділитися тільки з… Готово + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Додати до історії? @@ -5141,6 +5182,8 @@ Не вдалося надіслати сторі. Перевірте підключення до мережі і спробуйте знову. Відправити + + Turn off and delete Поділитись & Переглянути історії @@ -5297,6 +5340,13 @@ Групова сторі · %1$d глядачів Групова сторі · %1$d глядача + + + %1$d member + %1$d members + %1$d members + %1$d members + %1$s · %2$d глядач @@ -5442,7 +5492,7 @@ Вимкнути сторіз? - Ви більше не зможете ділитися сторіз чи переглядати їх. Якщо ви нещодавно публікували сторіз, вони будуть видимі, доки не закінчиться їхній термін дії. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Конфіденційність сторіз diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index 61f6092da0..c239c76729 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -1960,6 +1960,7 @@ %1$s کی طرف سے آپ کے لیے میڈیا اب دستیاب نہیں ہے۔ کوئی ایپ اس میڈیا کو شیئر کرنے کے قابل نہیں پاسکتی ہے۔ + Close %1$dنئے پیغامات %2$dگفتگو میں @@ -2748,10 +2749,14 @@ آپشن کو کسٹمائز کریں + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2904,6 +2909,26 @@ ابھی نہیں + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + فنڈز شامل کریں آپ کا Wallet کا پتہ @@ -4911,6 +4936,22 @@ صرف ان کے ساتھ شیئر کریں… ہو گیا + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. اسٹوری میں شامل کریں؟ @@ -4923,6 +4964,8 @@ اسٹوری بھیجی نہیں جا سکی۔ اپنا کنکشن چیک کریں اور دوبارہ کوشش کریں۔ بھیجیں + + Turn off and delete اسٹوریز شیئر کریں & دیکھیں @@ -5071,6 +5114,11 @@ گروپ اسٹوری · %1$d ویور گروپ سٹوری · %1$d ویورز + + + %1$d member + %1$d members + %1$s %2$d ناظر @@ -5206,7 +5254,7 @@ سٹوریز آف کریں؟ - آپ سٹوریز مزید شیئر کرنے یا دیکھنے سے قاصر ہوں گے۔ آپ کی جانب سے حال ہی میں بھیجی گی سٹوریز اپنی میعاد پوری ہونے تک دوسروں کو نظر آئیں گی۔ + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. سٹوری کی پرائیویسی diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 86bf84c74c..4c0a374e4d 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -1886,6 +1886,7 @@ %1$s gửi đến bạn Tệp đa phương tiện không còn khả dụng. Không thể tìm được ứng dụng có thể chia sẻ tệp đa phương tiện này. + Close %1$d tin nhắn mới trong %2$d cuộc trò chuyện @@ -2662,10 +2663,14 @@ Sửa cài đặt + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2818,6 +2823,26 @@ Để sau + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Thêm quỹ Địa chỉ Ví của bạn @@ -4802,6 +4827,22 @@ Chỉ chia sẻ với… Xong + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. Thêm vào story? @@ -4814,6 +4855,8 @@ Không thể gửi story. Hãy kiểm tra kết nối và thử lại. Gửi + + Turn off and delete Chia sẻ & Xem Story @@ -4958,6 +5001,10 @@ Story nhóm · %1$d người xem + + + %1$d members + %1$s · %2$d người xem @@ -5088,7 +5135,7 @@ Tắt story? - Bạn sẽ không thể tiếp tục chia sẻ hoặc xem story. Bất kỳ story nào bạn đã gửi gần đây vẫn có thể được xem bởi các người dùng khác cho đến khi story hết hạn. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Sự Riêng tư của Story diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index fbef910e4d..e770a19875 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1886,6 +1886,7 @@ %1$s发送给您 媒体已失效。 未找到可分享此媒体的应用。 + Close %2$d 个对话中有 %1$d 条新消息 @@ -2662,10 +2663,14 @@ 自定义选项 + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2818,6 +2823,26 @@ 以后再说 + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + 添加资金 您的钱包地址 @@ -4802,6 +4827,22 @@ 只分享给…… 完成 + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. 要添加到动态吗? @@ -4814,6 +4855,8 @@ 动态无法发送。请检查您的网络连接并重试。 发送 + + Turn off and delete 分享和浏览动态 @@ -4958,6 +5001,10 @@ 群组动态 · %1$d 位访客 + + + %1$d members + %1$s · %2$d 位访客 @@ -5088,7 +5135,7 @@ 要关闭动态吗? - 您将无法再分享或浏览动态。您最近发布的任何动态在到期之前仍然对其他人可见。 + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. 动态隐私 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index a286c91ba9..ce48ff9b41 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -1886,6 +1886,7 @@ %1$s 給您 媒體不再可用。 找不到能夠分享此媒體的應用程式。 + Close 來自 %2$d 個對話的 %1$d 則新訊息 @@ -2662,10 +2663,14 @@ 自訂選項 + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2818,6 +2823,26 @@ 暫時不要 + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + 增加款項 您的錢包位址 @@ -4802,6 +4827,22 @@ 只分享給… 完成 + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. 要新增至限時動態嗎? @@ -4814,6 +4855,8 @@ 限時動態無法傳送。請檢查您的連線,然後再試一次。 傳送 + + Turn off and delete 分享與觀看限時動態 @@ -4958,6 +5001,10 @@ 群組限時動態 · %1$d 位瀏覽者 + + + %1$d members + %1$s · %2$d 位瀏覽者 @@ -5088,7 +5135,7 @@ 要關閉限時動態嗎? - 你將無法再分享或瀏覽限時動態。其他人在限時內仍可看到你最近發佈的限時動態。 + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. 限時動態私隱 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index dccf02eb15..98c9e3bf21 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1886,6 +1886,7 @@ %1$s 給你 媒體已不存在。 找不到能夠分享此媒體檔案的應用程式。 + Close %1$d 則新訊息在 %2$d 的對話中 @@ -2662,10 +2663,14 @@ 自定義選項 + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher Retry send + Remove group story Clear onboarding state Clears onboarding flag and triggers download of onboarding stories. Internal Preferences @@ -2818,6 +2823,26 @@ 暫時不要 + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + 增加資金 你的錢包位址 @@ -4802,6 +4827,22 @@ 僅分享給… 完成 + + Remove group story? + + \"%1$s\" will be removed. + + Remove + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete + + Stories is available to Signal beta users only. + + If you share a story, it will only be available to people who are on Signal beta. 要新增至限時動態嗎? @@ -4814,6 +4855,8 @@ 限時動態無法傳送。 檢查你的連接,然後重試。 傳送 + + Turn off and delete 分享與觀看限時動態 @@ -4958,6 +5001,10 @@ 群組限動 · %1$d 位瀏覽者 + + + %1$d members + %1$s · %2$d 位瀏覽者 @@ -5088,7 +5135,7 @@ 要關閉限動嗎? - 你將無法再分享或瀏覽限動。任何你近期傳送給他人的限動,在到期之前仍可瀏覽。 + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. 限動隱私 From bb323dc5755e13fd2d466f35f22e70d1db2d3b16 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 6 Oct 2022 11:58:56 -0400 Subject: [PATCH 43/78] Bump version to 5.52.2 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index fe87bf35da..ffca1b3551 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -57,8 +57,8 @@ ktlint { version = "0.43.2" } -def canonicalVersionCode = 1138 -def canonicalVersionName = "5.52.1" +def canonicalVersionCode = 1139 +def canonicalVersionName = "5.52.2" def postFixSize = 100 def abiPostFix = ['universal' : 0, From f6878408915015ecbf81dc45f9a4c138fb158d1c Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 6 Oct 2022 13:10:41 -0300 Subject: [PATCH 44/78] Do not display story media in settings media rail. --- .../java/org/thoughtcrime/securesms/database/MediaDatabase.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java index ea5313c3b6..21222545f1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java @@ -63,6 +63,7 @@ public class MediaDatabase extends Database { + " FROM " + MmsDatabase.TABLE_NAME + " WHERE " + MmsDatabase.THREAD_ID + " __EQUALITY__ ?) AND (%s) AND " + MmsDatabase.VIEW_ONCE + " = 0 AND " + + MmsDatabase.STORY_TYPE + " = 0 AND " + AttachmentDatabase.DATA + " IS NOT NULL AND " + "(" + AttachmentDatabase.QUOTE + " = 0 OR (" + AttachmentDatabase.QUOTE + " = 1 AND " + AttachmentDatabase.DATA_HASH + " IS NULL)) AND " + AttachmentDatabase.STICKER_PACK_ID + " IS NULL AND " From 9ad55e2360511433f79455bd1c5c50ed653dceb2 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 6 Oct 2022 13:45:17 -0300 Subject: [PATCH 45/78] Fix issue where images were not properly rendered for previews. --- .../v2/review/MediaReviewFragment.kt | 29 +++++++++++++++++-- .../scribbles/ImageEditorFragment.java | 12 +++++--- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt index b55934e848..b7ec225a2b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt @@ -24,6 +24,7 @@ import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 import app.cash.exhaustive.Exhaustive import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import org.signal.core.util.concurrent.SimpleTask import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey import org.thoughtcrime.securesms.conversation.MessageSendType @@ -41,6 +42,7 @@ import org.thoughtcrime.securesms.mediasend.v2.stories.StoriesMultiselectForward import org.thoughtcrime.securesms.mms.SentMediaQuality import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.scribbles.ImageEditorFragment import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.MediaUtil import org.thoughtcrime.securesms.util.SystemWindowInsetsSetter @@ -167,8 +169,31 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment) { ) if (sharedViewModel.isStory()) { - val previews = sharedViewModel.state.value?.selectedMedia?.take(2)?.map { it.uri } ?: emptyList() - storiesLauncher.launch(StoriesMultiselectForwardActivity.Args(args, previews)) + val snapshot = sharedViewModel.state.value + + if (snapshot != null) { + sendButton.isEnabled = false + SimpleTask.run(viewLifecycleOwner.lifecycle, { + snapshot.selectedMedia.take(2).map { media -> + val editorData = snapshot.editorStateMap[media.uri] + if (MediaUtil.isImageType(media.mimeType) && editorData != null && editorData is ImageEditorFragment.Data) { + val model = editorData.readModel() + if (model != null) { + ImageEditorFragment.renderToSingleUseBlob(requireContext(), model) + } else { + media.uri + } + } else { + media.uri + } + } + }, { + sendButton.isEnabled = true + storiesLauncher.launch(StoriesMultiselectForwardActivity.Args(args, it)) + }) + } else { + storiesLauncher.launch(StoriesMultiselectForwardActivity.Args(args, emptyList())) + } } else { multiselectLauncher.launch(args) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java index 10e59e9e8c..17e1974674 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.scribbles; import android.Manifest; +import android.content.Context; import android.content.Intent; import android.content.res.Configuration; import android.graphics.Bitmap; @@ -33,10 +34,10 @@ import org.signal.core.util.FontUtil; import org.signal.core.util.concurrent.SignalExecutors; +import org.signal.core.util.concurrent.SimpleTask; import org.signal.core.util.logging.Log; import org.signal.imageeditor.core.Bounds; import org.signal.imageeditor.core.ColorableRenderer; -import org.signal.imageeditor.core.HiddenEditText; import org.signal.imageeditor.core.ImageEditorView; import org.signal.imageeditor.core.Renderer; import org.signal.imageeditor.core.SelectableRenderer; @@ -48,7 +49,6 @@ import org.signal.libsignal.protocol.util.Pair; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.animation.ResizeAnimation; -import org.thoughtcrime.securesms.components.emoji.EmojiUtil; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.fonts.FontTypefaceProvider; import org.thoughtcrime.securesms.keyvalue.SignalStore; @@ -66,7 +66,6 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.ThrottledDebouncer; import org.thoughtcrime.securesms.util.ViewUtil; -import org.signal.core.util.concurrent.SimpleTask; import org.thoughtcrime.securesms.util.views.SimpleProgressDialog; import java.io.ByteArrayOutputStream; @@ -791,8 +790,13 @@ private void performSaveToDisk() { @WorkerThread public @NonNull Uri renderToSingleUseBlob() { + return renderToSingleUseBlob(requireContext(), imageEditorView.getModel()); + } + + @WorkerThread + public static @NonNull Uri renderToSingleUseBlob(@NonNull Context context, @NonNull EditorModel editorModel) { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - Bitmap image = imageEditorView.getModel().render(requireContext(), new FontTypefaceProvider()); + Bitmap image = editorModel.render(context, new FontTypefaceProvider()); image.compress(Bitmap.CompressFormat.JPEG, 80, outputStream); image.recycle(); From aef0ed828cbfb015b8f65f36bfa73119fe4af467 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 6 Oct 2022 14:21:03 -0300 Subject: [PATCH 46/78] Add proper colorization to send button in stories flow. --- .../securesms/color/ViewColorSet.kt | 50 +++++++++++++++++++ .../forward/MultiselectForwardActivity.kt | 9 +++- .../forward/MultiselectForwardFragment.kt | 7 +-- .../forward/MultiselectForwardFragmentArgs.kt | 15 +++--- 4 files changed, 71 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/color/ViewColorSet.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/color/ViewColorSet.kt b/app/src/main/java/org/thoughtcrime/securesms/color/ViewColorSet.kt new file mode 100644 index 0000000000..654d62d518 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/color/ViewColorSet.kt @@ -0,0 +1,50 @@ +package org.thoughtcrime.securesms.color + +import android.content.Context +import android.os.Parcelable +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.core.content.ContextCompat +import kotlinx.parcelize.Parcelize +import org.thoughtcrime.securesms.R + +/** + * Represents a set of colors to be applied to the foreground and background of a view. + * + * Supports mixing color ints and color resource ids. + */ +@Parcelize +data class ViewColorSet( + val foreground: ViewColor, + val background: ViewColor +) : Parcelable { + companion object { + fun forCustomColor(@ColorInt customColor: Int): ViewColorSet { + return ViewColorSet( + foreground = ViewColor.ColorResource(R.color.signal_colorOnCustom), + background = ViewColor.ColorValue(customColor) + ) + } + } + + @Parcelize + sealed class ViewColor : Parcelable { + + @ColorInt + abstract fun resolve(context: Context): Int + + @Parcelize + data class ColorValue(@ColorInt val colorInt: Int) : ViewColor() { + override fun resolve(context: Context): Int { + return colorInt + } + } + + @Parcelize + data class ColorResource(@ColorRes val colorRes: Int) : ViewColor() { + override fun resolve(context: Context): Int { + return ContextCompat.getColor(context, colorRes) + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardActivity.kt index 223b378907..8e0fd39ded 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardActivity.kt @@ -10,6 +10,7 @@ import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.color.ViewColorSet import org.thoughtcrime.securesms.components.FragmentWrapperActivity import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment.Companion.RESULT_SELECTION @@ -36,8 +37,14 @@ open class MultiselectForwardActivity : FragmentWrapperActivity(), MultiselectFo override fun getFragment(): Fragment { return MultiselectForwardFragment.create( args.let { - if (it.sendButtonTint == -1) { + if (it.sendButtonColors == null) { args.withSendButtonTint(ContextCompat.getColor(this, R.color.signal_colorPrimary)) + args.copy( + sendButtonColors = ViewColorSet( + foreground = ViewColorSet.ViewColor.ColorResource(R.color.signal_colorOnPrimary), + background = ViewColorSet.ViewColor.ColorResource(R.color.signal_colorPrimary) + ) + ) } else { args } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt index 7c6ebba575..77ec1289b3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt @@ -144,9 +144,10 @@ class MultiselectForwardFragment : val sendButton: AppCompatImageView = bottomBar.findViewById(R.id.share_confirm) val backgroundHelper: View = bottomBar.findViewById(R.id.background_helper) - if (args.sendButtonTint != -1) { - sendButton.setColorFilter(ContextCompat.getColor(requireContext(), R.color.signal_colorOnCustom)) - ViewCompat.setBackgroundTintList(sendButton, ColorStateList.valueOf(args.sendButtonTint)) + val sendButtonColors = args.sendButtonColors + if (sendButtonColors != null) { + sendButton.setColorFilter(sendButtonColors.foreground.resolve(requireContext())) + ViewCompat.setBackgroundTintList(sendButton, ColorStateList.valueOf(sendButtonColors.background.resolve(requireContext()))) } FullscreenHelper.configureBottomBarLayout(requireActivity(), bottomBarSpacer, bottomBar) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragmentArgs.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragmentArgs.kt index 1d34fa3f30..940ee8fdd7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragmentArgs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragmentArgs.kt @@ -12,6 +12,7 @@ import org.signal.core.util.ThreadUtil import org.signal.core.util.concurrent.SignalExecutors import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.attachments.Attachment +import org.thoughtcrime.securesms.color.ViewColorSet import org.thoughtcrime.securesms.conversation.ConversationMessage import org.thoughtcrime.securesms.conversation.mutiselect.Multiselect import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart @@ -43,12 +44,12 @@ data class MultiselectForwardFragmentArgs @JvmOverloads constructor( val forceDisableAddMessage: Boolean = false, val forceSelectionOnly: Boolean = false, val selectSingleRecipient: Boolean = false, - @ColorInt val sendButtonTint: Int = -1, + val sendButtonColors: ViewColorSet? = null, val storySendRequirements: Stories.MediaTransform.SendRequirements = Stories.MediaTransform.SendRequirements.CAN_NOT_SEND, val isSearchEnabled: Boolean = true ) : Parcelable { - fun withSendButtonTint(@ColorInt sendButtonTint: Int) = copy(sendButtonTint = sendButtonTint) + fun withSendButtonTint(@ColorInt sendButtonTint: Int) = copy(sendButtonColors = ViewColorSet.forCustomColor(sendButtonTint)) companion object { @JvmStatic @@ -61,9 +62,11 @@ data class MultiselectForwardFragmentArgs @JvmOverloads constructor( .withDataType(mediaType) .build() - val sendButtonTint: Int = threadId.takeIf { it > 0 } - ?.let { SignalDatabase.threads.getRecipientForThreadId(it) }?.chatColors?.asSingleColor() - ?: -1 + val sendButtonColors: ViewColorSet? = threadId.takeIf { it > 0 } + ?.let { SignalDatabase.threads.getRecipientForThreadId(it) } + ?.chatColors + ?.asSingleColor() + ?.let { ViewColorSet.forCustomColor(it) } ThreadUtil.runOnMain { consumer.accept( @@ -71,7 +74,7 @@ data class MultiselectForwardFragmentArgs @JvmOverloads constructor( isMmsSupported, listOf(multiShareArgs), storySendRequirements = Stories.MediaTransform.SendRequirements.CAN_NOT_SEND, - sendButtonTint = sendButtonTint + sendButtonColors = sendButtonColors ) ) } From 8a452ddf119f2bd99b102fb1e0260ae0c2cfd877 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 6 Oct 2022 13:29:33 -0400 Subject: [PATCH 47/78] Allow remote deletes to be tagged with story=true. --- .../thoughtcrime/securesms/jobs/PushDecryptMessageJob.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java index f387abb3f3..97201f3c17 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java @@ -193,6 +193,12 @@ private boolean isStoryMessage(@NonNull DecryptionResult result) { return true; } + if (result.getContent().getDataMessage().isPresent() && + result.getContent().getDataMessage().get().getRemoteDelete().isPresent()) + { + return true; + } + return false; } From 891c99a1485870212b866bf0ec1c9d7355a46653 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 6 Oct 2022 14:53:45 -0300 Subject: [PATCH 48/78] Do not allow users to attempt to send story replies to an inactive group. --- .../reply/group/StoryGroupReplyFragment.kt | 24 ++++++++++--- .../layout/stories_group_replies_fragment.xml | 34 +++++++++++++++---- app/src/main/res/values/strings.xml | 2 ++ 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyFragment.kt index 8806782e3c..ba4e10c5c2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyFragment.kt @@ -16,7 +16,6 @@ import com.google.android.material.bottomsheet.BottomSheetBehaviorHack import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.dialog.MaterialAlertDialogBuilder import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.kotlin.plusAssign import io.reactivex.rxjava3.kotlin.subscribeBy import org.signal.core.util.concurrent.SignalExecutors import org.signal.core.util.logging.Log @@ -146,6 +145,7 @@ class StoryGroupReplyFragment : private lateinit var adapter: PagingMappingAdapter private lateinit var dataObserver: RecyclerView.AdapterDataObserver private lateinit var composer: StoryReplyComposer + private lateinit var notInGroup: View private var markReadHelper: MarkReadHelper? = null @@ -164,6 +164,7 @@ class StoryGroupReplyFragment : recyclerView = view.findViewById(R.id.recycler) composer = view.findViewById(R.id.composer) + notInGroup = view.findViewById(R.id.not_in_group) lifecycleDisposable.bindTo(viewLifecycleOwner) @@ -215,10 +216,7 @@ class StoryGroupReplyFragment : adapter.registerAdapterDataObserver(dataObserver) initializeMentions() - - if (savedInstanceState == null) { - ViewUtil.focusAndShowKeyboard(composer) - } + initializeComposer(savedInstanceState) recyclerView.addOnScrollListener(GroupReplyScrollObserver()) } @@ -433,6 +431,22 @@ class StoryGroupReplyFragment : sendReaction(emoji) } + private fun initializeComposer(savedInstanceState: Bundle?) { + val isActiveGroup = Recipient.observable(groupRecipientId).map { it.isActiveGroup } + if (savedInstanceState == null) { + lifecycleDisposable += isActiveGroup.firstOrError().observeOn(AndroidSchedulers.mainThread()).subscribe { active -> + if (active) { + ViewUtil.focusAndShowKeyboard(composer) + } + } + } + + lifecycleDisposable += isActiveGroup.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread()).forEach { active -> + composer.visible = active + notInGroup.visible = !active + } + } + private fun initializeMentions() { inlineQueryResultsController = InlineQueryResultsController( requireContext(), diff --git a/app/src/main/res/layout/stories_group_replies_fragment.xml b/app/src/main/res/layout/stories_group_replies_fragment.xml index 1ecb934529..caabd0232a 100644 --- a/app/src/main/res/layout/stories_group_replies_fragment.xml +++ b/app/src/main/res/layout/stories_group_replies_fragment.xml @@ -1,11 +1,11 @@ + tools:layout_gravity="bottom" + tools:viewBindingIgnore="true"> - + app:layout_constraintBottom_toBottomOf="parent"> + + + + + \ 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 1c49ca9e85..94d567e9a9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4829,6 +4829,8 @@ Remove viewer No replies yet + + You can\'t reply to this story because you\'re no longer a member of this group. Reacted to the story From e3dff46136eb7ba7a52dc8f795fd6c0ea8542042 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 6 Oct 2022 13:57:45 -0400 Subject: [PATCH 49/78] Rotate AccountRecord.storiesDisabled iOS had a bug and we need to try again. --- libsignal/service/src/main/proto/StorageService.proto | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libsignal/service/src/main/proto/StorageService.proto b/libsignal/service/src/main/proto/StorageService.proto index 1ad46d802d..3b47f03382 100644 --- a/libsignal/service/src/main/proto/StorageService.proto +++ b/libsignal/service/src/main/proto/StorageService.proto @@ -174,7 +174,8 @@ message AccountRecord { bool keepMutedChatsArchived = 25; bool hasSetMyStoriesPrivacy = 26; bool hasViewedOnboardingStory = 27; - bool storiesDisabled = 28; + reserved /* storiesDisabled */ 28; + bool storiesDisabled = 29; } message StoryDistributionListRecord { From 72347af967fd397cc6e25d22bdf7167220cd529e Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 6 Oct 2022 16:17:36 -0300 Subject: [PATCH 50/78] Disassociate direct replies when remote-deleting a story. --- .../thoughtcrime/securesms/database/MmsDatabase.java | 12 ++++++++++++ .../thoughtcrime/securesms/database/model/Quote.java | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index dd6b2b5216..53dede3bd9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -989,6 +989,17 @@ public int deleteStoriesOlderThan(long timestamp, boolean hasSeenReleaseChannelS } } + private void disassociateStoryQuotes(long storyId) { + ContentValues contentValues = new ContentValues(2); + contentValues.put(QUOTE_MISSING, 1); + contentValues.putNull(QUOTE_BODY); + + getWritableDatabase().update(TABLE_NAME, + contentValues, + PARENT_STORY_ID + " = ?", + SqlUtil.buildArgs(new ParentStoryId.DirectReply(storyId).serialize())); + } + @Override public boolean isGroupQuitMessage(long messageId) { SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); @@ -1380,6 +1391,7 @@ public void markAsRemoteDelete(long messageId) { SignalDatabase.messageLog().deleteAllRelatedToMessage(messageId, true); SignalDatabase.reactions().deleteReactions(new MessageId(messageId, true)); deleteGroupStoryReplies(messageId); + disassociateStoryQuotes(messageId); threadId = getThreadIdForMessage(messageId); SignalDatabase.threads().update(threadId, false); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/Quote.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/Quote.java index 795af6af5a..de84d5ee89 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/Quote.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/Quote.java @@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.mms.QuoteModel; import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.util.Util; import java.util.List; @@ -37,7 +38,7 @@ public Quote(long id, this.mentions = mentions; this.quoteType = quoteType; - SpannableString spannable = new SpannableString(text); + SpannableString spannable = new SpannableString(Util.emptyIfNull(text)); MentionAnnotation.setMentionAnnotations(spannable, mentions); this.text = spannable; From 9d469db7aebe6e8719000546e23a7dc2853c4e20 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 6 Oct 2022 16:19:25 -0300 Subject: [PATCH 51/78] Move stories above app security section. --- .../app/privacy/PrivacySettingsFragment.kt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsFragment.kt index b9042a5902..bb3fb76a3e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsFragment.kt @@ -199,6 +199,18 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac ) ) + if (Stories.isFeatureAvailable()) { + dividerPref() + + clickPref( + title = DSLSettingsText.from(R.string.preferences__stories), + summary = DSLSettingsText.from(R.string.PrivacySettingsFragment__manage_your_stories), + onClick = { + findNavController().safeNavigate(PrivacySettingsFragmentDirections.actionPrivacySettingsFragmentToStoryPrivacySettings(R.string.preferences__stories)) + } + ) + } + dividerPref() sectionHeaderPref(R.string.PrivacySettingsFragment__app_security) @@ -333,18 +345,6 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac summary = DSLSettingsText.from(incognitoSummary), ) - if (Stories.isFeatureAvailable()) { - dividerPref() - - clickPref( - title = DSLSettingsText.from(R.string.preferences__stories), - summary = DSLSettingsText.from(R.string.PrivacySettingsFragment__manage_your_stories), - onClick = { - findNavController().safeNavigate(PrivacySettingsFragmentDirections.actionPrivacySettingsFragmentToStoryPrivacySettings(R.string.preferences__stories)) - } - ) - } - dividerPref() sectionHeaderPref(R.string.preferences_app_protection__payments) From 22e97457a3689a6ece4b59e228e84493fc0bcc27 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 6 Oct 2022 21:43:52 +0200 Subject: [PATCH 52/78] Fix sending normal group messages when falling back to socket. In the sendGroupMessage message the socket fallback for sending normal group messages always set the story parameter to true. This causes the message to be discarded by the receivers, because it has a story envelope, but no story content > Envelope was flagged as a story, but it did not have any story-related content! Dropping. Issue was introduced in 3895578d5188c7d2ef3378d61d7a9aa6efa03928 Closes #12496 --- .../signalservice/api/SignalServiceMessageSender.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index a8a86a4b66..9566b8c4c3 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -1968,7 +1968,7 @@ private List sendGroupMessage(DistributionId dist Log.w(TAG, "[sendGroupMessage][" + timestamp + "] Pipe failed, falling back... (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")"); } - SendGroupMessageResponse response = socket.sendGroupMessage(ciphertext, joinedUnidentifiedAccess, timestamp, online, urgent, true); + SendGroupMessageResponse response = socket.sendGroupMessage(ciphertext, joinedUnidentifiedAccess, timestamp, online, urgent, story); return transformGroupResponseToMessageResults(targetInfo.devices, response, content); } catch (GroupMismatchedDevicesException e) { Log.w(TAG, "[sendGroupMessage][" + timestamp + "] Handling mismatched devices. (" + e.getMessage() + ")"); From 4ee82181945f84bfe1ae9e0a118052e07b7d6144 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 6 Oct 2022 16:12:25 -0400 Subject: [PATCH 53/78] Updated language translations. --- app/src/main/res/values-af/strings.xml | 2 ++ app/src/main/res/values-ar/strings.xml | 2 ++ app/src/main/res/values-az/strings.xml | 2 ++ app/src/main/res/values-bg/strings.xml | 2 ++ app/src/main/res/values-bn/strings.xml | 2 ++ app/src/main/res/values-bs/strings.xml | 2 ++ app/src/main/res/values-ca/strings.xml | 2 ++ app/src/main/res/values-cs/strings.xml | 2 ++ app/src/main/res/values-da/strings.xml | 2 ++ app/src/main/res/values-de/strings.xml | 2 ++ app/src/main/res/values-el/strings.xml | 2 ++ app/src/main/res/values-es/strings.xml | 2 ++ app/src/main/res/values-et/strings.xml | 2 ++ app/src/main/res/values-eu/strings.xml | 2 ++ app/src/main/res/values-fa/strings.xml | 2 ++ app/src/main/res/values-fi/strings.xml | 2 ++ app/src/main/res/values-fr/strings.xml | 2 ++ app/src/main/res/values-ga/strings.xml | 36 ++++++++++++---------- app/src/main/res/values-gl/strings.xml | 2 ++ app/src/main/res/values-gu/strings.xml | 2 ++ app/src/main/res/values-hi/strings.xml | 2 ++ app/src/main/res/values-hr/strings.xml | 2 ++ app/src/main/res/values-hu/strings.xml | 2 ++ app/src/main/res/values-in/strings.xml | 2 ++ app/src/main/res/values-it/strings.xml | 2 ++ app/src/main/res/values-iw/strings.xml | 2 ++ app/src/main/res/values-ja/strings.xml | 2 ++ app/src/main/res/values-ka/strings.xml | 2 ++ app/src/main/res/values-kk/strings.xml | 2 ++ app/src/main/res/values-km/strings.xml | 2 ++ app/src/main/res/values-kn/strings.xml | 2 ++ app/src/main/res/values-ko/strings.xml | 2 ++ app/src/main/res/values-ky/strings.xml | 2 ++ app/src/main/res/values-lt/strings.xml | 2 ++ app/src/main/res/values-lv/strings.xml | 2 ++ app/src/main/res/values-mk/strings.xml | 2 ++ app/src/main/res/values-ml/strings.xml | 2 ++ app/src/main/res/values-mr/strings.xml | 2 ++ app/src/main/res/values-ms/strings.xml | 2 ++ app/src/main/res/values-my/strings.xml | 2 ++ app/src/main/res/values-nb/strings.xml | 2 ++ app/src/main/res/values-nl/strings.xml | 2 ++ app/src/main/res/values-pa/strings.xml | 2 ++ app/src/main/res/values-pl/strings.xml | 2 ++ app/src/main/res/values-pt-rBR/strings.xml | 2 ++ app/src/main/res/values-pt/strings.xml | 2 ++ app/src/main/res/values-ro/strings.xml | 2 ++ app/src/main/res/values-ru/strings.xml | 2 ++ app/src/main/res/values-sk/strings.xml | 2 ++ app/src/main/res/values-sl/strings.xml | 2 ++ app/src/main/res/values-sq/strings.xml | 2 ++ app/src/main/res/values-sr/strings.xml | 2 ++ app/src/main/res/values-sv/strings.xml | 2 ++ app/src/main/res/values-sw/strings.xml | 2 ++ app/src/main/res/values-ta/strings.xml | 2 ++ app/src/main/res/values-te/strings.xml | 2 ++ app/src/main/res/values-th/strings.xml | 2 ++ app/src/main/res/values-tl/strings.xml | 2 ++ app/src/main/res/values-tr/strings.xml | 2 ++ app/src/main/res/values-uk/strings.xml | 2 ++ app/src/main/res/values-ur/strings.xml | 2 ++ app/src/main/res/values-vi/strings.xml | 2 ++ app/src/main/res/values-zh-rCN/strings.xml | 2 ++ app/src/main/res/values-zh-rHK/strings.xml | 2 ++ app/src/main/res/values-zh-rTW/strings.xml | 2 ++ 65 files changed, 147 insertions(+), 17 deletions(-) diff --git a/app/src/main/res/values-af/strings.xml b/app/src/main/res/values-af/strings.xml index fe9c85107a..05dd81e503 100644 --- a/app/src/main/res/values-af/strings.xml +++ b/app/src/main/res/values-af/strings.xml @@ -4824,6 +4824,8 @@ Verwyder kyker. Nog geen antwoorde nie + + You can\'t reply to this story because you\'re no longer a member of this group. Het op die storie gereageer diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 1508620357..263133adc1 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -5252,6 +5252,8 @@ إزالة المُشاهد؟ لا جواب حاليا + + You can\'t reply to this story because you\'re no longer a member of this group. المتفاعلون مع القصة diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index a307084b26..fa62ffdde7 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -4824,6 +4824,8 @@ İzləyicini sil Hələ ki, cavab yoxdur + + You can\'t reply to this story because you\'re no longer a member of this group. Hekayəyə reaksiya verildi diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 7636e8fce5..4b92601017 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -4824,6 +4824,8 @@ Премахване на зрител Все още няма отговори + + You can\'t reply to this story because you\'re no longer a member of this group. Реагира на историята diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 4dd7f5a9f8..9f84a047d4 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -4824,6 +4824,8 @@ দর্শককে বাদ দিন এখন পর্যন্ত কোন রিপ্লাই নেই + + You can\'t reply to this story because you\'re no longer a member of this group. স্টোরি-তে রিঅ্যাক্ট করা হয়েছে diff --git a/app/src/main/res/values-bs/strings.xml b/app/src/main/res/values-bs/strings.xml index bb2dc828cb..66f73da23e 100644 --- a/app/src/main/res/values-bs/strings.xml +++ b/app/src/main/res/values-bs/strings.xml @@ -5038,6 +5038,8 @@ Ukloni gledaoca Još nema odgovora + + You can\'t reply to this story because you\'re no longer a member of this group. Reagovao/la na priču diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 0ff8d3bd72..dacc3eb443 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -4824,6 +4824,8 @@ Eliminar espectador Encara no té respostes + + You can\'t reply to this story because you\'re no longer a member of this group. Ha reaccionat a la història diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 6c09639e80..2d8f42411d 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -5038,6 +5038,8 @@ Odebrat sledujícího Zatím žádné odpovědi + + You can\'t reply to this story because you\'re no longer a member of this group. Reagoval(a) na příběh diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index f720e47c76..f205e0424c 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -4824,6 +4824,8 @@ Fjern tilskuer Ingen svar endnu + + You can\'t reply to this story because you\'re no longer a member of this group. Reagerede på historien diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 833c29c904..034c333a86 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -4824,6 +4824,8 @@ Betrachter entfernen Bisher keine Antworten + + You can\'t reply to this story because you\'re no longer a member of this group. Auf Story reagiert diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index d9fd45a2e7..7fdb8486ea 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -4824,6 +4824,8 @@ Αφαίρεση θεατή Καμία απάντηση ακόμα + + You can\'t reply to this story because you\'re no longer a member of this group. Αντέδρασε στην ιστορία diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 0e9a1d1584..dca66444ac 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -4824,6 +4824,8 @@ Quitar espectador Sin respuestas por ahora + + You can\'t reply to this story because you\'re no longer a member of this group. Reaccionó a la historia diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 23f7808adf..bfc462d2e2 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -4824,6 +4824,8 @@ Eemalda vaataja Vastuseid veel pole + + You can\'t reply to this story because you\'re no longer a member of this group. Reageeris loole diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 91d4fe5b5c..e5692aa970 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -4824,6 +4824,8 @@ Kendu ikuslea Erantzunik ez oraindik + + You can\'t reply to this story because you\'re no longer a member of this group. Storie-ari erreakzionatu diozu diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 503762b3c8..93046cb099 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -4824,6 +4824,8 @@ حذف بازدیدکننده هنوز پاسخی داده نشده است + + You can\'t reply to this story because you\'re no longer a member of this group. به استوری واکنش نشان داده شد diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 233168507b..25f10c1829 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -4824,6 +4824,8 @@ Poista katsoja Ei vastauksia vielä + + You can\'t reply to this story because you\'re no longer a member of this group. Lähetti reaktion tarinaan diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 893e7ccffc..d5c58805bb 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -4824,6 +4824,8 @@ Retirer le spectateur Aucune réponse pour le moment + + You can\'t reply to this story because you\'re no longer a member of this group. A réagi à l’histoire diff --git a/app/src/main/res/values-ga/strings.xml b/app/src/main/res/values-ga/strings.xml index 98937f83ca..e0c7debb12 100644 --- a/app/src/main/res/values-ga/strings.xml +++ b/app/src/main/res/values-ga/strings.xml @@ -4103,9 +4103,9 @@ Ligeann do fhrása athshlánaithe duit d\'iarmhéid a aischur sa chás is measa. Molaimid go láidir duit é a shábháil. - Ná bac le Frása Athshlánaithe + Scipeáil Frása Athshlánaithe - Cealaigh + Cuir ar ceal Paste Recovery Phrase @@ -4259,9 +4259,9 @@ Remove SMS messages - Teachtaireachtaí SMS á mbaint de Signal... + Teachtaireachtaí SMS á mbaint ó Signal... - Is féidir leat teachtaireachtaí SMS a bhaint de Signal i Socruithe uair ar bith. + Is féidir leat teachtaireachtaí SMS a bhaint ó Signal sna Socruithe am ar bith. Teachtaireachtaí @@ -4507,27 +4507,27 @@ Teachtaireacht - Glao Fuaime + Guthghlao Físghlao - Bain é + Bain - Bac Nua + Cuir bac air/uirthi Bain %1$s? Ní fheicfidh tú an duine seo agus cuardach á dhéanamh agat. Gheobhaidh tú iarratas teachtaireachta má sheolfaidh an duine sin teachtaireacht chugat amach anseo. - Baineadh %1$s + %1$s bainte - Cuireadh bac ar %1$s + Bac curtha ar %1$s Ní féidir %1$s a bhaint - Tá an duine seo sábháilte i dTeagmhálaithe ar do ghléas. Scrios an duine sin de do Theagmhálaithe agus triail arís. + Tá an duine seo sábháilte i dTeagmhálaithe ar do ghléas. Scrios an duine sin ó do Theagmhálaithe agus triail arís. - Féach an teagmhálaí + Féach ar an teagmhálaí Cuardaigh ainm nó uimhir @@ -5046,7 +5046,7 @@ Tapáil chun scéal a chur leis - Níl aon nuashonruithe le déanaí ann lena dtaispeáint díreach anois. + Níl aon nuashonruithe le déanaí ann le taispeáint díreach anois. Hide Story @@ -5145,6 +5145,8 @@ Bain an t-amharcóir No replies yet + + You can\'t reply to this story because you\'re no longer a member of this group. Reacted to the story @@ -5593,7 +5595,7 @@ Seolta ó - Teipthe + Theip air Eolas @@ -5634,7 +5636,7 @@ Export your SMS messages - Is féidir leat do theachtaireachtaí SMS a easpórtáil go dtí bunachar sonraí SMS do ghutháin. Ligeann sé seo d\'aipeanna SMS eile ar do ghuthán iad a rochtain agus a iompórtáil. Ní chruthaíonn sé seo comhad inroinnte de stair do theachtaireachtaí SMS. + Is féidir leat do theachtaireachtaí SMS a easpórtáil chuig bunachar sonraí SMS do ghutháin. Ligeann sé seo d\'aipeanna eile SMS ar do ghuthán iad a rochtain agus a iompórtáil. Ní chruthaíonn sé seo comhad in-chomhroinnte de stair do theachtaireachtaí SMS. Ar Aghaidh @@ -5664,13 +5666,13 @@ Roghnaigh \"Aip SMS\" ón liosta - Roghnaigh aip eile chun í a úsáid le haghaidh cur teachtaireachtaí SMS + Roghnaigh aip eile le húsáid le cur teachtaireachtaí SMS Fill ar Signal Oscail aip Socruithe do ghutháin - Téigh go dtí \"Aipeanna\" > \"Aipeanna réamhshocraithe\" > \"Aip SMS\" + Téigh chuig \"Aipeanna\" > \"Aipeanna réamhshocraithe\" > \"Aip SMS\" @@ -5680,7 +5682,7 @@ Remove SMS messages from Signal? - Is féidir leat teachtaireachtaí SMS a bhaint de Signal anois chun spás stórála a ghlanadh. Beidh siad fós ar fáil d\'aipeanna SMS eile ar do ghuthán fiú má bhaineann tú iad. + Is féidir leat teachtaireachtaí SMS a bhaint ó Signal anois chun spás stórála a ghlanadh. Beidh siad fós ar fáil ag aipeanna eile SMS ar do ghuthán fiú má bhaineann tú iad. diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 9815c88d8e..2821c85e32 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -4824,6 +4824,8 @@ Eliminar espectador Sen respostas + + You can\'t reply to this story because you\'re no longer a member of this group. Reaccionou á túa historia diff --git a/app/src/main/res/values-gu/strings.xml b/app/src/main/res/values-gu/strings.xml index b7baff39be..0fde9bcb18 100644 --- a/app/src/main/res/values-gu/strings.xml +++ b/app/src/main/res/values-gu/strings.xml @@ -4824,6 +4824,8 @@ દર્શકને દૂર કરો હજી કોઈ જવાબો નથી + + You can\'t reply to this story because you\'re no longer a member of this group. સ્ટોરી પર પ્રતિક્રિયા આપી diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index a13814807c..1d74d732cf 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -4824,6 +4824,8 @@ व्यूअर को हटाएँ कोई जवाब नहीं + + You can\'t reply to this story because you\'re no longer a member of this group. स्टोरी पर प्रतिक्रिया दी diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 8e21fefa7c..ff2c676360 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -5038,6 +5038,8 @@ Ukloni gledatelja Još nema odgovora + + You can\'t reply to this story because you\'re no longer a member of this group. Reakcija na priču diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 3a1cba8243..c93a77e0ae 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -4824,6 +4824,8 @@ Néző eltávolítása Még nem érkeztek reakciók + + You can\'t reply to this story because you\'re no longer a member of this group. Reakció elküldve a történetre diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 64f72f0d15..9d14bbc666 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -4717,6 +4717,8 @@ Hapus pemirsa Belum ada balasan + + You can\'t reply to this story because you\'re no longer a member of this group. Menanggapi cerita diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 32cd5ea2a6..8e84ecae17 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -4824,6 +4824,8 @@ Rimuovi persona Ancora nessuna risposta + + You can\'t reply to this story because you\'re no longer a member of this group. Ha reagito alla storia diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 702e196a18..ed17f19716 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -5038,6 +5038,8 @@ הסרת צופה אין תשובות עדין + + You can\'t reply to this story because you\'re no longer a member of this group. הגיב/ה אל סיפור diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 9cc20c6db7..89e41f8626 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -4717,6 +4717,8 @@ 閲覧者を削除 返信 なし + + You can\'t reply to this story because you\'re no longer a member of this group. ストーリーにリアクションがありました diff --git a/app/src/main/res/values-ka/strings.xml b/app/src/main/res/values-ka/strings.xml index 9f3069962f..c35716cf8b 100644 --- a/app/src/main/res/values-ka/strings.xml +++ b/app/src/main/res/values-ka/strings.xml @@ -4824,6 +4824,8 @@ მნახველის ამოშლა პასუხები ჯერ არაა + + You can\'t reply to this story because you\'re no longer a member of this group. Story-იზე გამოხმაურება diff --git a/app/src/main/res/values-kk/strings.xml b/app/src/main/res/values-kk/strings.xml index 64cd256ece..0527c3f322 100644 --- a/app/src/main/res/values-kk/strings.xml +++ b/app/src/main/res/values-kk/strings.xml @@ -4824,6 +4824,8 @@ Көрерменді өшіру Ешкім жауап жазбаған + + You can\'t reply to this story because you\'re no longer a member of this group. Осы сториске реакция қалдырды diff --git a/app/src/main/res/values-km/strings.xml b/app/src/main/res/values-km/strings.xml index bc6f7e792b..a332bc17c7 100644 --- a/app/src/main/res/values-km/strings.xml +++ b/app/src/main/res/values-km/strings.xml @@ -4717,6 +4717,8 @@ ដកអ្នកមើលចេញ មិនទាន់មានការឆ្លើយតបទេ + + You can\'t reply to this story because you\'re no longer a member of this group. បានប្រតិកម្មនឹងរឿងរ៉ាវ diff --git a/app/src/main/res/values-kn/strings.xml b/app/src/main/res/values-kn/strings.xml index ffcce25b6d..be1965778d 100644 --- a/app/src/main/res/values-kn/strings.xml +++ b/app/src/main/res/values-kn/strings.xml @@ -4824,6 +4824,8 @@ ವೀಕ್ಷಕರನ್ನು ತೆಗೆದುಹಾಕಿ ಇನ್ನೂ ಯಾವುದೇ ಪ್ರತ್ಯುತ್ತರವಿಲ್ಲ + + You can\'t reply to this story because you\'re no longer a member of this group. ಸ್ಟೋರಿಗೆ ಪ್ರತಿಕ್ರಿಯಿಸಲಾಗಿದೆ diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index f56e77f122..bf0dc2138c 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -4717,6 +4717,8 @@ 볼 수 있는 사람 제거 답장 없음 + + You can\'t reply to this story because you\'re no longer a member of this group. 스토리에 반응함 diff --git a/app/src/main/res/values-ky/strings.xml b/app/src/main/res/values-ky/strings.xml index 1c217bcc77..063a25c25b 100644 --- a/app/src/main/res/values-ky/strings.xml +++ b/app/src/main/res/values-ky/strings.xml @@ -4717,6 +4717,8 @@ Көрүүчүнү алып салуу Азырынча жооп жок + + You can\'t reply to this story because you\'re no longer a member of this group. Окуяга реакция кылды diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 1e0bea6ca9..b6607403b6 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -5038,6 +5038,8 @@ Pašalinti žiūrovą Atsakymų kol kas nėra + + You can\'t reply to this story because you\'re no longer a member of this group. Sureagavo į istoriją diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index a1c037d5a4..36e0da69dc 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -4931,6 +4931,8 @@ Noņemt skatītāju Vēl nav atbilžu + + You can\'t reply to this story because you\'re no longer a member of this group. Reaģēja uz stāstu diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index a4facc2bd6..40b34e7e9f 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -4824,6 +4824,8 @@ Отстрани го гледачот Сè уште нема одговори + + You can\'t reply to this story because you\'re no longer a member of this group. Реагираше на приказната diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index bab7d4c4a9..436b49e8af 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -4824,6 +4824,8 @@ കാണുന്നയാളെ നീക്കം ചെയ്യുക ഇതുവരെ മറുപടികളൊന്നുമില്ല + + You can\'t reply to this story because you\'re no longer a member of this group. സ്റ്റോറിയോട് പ്രതികരിച്ചു diff --git a/app/src/main/res/values-mr/strings.xml b/app/src/main/res/values-mr/strings.xml index 6b259b9772..9fae8ab357 100644 --- a/app/src/main/res/values-mr/strings.xml +++ b/app/src/main/res/values-mr/strings.xml @@ -4824,6 +4824,8 @@ दर्शकाला हटवा अद्याप कोणतीही प्रत्युत्तरे नाहीत + + You can\'t reply to this story because you\'re no longer a member of this group. स्टोरीला प्रतिक्रिया दिली diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index b6cfb64133..204b7b40f6 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -4717,6 +4717,8 @@ Alih keluar penonton Tiada balasan lagi + + You can\'t reply to this story because you\'re no longer a member of this group. Telah bereaksi kepada cerita diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index 34fcb71426..53f392e966 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -4717,6 +4717,8 @@ ကြည့်ရှုသူအား ဖယ်ရှားရန် ပြန်စာများ မရှိသေးပါ + + You can\'t reply to this story because you\'re no longer a member of this group. စတိုရီကို တုံ့ပြန်မှု ပေးခဲ့ပါသည် diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index d40f254e11..3570d27dae 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -4824,6 +4824,8 @@ Fjern seer Ingen svar ennå + + You can\'t reply to this story because you\'re no longer a member of this group. Reagerte på storyen diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 3afdc89614..2d8d0f9f4e 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -4824,6 +4824,8 @@ Gebruiker verwijderen Nog geen reacties + + You can\'t reply to this story because you\'re no longer a member of this group. Reageerde op dit verhaal diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index f2b27870b9..2cb9ea7d03 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -4824,6 +4824,8 @@ ਦਰਸ਼ਕ ਨੂੰ ਹਟਾਓ ਅਜੇ ਤੱਕ ਕਿਸੇ ਨੇ ਜਵਾਬ ਨਹੀਂ ਦਿੱਤਾ + + You can\'t reply to this story because you\'re no longer a member of this group. ਸਟੋਰੀ ਉੱਤੇ ਰਿਐਕਸ਼ਨ ਦਿੱਤਾ diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 24485e3bb2..c476b962b1 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -5038,6 +5038,8 @@ Usuń widza Brak odpowiedzi + + You can\'t reply to this story because you\'re no longer a member of this group. Zareagowano na historię diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 6596da1fe9..730d5435c2 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -4824,6 +4824,8 @@ Remover visualizador Ainda não há respostas + + You can\'t reply to this story because you\'re no longer a member of this group. Reagiu ao story diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 5062999435..7bbef289e9 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -4824,6 +4824,8 @@ Remover visualizador Ainda não existem respostas + + You can\'t reply to this story because you\'re no longer a member of this group. Reagiu à história diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 98f50cd561..682cc35eea 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -4931,6 +4931,8 @@ Elimină vizualizatorul Niciun răspuns încă + + You can\'t reply to this story because you\'re no longer a member of this group. Ai reacționat la poveste diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 56afaa9b71..61987c9992 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -5038,6 +5038,8 @@ Удалить зрителя Ещё нет ответов + + You can\'t reply to this story because you\'re no longer a member of this group. Отреагировал(-а) на историю diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index e094aa86bf..b8e8735b12 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -5038,6 +5038,8 @@ Odstrániť sledovateľa Zatiaľ žiadne odpovede + + You can\'t reply to this story because you\'re no longer a member of this group. Reagoval na príbeh diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 0862357903..4ed38cfcb3 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -5038,6 +5038,8 @@ Odstrani gledalca_ko Brez odgovorov + + You can\'t reply to this story because you\'re no longer a member of this group. Reakcije na zgodbo diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml index f8af5be838..ce1efb7c4e 100644 --- a/app/src/main/res/values-sq/strings.xml +++ b/app/src/main/res/values-sq/strings.xml @@ -4824,6 +4824,8 @@ Hiqe shikuesin Ende pa përgjigje + + You can\'t reply to this story because you\'re no longer a member of this group. Reagoi ndaj historisë diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 8ad906ae28..cb07a77352 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -4824,6 +4824,8 @@ Ukloni gledaoca Без одговора + + You can\'t reply to this story because you\'re no longer a member of this group. Реаговано на причу diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 97c5a46274..1e8941a7fc 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -4824,6 +4824,8 @@ Ta bort tittare Inga svar ännu + + You can\'t reply to this story because you\'re no longer a member of this group. Reagerade på berättelsen diff --git a/app/src/main/res/values-sw/strings.xml b/app/src/main/res/values-sw/strings.xml index d3ccfc5eea..87b9772b69 100644 --- a/app/src/main/res/values-sw/strings.xml +++ b/app/src/main/res/values-sw/strings.xml @@ -4824,6 +4824,8 @@ Muondoe mtazamaji Hakuna majibu bado + + You can\'t reply to this story because you\'re no longer a member of this group. Ame-react kwa stori diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 3bac6ede24..905b440abd 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -4824,6 +4824,8 @@ பார்வையாளரை அகற்று இதுவரை பதில்கள் ஏதுமில்லை + + You can\'t reply to this story because you\'re no longer a member of this group. ஸ்டோரிக்கு பதிலளிக்கப்பட்டது diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index fc9881305a..2604db65ae 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -4824,6 +4824,8 @@ వీక్షకుడిని తొలగించండి ఇంకా ఎలాంటి రిప్లైలు లేవు + + You can\'t reply to this story because you\'re no longer a member of this group. స్టోరీకి రియాక్ట్ అయ్యారు diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index ed6a96566c..ffa60023c0 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -4717,6 +4717,8 @@ ลบผู้ชม ยังไม่มีการตอบกลับ + + You can\'t reply to this story because you\'re no longer a member of this group. ตอบสนองสตอรี่ของคุณ diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml index 3fbfb45633..37f63660c1 100644 --- a/app/src/main/res/values-tl/strings.xml +++ b/app/src/main/res/values-tl/strings.xml @@ -4824,6 +4824,8 @@ Tanggalin ang viewer Wala pang replies + + You can\'t reply to this story because you\'re no longer a member of this group. Reacted to the story diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 224390853f..0778dd68f2 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -4824,6 +4824,8 @@ Görüntüleyeni kaldır Henüz yanıt yok + + You can\'t reply to this story because you\'re no longer a member of this group. Hikayeye tepki verildi diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index b8000dd1c6..5c2b5dba8b 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -5038,6 +5038,8 @@ Вилучити глядача Ще немає відповідей + + You can\'t reply to this story because you\'re no longer a member of this group. Відреагував на історію diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index c239c76729..acc4db8e81 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -4824,6 +4824,8 @@ ناظر کو ہٹائیں ابھی تک کوئی جوابات نہیں + + You can\'t reply to this story because you\'re no longer a member of this group. اسٹوری پر ردعمل دیا diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 4c0a374e4d..7aab083c1f 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -4717,6 +4717,8 @@ Gỡ người xem Chưa có phản hồi + + You can\'t reply to this story because you\'re no longer a member of this group. Đã bày tỏ cảm xúc với story diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index e770a19875..fd3a23c81d 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -4717,6 +4717,8 @@ 移除访客 未有回复 + + You can\'t reply to this story because you\'re no longer a member of this group. 已回应此动态 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index ce48ff9b41..2e0abcbd8d 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -4717,6 +4717,8 @@ 移除瀏覽者 未有回覆 + + You can\'t reply to this story because you\'re no longer a member of this group. 對此限時動態表達了心情 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 98c9e3bf21..0267a69532 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -4717,6 +4717,8 @@ 移除瀏覽者 未有回覆 + + You can\'t reply to this story because you\'re no longer a member of this group. 以對此限時動態做出回應 From 742d1bece03573e9d08ffa5f1faad6ccaf58fb8a Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 6 Oct 2022 16:14:17 -0400 Subject: [PATCH 54/78] Bump version to 5.52.3 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ffca1b3551..ea1605dfbc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -57,8 +57,8 @@ ktlint { version = "0.43.2" } -def canonicalVersionCode = 1139 -def canonicalVersionName = "5.52.2" +def canonicalVersionCode = 1140 +def canonicalVersionName = "5.52.3" def postFixSize = 100 def abiPostFix = ['universal' : 0, From 20417565139583a22aa59d525fd300ee100374ef Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 6 Oct 2022 15:44:48 -0300 Subject: [PATCH 55/78] Story info page should mirror message details. --- .../messagedetails/MessageDetails.java | 18 ++--- .../MessageDetailsRepository.java | 31 +++++++- .../RecipientDeliveryStatus.java | 16 ++-- .../StoryInfoBottomSheetDialogFragment.kt | 64 +++++++++++++--- .../viewer/info/StoryInfoRecipientRow.kt | 18 ++--- .../viewer/info/StoryInfoRepository.kt | 73 ------------------- .../stories/viewer/info/StoryInfoState.kt | 22 +++--- .../stories/viewer/info/StoryInfoViewModel.kt | 54 ++------------ 8 files changed, 122 insertions(+), 174 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoRepository.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetails.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetails.java index cb8b30beec..03d141fac2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetails.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetails.java @@ -12,7 +12,7 @@ import java.util.List; import java.util.TreeSet; -final class MessageDetails { +public final class MessageDetails { private static final Comparator HAS_DISPLAY_NAME = (r1, r2) -> Boolean.compare(r2.getRecipient().hasAUserSetDisplayName(ApplicationDependencies.getApplication()), r1.getRecipient().hasAUserSetDisplayName(ApplicationDependencies.getApplication())); private static final Comparator ALPHABETICAL = (r1, r2) -> r1.getRecipient().getDisplayName(ApplicationDependencies.getApplication()).compareToIgnoreCase(r2.getRecipient().getDisplayName(ApplicationDependencies.getApplication())); private static final Comparator RECIPIENT_COMPARATOR = ComparatorCompat.chain(HAS_DISPLAY_NAME).thenComparing(ALPHABETICAL); @@ -71,33 +71,33 @@ final class MessageDetails { } } - @NonNull ConversationMessage getConversationMessage() { + public @NonNull ConversationMessage getConversationMessage() { return conversationMessage; } - @NonNull Collection getPending() { + public @NonNull Collection getPending() { return pending; } - @NonNull Collection getSent() { + public @NonNull Collection getSent() { return sent; } - @NonNull Collection getSkipped() {return skipped;} + public @NonNull Collection getSkipped() {return skipped;} - @NonNull Collection getDelivered() { + public @NonNull Collection getDelivered() { return delivered; } - @NonNull Collection getRead() { + public @NonNull Collection getRead() { return read; } - @NonNull Collection getNotSent() { + public @NonNull Collection getNotSent() { return notSent; } - @NonNull Collection getViewed() { + public @NonNull Collection getViewed() { return viewed; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsRepository.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsRepository.java index 8d92baf1c9..60efc0c5c4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsRepository.java @@ -10,9 +10,11 @@ import org.signal.core.util.concurrent.SignalExecutors; import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory; +import org.thoughtcrime.securesms.database.DatabaseObserver; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.GroupReceiptDatabase; import org.thoughtcrime.securesms.database.MmsSmsDatabase; +import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.documents.NetworkFailure; @@ -24,7 +26,10 @@ import java.util.LinkedList; import java.util.List; -final class MessageDetailsRepository { +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public final class MessageDetailsRepository { private final Context context = ApplicationDependencies.getApplication(); @@ -44,11 +49,33 @@ final class MessageDetailsRepository { return liveData; } + public @NonNull Observable getMessageDetails(@NonNull MessageId messageId) { + return Observable.create(emitter -> { + DatabaseObserver.MessageObserver messageObserver = mId -> { + try { + MessageRecord messageRecord = messageId.isMms() ? SignalDatabase.mms().getMessageRecord(messageId.getId()) + : SignalDatabase.sms().getMessageRecord(messageId.getId()); + + MessageDetails messageDetails = getRecipientDeliveryStatusesInternal(messageRecord); + + emitter.onNext(messageDetails); + } catch (NoSuchMessageException e) { + emitter.onError(e); + } + }; + + ApplicationDependencies.getDatabaseObserver().registerMessageUpdateObserver(messageObserver); + emitter.setCancellable(() -> ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageObserver)); + + messageObserver.onMessageChanged(messageId); + }).observeOn(Schedulers.io()); + } + @WorkerThread private @NonNull MessageDetails getRecipientDeliveryStatusesInternal(@NonNull MessageRecord messageRecord) { List recipients = new LinkedList<>(); - if (!messageRecord.getRecipient().isGroup()) { + if (!messageRecord.getRecipient().isGroup() && !messageRecord.getRecipient().isDistributionList()) { recipients.add(new RecipientDeliveryStatus(messageRecord, messageRecord.getRecipient(), getStatusFor(messageRecord), diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/RecipientDeliveryStatus.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/RecipientDeliveryStatus.java index 1f2d9e247f..baee7b158a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/RecipientDeliveryStatus.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/RecipientDeliveryStatus.java @@ -8,7 +8,7 @@ import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.recipients.Recipient; -final class RecipientDeliveryStatus { +public final class RecipientDeliveryStatus { enum Status { UNKNOWN, PENDING, SENT, DELIVERED, READ, VIEWED, SKIPPED, @@ -32,31 +32,31 @@ enum Status { this.keyMismatchFailure = keyMismatchFailure; } - @NonNull MessageRecord getMessageRecord() { + public @NonNull MessageRecord getMessageRecord() { return messageRecord; } - @NonNull Status getDeliveryStatus() { + public @NonNull Status getDeliveryStatus() { return deliveryStatus; } - boolean isUnidentified() { + public boolean isUnidentified() { return isUnidentified; } - long getTimestamp() { + public long getTimestamp() { return timestamp; } - @NonNull Recipient getRecipient() { + public @NonNull Recipient getRecipient() { return recipient; } - @Nullable NetworkFailure getNetworkFailure() { + public @Nullable NetworkFailure getNetworkFailure() { return networkFailure; } - @Nullable IdentityKeyMismatch getKeyMismatchFailure() { + public @Nullable IdentityKeyMismatch getKeyMismatchFailure() { return keyMismatchFailure; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoBottomSheetDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoBottomSheetDialogFragment.kt index 3b9b276a2f..1f26a14a34 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoBottomSheetDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoBottomSheetDialogFragment.kt @@ -1,12 +1,14 @@ package org.thoughtcrime.securesms.stories.viewer.info import android.content.DialogInterface +import androidx.annotation.StringRes import androidx.core.os.bundleOf import androidx.fragment.app.viewModels import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.settings.DSLConfiguration import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment +import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.fragments.findListener @@ -58,23 +60,61 @@ class StoryInfoBottomSheetDialogFragment : DSLSettingsBottomSheetFragment() { ) ) - state.sections.map { (section, recipients) -> - renderSection(section, recipients) + val details = state.messageDetails!! + + if (state.isOutgoing) { + renderSection( + title = R.string.message_details_recipient_header__not_sent, + recipients = details.notSent.map { StoryInfoRecipientRow.Model(it) } + ) + + renderSection( + title = R.string.message_details_recipient_header__viewed, + recipients = details.viewed.map { StoryInfoRecipientRow.Model(it) } + ) + + renderSection( + title = R.string.message_details_recipient_header__read_by, + recipients = details.read.map { StoryInfoRecipientRow.Model(it) } + ) + + renderSection( + title = R.string.message_details_recipient_header__delivered_to, + recipients = details.delivered.map { StoryInfoRecipientRow.Model(it) } + ) + + renderSection( + title = R.string.message_details_recipient_header__sent_to, + recipients = details.sent.map { StoryInfoRecipientRow.Model(it) } + ) + + renderSection( + title = R.string.message_details_recipient_header__pending_send, + recipients = details.pending.map { StoryInfoRecipientRow.Model(it) } + ) + + renderSection( + title = R.string.message_details_recipient_header__skipped, + recipients = details.skipped.map { StoryInfoRecipientRow.Model(it) } + ) + } else { + renderSection( + title = R.string.message_details_recipient_header__sent_from, + recipients = details.sent.map { StoryInfoRecipientRow.Model(it) } + ) } } } - private fun DSLConfiguration.renderSection(sectionKey: StoryInfoState.SectionKey, recipients: List) { - sectionHeaderPref( - title = when (sectionKey) { - StoryInfoState.SectionKey.FAILED -> R.string.StoryInfoBottomSheetDialogFragment__failed - StoryInfoState.SectionKey.SENT_TO -> R.string.StoryInfoBottomSheetDialogFragment__sent_to - StoryInfoState.SectionKey.SENT_FROM -> R.string.StoryInfoBottomSheetDialogFragment__sent_from - } - ) + private fun DSLConfiguration.renderSection(@StringRes title: Int, recipients: List) { + if (recipients.isNotEmpty()) { + sectionHeaderPref( + title = DSLSettingsText.from(title) + ) - recipients.forEach { - customPref(it) + recipients.forEach { + customPref(it) + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoRecipientRow.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoRecipientRow.kt index 00950398c9..0ca8615c03 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoRecipientRow.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoRecipientRow.kt @@ -4,7 +4,7 @@ import android.view.View import android.widget.TextView import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.AvatarImageView -import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.messagedetails.RecipientDeliveryStatus import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter @@ -21,17 +21,15 @@ object StoryInfoRecipientRow { } class Model( - val recipient: Recipient, - val date: Long, - val status: Int, - val isFailed: Boolean + val recipientDeliveryStatus: RecipientDeliveryStatus ) : MappingModel { override fun areItemsTheSame(newItem: Model): Boolean { - return recipient.id == newItem.recipient.id + return recipientDeliveryStatus.recipient.id == newItem.recipientDeliveryStatus.recipient.id } override fun areContentsTheSame(newItem: Model): Boolean { - return recipient.hasSameContent(newItem.recipient) && date == newItem.date + return recipientDeliveryStatus.recipient.hasSameContent(newItem.recipientDeliveryStatus.recipient) && + recipientDeliveryStatus.timestamp == newItem.recipientDeliveryStatus.timestamp } } @@ -42,9 +40,9 @@ object StoryInfoRecipientRow { private val timestampView: TextView = itemView.findViewById(R.id.story_info_timestamp) override fun bind(model: Model) { - avatarView.setRecipient(model.recipient) - nameView.text = model.recipient.getDisplayName(context) - timestampView.text = DateUtils.getTimeString(context, Locale.getDefault(), model.date) + avatarView.setRecipient(model.recipientDeliveryStatus.recipient) + nameView.text = model.recipientDeliveryStatus.recipient.getDisplayName(context) + timestampView.text = DateUtils.getTimeString(context, Locale.getDefault(), model.recipientDeliveryStatus.timestamp) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoRepository.kt deleted file mode 100644 index 2ad567eac0..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoRepository.kt +++ /dev/null @@ -1,73 +0,0 @@ -package org.thoughtcrime.securesms.stories.viewer.info - -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers -import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.database.DatabaseObserver -import org.thoughtcrime.securesms.database.GroupReceiptDatabase -import org.thoughtcrime.securesms.database.NoSuchMessageException -import org.thoughtcrime.securesms.database.SignalDatabase -import org.thoughtcrime.securesms.database.model.MessageRecord -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies - -/** - * Gathers necessary message record and receipt data for a given story id. - */ -class StoryInfoRepository { - - companion object { - private val TAG = Log.tag(StoryInfoRepository::class.java) - } - - /** - * Retrieves the StoryInfo for a given ID and emits a new item whenever the underlying - * message record changes. - */ - fun getStoryInfo(storyId: Long): Observable { - return observeMessageRecord(storyId) - .switchMap { record -> - getReceiptInfo(storyId).map { receiptInfo -> - StoryInfo(record, receiptInfo) - }.toObservable() - } - .subscribeOn(Schedulers.io()) - } - - private fun observeMessageRecord(storyId: Long): Observable { - return Observable.create { emitter -> - fun refresh() { - try { - emitter.onNext(SignalDatabase.mms.getMessageRecord(storyId)) - } catch (e: NoSuchMessageException) { - Log.w(TAG, "The story message disappeared. Terminating emission.") - emitter.onComplete() - } - } - - val observer = DatabaseObserver.MessageObserver { - if (it.mms && it.id == storyId) { - refresh() - } - } - - ApplicationDependencies.getDatabaseObserver().registerMessageUpdateObserver(observer) - emitter.setCancellable { - ApplicationDependencies.getDatabaseObserver().unregisterObserver(observer) - } - - refresh() - } - } - - private fun getReceiptInfo(storyId: Long): Single> { - return Single.fromCallable { - SignalDatabase.groupReceipts.getGroupReceiptInfo(storyId) - } - } - - /** - * The message record and receipt info for a given story id. - */ - data class StoryInfo(val messageRecord: MessageRecord, val receiptInfo: List) -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoState.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoState.kt index 1d96a19dd8..20cadbd719 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoState.kt @@ -1,19 +1,19 @@ package org.thoughtcrime.securesms.stories.viewer.info +import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord +import org.thoughtcrime.securesms.messagedetails.MessageDetails + /** * Contains the needed information to render the story info sheet. */ data class StoryInfoState( - val sentMillis: Long = -1L, - val receivedMillis: Long = -1L, - val size: Long = -1L, - val isOutgoing: Boolean = false, - val sections: Map> = emptyMap(), - val isLoaded: Boolean = false + val messageDetails: MessageDetails? = null ) { - enum class SectionKey { - FAILED, - SENT_TO, - SENT_FROM - } + private val mediaMessage = messageDetails?.conversationMessage?.messageRecord as? MediaMmsMessageRecord + + val sentMillis: Long = mediaMessage?.dateSent ?: -1L + val receivedMillis: Long = mediaMessage?.dateReceived ?: -1L + val size: Long = mediaMessage?.slideDeck?.thumbnailSlide?.fileSize ?: 0 + val isOutgoing: Boolean = mediaMessage?.isOutgoing ?: false + val isLoaded: Boolean = mediaMessage != null } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoViewModel.kt index 1ed2e043a4..22ccc80a14 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoViewModel.kt @@ -7,17 +7,14 @@ import io.reactivex.rxjava3.core.BackpressureStrategy import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign -import org.thoughtcrime.securesms.database.model.MessageRecord -import org.thoughtcrime.securesms.database.model.MmsMessageRecord -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies -import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.database.model.MessageId +import org.thoughtcrime.securesms.messagedetails.MessageDetailsRepository import org.thoughtcrime.securesms.util.rx.RxStore /** * Gathers and stores the StoryInfoState which is used to render the story info sheet. */ -class StoryInfoViewModel(storyId: Long, repository: StoryInfoRepository = StoryInfoRepository()) : ViewModel() { +class StoryInfoViewModel(storyId: Long, repository: MessageDetailsRepository = MessageDetailsRepository()) : ViewModel() { private val store = RxStore(StoryInfoState()) private val disposables = CompositeDisposable() @@ -25,54 +22,13 @@ class StoryInfoViewModel(storyId: Long, repository: StoryInfoRepository = StoryI val state: Flowable = store.stateFlowable.observeOn(AndroidSchedulers.mainThread()) init { - disposables += store.update(repository.getStoryInfo(storyId).toFlowable(BackpressureStrategy.LATEST)) { storyInfo, storyInfoState -> + disposables += store.update(repository.getMessageDetails(MessageId(storyId, true)).toFlowable(BackpressureStrategy.LATEST)) { messageDetails, storyInfoState -> storyInfoState.copy( - isLoaded = true, - sentMillis = storyInfo.messageRecord.dateSent, - receivedMillis = storyInfo.messageRecord.dateReceived, - size = (storyInfo.messageRecord as? MmsMessageRecord)?.let { it.slideDeck.firstSlide?.fileSize } ?: -1L, - isOutgoing = storyInfo.messageRecord.isOutgoing, - sections = buildSections(storyInfo) + messageDetails = messageDetails ) } } - private fun buildSections(storyInfo: StoryInfoRepository.StoryInfo): Map> { - return if (storyInfo.messageRecord.isOutgoing) { - storyInfo.receiptInfo.map { groupReceiptInfo -> - StoryInfoRecipientRow.Model( - recipient = Recipient.resolved(groupReceiptInfo.recipientId), - date = groupReceiptInfo.timestamp, - status = groupReceiptInfo.status, - isFailed = hasFailure(storyInfo.messageRecord, groupReceiptInfo.recipientId) - ) - }.groupBy { - when { - it.isFailed -> StoryInfoState.SectionKey.FAILED - else -> StoryInfoState.SectionKey.SENT_TO - } - } - } else { - mapOf( - StoryInfoState.SectionKey.SENT_FROM to listOf( - StoryInfoRecipientRow.Model( - recipient = storyInfo.messageRecord.individualRecipient, - date = storyInfo.messageRecord.dateSent, - status = -1, - isFailed = false - ) - ) - ) - } - } - - private fun hasFailure(messageRecord: MessageRecord, recipientId: RecipientId): Boolean { - val hasNetworkFailure = messageRecord.networkFailures.any { it.getRecipientId(ApplicationDependencies.getApplication()) == recipientId } - val hasIdentityFailure = messageRecord.identityKeyMismatches.any { it.getRecipientId(ApplicationDependencies.getApplication()) == recipientId } - - return hasNetworkFailure || hasIdentityFailure - } - override fun onCleared() { disposables.clear() store.dispose() From 50ded5c92ac8c4a35977876598a006ffd1be264e Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Fri, 7 Oct 2022 09:46:02 -0400 Subject: [PATCH 56/78] Rotate SMS exporter flag. --- .../main/java/org/thoughtcrime/securesms/util/FeatureFlags.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index 554190ab1f..dbb51fb0db 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -100,7 +100,7 @@ public final class FeatureFlags { private static final String CAMERAX_MIXED_MODEL_BLOCKLIST = "android.cameraXMixedModelBlockList"; private static final String RECIPIENT_MERGE_V2 = "android.recipientMergeV2"; private static final String CDS_V2_LOAD_TEST = "android.cdsV2LoadTest"; - private static final String SMS_EXPORTER = "android.sms.exporter"; + private static final String SMS_EXPORTER = "android.sms.exporter.2"; private static final String CDS_V2_COMPAT = "android.cdsV2Compat.4"; public static final String STORIES_LOCALE = "android.stories.locale"; private static final String HIDE_CONTACTS = "android.hide.contacts"; From 04b0c010159926cb65d584f32c0b129725816f8f Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Fri, 7 Oct 2022 09:52:23 -0400 Subject: [PATCH 57/78] Catch a foreground service start exception. --- .../java/org/thoughtcrime/securesms/gcm/FcmFetchManager.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmFetchManager.kt b/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmFetchManager.kt index 3d58e4fbde..fa039b60c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmFetchManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmFetchManager.kt @@ -83,7 +83,12 @@ object FcmFetchManager { context.stopService(Intent(context, FcmFetchBackgroundService::class.java)) if (startedForeground) { - context.startService(FcmFetchForegroundService.buildStopIntent(context)) + try { + context.startService(FcmFetchForegroundService.buildStopIntent(context)) + } catch (e: IllegalStateException) { + Log.w(TAG, "Failed to stop the foreground notification!", e) + } + startedForeground = false } } From be98ff35089fb58d3f9d0c88b8b393210c741eb3 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Fri, 7 Oct 2022 11:51:05 -0400 Subject: [PATCH 58/78] Fix bottom bar color in group story selector. --- .../v2/stories/ChooseGroupStoryBottomSheet.kt | 16 ++-- .../stories_choose_group_bottom_bar.xml | 79 ++++++++++--------- 2 files changed, 50 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/ChooseGroupStoryBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/ChooseGroupStoryBottomSheet.kt index f8cbb1cc0c..e2e7c72ea3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/ChooseGroupStoryBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/ChooseGroupStoryBottomSheet.kt @@ -32,10 +32,9 @@ class ChooseGroupStoryBottomSheet : FixedRoundedCornerBottomSheetDialogFragment( const val RESULT_SET = "groups" } - private lateinit var confirmButton: View - private lateinit var selectedList: RecyclerView private lateinit var divider: View private lateinit var mediator: ContactSearchMediator + private lateinit var innerContainer: View private var animatorSet: AnimatorSet? = null @@ -49,13 +48,14 @@ class ChooseGroupStoryBottomSheet : FixedRoundedCornerBottomSheetDialogFragment( val container = view.parent.parent.parent as FrameLayout val bottomBar = LayoutInflater.from(requireContext()).inflate(R.layout.stories_choose_group_bottom_bar, container, true) - confirmButton = bottomBar.findViewById(R.id.share_confirm) - selectedList = bottomBar.findViewById(R.id.selected_list) + innerContainer = bottomBar.findViewById(R.id.inner_container) divider = bottomBar.findViewById(R.id.divider) val adapter = ShareSelectionAdapter() + val selectedList: RecyclerView = bottomBar.findViewById(R.id.selected_list) selectedList.adapter = adapter + val confirmButton: View = bottomBar.findViewById(R.id.share_confirm) confirmButton.setOnClickListener { onDone() } @@ -116,8 +116,7 @@ class ChooseGroupStoryBottomSheet : FixedRoundedCornerBottomSheetDialogFragment( animatorSet?.cancel() animatorSet = AnimatorSet().apply { playTogether( - ObjectAnimator.ofFloat(confirmButton, View.TRANSLATION_Y, 0f), - ObjectAnimator.ofFloat(selectedList, View.TRANSLATION_Y, 0f), + ObjectAnimator.ofFloat(innerContainer, View.TRANSLATION_Y, 0f), ObjectAnimator.ofFloat(divider, View.TRANSLATION_Y, 0f) ) start() @@ -125,13 +124,12 @@ class ChooseGroupStoryBottomSheet : FixedRoundedCornerBottomSheetDialogFragment( } private fun animateOutBottomBar() { - val translationY = DimensionUnit.SP.toPixels(64f) + val translationY = DimensionUnit.SP.toPixels(68f) animatorSet?.cancel() animatorSet = AnimatorSet().apply { playTogether( - ObjectAnimator.ofFloat(confirmButton, View.TRANSLATION_Y, translationY), - ObjectAnimator.ofFloat(selectedList, View.TRANSLATION_Y, translationY), + ObjectAnimator.ofFloat(innerContainer, View.TRANSLATION_Y, translationY), ObjectAnimator.ofFloat(divider, View.TRANSLATION_Y, translationY) ) start() diff --git a/app/src/main/res/layout/stories_choose_group_bottom_bar.xml b/app/src/main/res/layout/stories_choose_group_bottom_bar.xml index 9c1e40e3dc..d0fb6cf773 100644 --- a/app/src/main/res/layout/stories_choose_group_bottom_bar.xml +++ b/app/src/main/res/layout/stories_choose_group_bottom_bar.xml @@ -1,53 +1,60 @@ - + android:layout_gravity="bottom" + android:orientation="vertical"> + + + - + - + - \ No newline at end of file + From 3de75f48cf6b5eeb7f591b9f5a405029a789e5e1 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Fri, 7 Oct 2022 14:20:10 -0300 Subject: [PATCH 59/78] Add padding to bottom of selection recycler. --- .../settings/select/BaseStoryRecipientSelectionFragment.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/select/BaseStoryRecipientSelectionFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/select/BaseStoryRecipientSelectionFragment.kt index eb121c8944..560173a029 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/select/BaseStoryRecipientSelectionFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/select/BaseStoryRecipientSelectionFragment.kt @@ -9,6 +9,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import com.google.android.material.button.MaterialButton +import org.signal.core.util.dp import org.thoughtcrime.securesms.ContactSelectionListFragment import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.contacts.ContactsCursorLoader @@ -148,7 +149,9 @@ abstract class BaseStoryRecipientSelectionFragment : Fragment(R.layout.stories_b currentSelection = emptyList(), displaySelectionCount = false, displayChips = true, - checkboxResource = checkboxResource + checkboxResource = checkboxResource, + recyclerPadBottom = 76.dp, + recyclerChildClipping = false ) contactSelectionListFragment.arguments = arguments.toArgumentBundle() From 3dd31432c814f529c87dfd0f2fc5417e4164f926 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Fri, 7 Oct 2022 14:12:00 -0300 Subject: [PATCH 60/78] Allow getMessageDestination to handle Story messages. --- .../securesms/messages/MessageContentProcessor.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java index 08f9f9e0ea..9fe8611310 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java @@ -3011,8 +3011,13 @@ private Recipient getSyncMessageDestination(@NonNull SentTranscriptMessage messa } private Recipient getMessageDestination(@NonNull SignalServiceContent content) throws BadGroupIdException { - SignalServiceDataMessage message = content.getDataMessage().orElse(null); - return getGroupRecipient(message != null ? message.getGroupContext() : Optional.empty()).orElseGet(() -> Recipient.externalPush(content.getSender())); + if (content.getStoryMessage().isPresent()) { + SignalServiceStoryMessage message = content.getStoryMessage().get(); + return getGroupRecipient(message.getGroupContext()).orElseGet(() -> Recipient.externalPush(content.getSender())); + } else { + SignalServiceDataMessage message = content.getDataMessage().orElse(null); + return getGroupRecipient(message != null ? message.getGroupContext() : Optional.empty()).orElseGet(() -> Recipient.externalPush(content.getSender())); + } } private Optional getGroupRecipient(Optional message) { From 5c77c33dffd23b47066ad23d095979cdaa9bca0f Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Fri, 7 Oct 2022 14:35:05 -0300 Subject: [PATCH 61/78] Fix flow colors. --- .../thoughtcrime/securesms/color/ViewColorSet.kt | 5 +++++ .../forward/MultiselectForwardActivity.kt | 16 +--------------- .../forward/MultiselectForwardFragmentArgs.kt | 4 ++-- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/color/ViewColorSet.kt b/app/src/main/java/org/thoughtcrime/securesms/color/ViewColorSet.kt index 654d62d518..60cfe75996 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/color/ViewColorSet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/color/ViewColorSet.kt @@ -19,6 +19,11 @@ data class ViewColorSet( val background: ViewColor ) : Parcelable { companion object { + val PRIMARY = ViewColorSet( + foreground = ViewColor.ColorResource(R.color.signal_colorOnPrimary), + background = ViewColor.ColorResource(R.color.signal_colorPrimary) + ) + fun forCustomColor(@ColorInt customColor: Int): ViewColorSet { return ViewColorSet( foreground = ViewColor.ColorResource(R.color.signal_colorOnCustom), diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardActivity.kt index 8e0fd39ded..16e800524d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardActivity.kt @@ -35,21 +35,7 @@ open class MultiselectForwardActivity : FragmentWrapperActivity(), MultiselectFo } override fun getFragment(): Fragment { - return MultiselectForwardFragment.create( - args.let { - if (it.sendButtonColors == null) { - args.withSendButtonTint(ContextCompat.getColor(this, R.color.signal_colorPrimary)) - args.copy( - sendButtonColors = ViewColorSet( - foreground = ViewColorSet.ViewColor.ColorResource(R.color.signal_colorOnPrimary), - background = ViewColorSet.ViewColor.ColorResource(R.color.signal_colorPrimary) - ) - ) - } else { - args - } - } - ) + return MultiselectForwardFragment.create(args) } override fun onFinishForwardAction() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragmentArgs.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragmentArgs.kt index 940ee8fdd7..7f967071ba 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragmentArgs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragmentArgs.kt @@ -44,7 +44,7 @@ data class MultiselectForwardFragmentArgs @JvmOverloads constructor( val forceDisableAddMessage: Boolean = false, val forceSelectionOnly: Boolean = false, val selectSingleRecipient: Boolean = false, - val sendButtonColors: ViewColorSet? = null, + val sendButtonColors: ViewColorSet = ViewColorSet.PRIMARY, val storySendRequirements: Stories.MediaTransform.SendRequirements = Stories.MediaTransform.SendRequirements.CAN_NOT_SEND, val isSearchEnabled: Boolean = true ) : Parcelable { @@ -74,7 +74,7 @@ data class MultiselectForwardFragmentArgs @JvmOverloads constructor( isMmsSupported, listOf(multiShareArgs), storySendRequirements = Stories.MediaTransform.SendRequirements.CAN_NOT_SEND, - sendButtonColors = sendButtonColors + sendButtonColors = sendButtonColors ?: ViewColorSet.PRIMARY ) ) } From 9aa7543f2f51d7152401807558ad20bbc1d06524 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Fri, 7 Oct 2022 14:42:49 -0300 Subject: [PATCH 62/78] Do not display stories as valid selections when sending view-once media. --- .../mutiselect/forward/MultiselectForwardActivity.kt | 1 - .../mutiselect/forward/MultiselectForwardFragment.kt | 2 +- .../mutiselect/forward/MultiselectForwardFragmentArgs.kt | 3 ++- .../securesms/mediasend/v2/review/MediaReviewFragment.kt | 5 ++++- .../org/thoughtcrime/securesms/sharing/MultiShareArgs.java | 4 ++++ 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardActivity.kt index 16e800524d..4d7b72a2bb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardActivity.kt @@ -10,7 +10,6 @@ import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.color.ViewColorSet import org.thoughtcrime.securesms.components.FragmentWrapperActivity import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment.Companion.RESULT_SELECTION diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt index 77ec1289b3..0ce2139e55 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt @@ -459,7 +459,7 @@ class MultiselectForwardFragment : } private fun isSelectedMediaValidForStories(): Boolean { - return args.multiShareArgs.all { it.isValidForStories } + return !args.isViewOnce && args.multiShareArgs.all { it.isValidForStories } } private fun isSelectedMediaValidForNonStories(): Boolean { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragmentArgs.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragmentArgs.kt index 7f967071ba..ec423cfe87 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragmentArgs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragmentArgs.kt @@ -46,7 +46,8 @@ data class MultiselectForwardFragmentArgs @JvmOverloads constructor( val selectSingleRecipient: Boolean = false, val sendButtonColors: ViewColorSet = ViewColorSet.PRIMARY, val storySendRequirements: Stories.MediaTransform.SendRequirements = Stories.MediaTransform.SendRequirements.CAN_NOT_SEND, - val isSearchEnabled: Boolean = true + val isSearchEnabled: Boolean = true, + val isViewOnce: Boolean = false ) : Parcelable { fun withSendButtonTint(@ColorInt sendButtonTint: Int) = copy(sendButtonColors = ViewColorSet.forCustomColor(sendButtonTint)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt index b7ec225a2b..331c8ccfed 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt @@ -160,12 +160,15 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment) { } sendButton.setOnClickListener { + val viewOnce: Boolean = sharedViewModel.state.value?.viewOnceToggleState == MediaSelectionState.ViewOnceToggleState.ONCE + if (sharedViewModel.isContactSelectionRequired) { val args = MultiselectForwardFragmentArgs( false, title = R.string.MediaReviewFragment__send_to, storySendRequirements = sharedViewModel.getStorySendRequirements(), - isSearchEnabled = !sharedViewModel.isStory() + isSearchEnabled = !sharedViewModel.isStory(), + isViewOnce = viewOnce ) if (sharedViewModel.isStory()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareArgs.java b/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareArgs.java index 924484bd5b..923dbc5191 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareArgs.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareArgs.java @@ -149,6 +149,10 @@ public long getExpiresAt() { } public boolean isValidForStories() { + if (isViewOnce()) { + return false; + } + return isTextStory || (!media.isEmpty() && media.stream().allMatch(m -> MediaUtil.isStorySupportedType(m.getMimeType()))) || MediaUtil.isStorySupportedType(dataType) || From c239ba1e35f79ef4630b74d5e15526e1224d0519 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Fri, 7 Oct 2022 15:29:43 -0300 Subject: [PATCH 63/78] Fix crash after replying to a group story. --- .../securesms/conversation/ConversationDataSource.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java index f2d120b2cb..42dc29c5e3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java @@ -159,6 +159,12 @@ private int getSizeInternal() { MessageDatabase database = messageId.isMms() ? SignalDatabase.mms() : SignalDatabase.sms(); MessageRecord record = database.getMessageRecordOrNull(messageId.getId()); + if (record instanceof MediaMmsMessageRecord && + ((MediaMmsMessageRecord) record).getParentStoryId() != null && + ((MediaMmsMessageRecord) record).getParentStoryId().isGroupReply()) { + return null; + } + stopwatch.split("message"); try { From 842626e96c2c64b6977e983e087d32565fd68c76 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Fri, 7 Oct 2022 09:40:10 -0300 Subject: [PATCH 64/78] Add viewer count and list to 'All Signal Connections'. --- .../securesms/components/FromTextView.java | 2 + .../paged/ContactSearchConfiguration.kt | 3 +- .../contacts/paged/ContactSearchData.kt | 6 +- .../contacts/paged/ContactSearchItems.kt | 14 +++- .../paged/ContactSearchPagedDataSource.kt | 17 ++++- .../ContactSearchPagedDataSourceRepository.kt | 4 ++ .../securesms/database/RecipientDatabase.kt | 38 ++++++++++ .../ViewAllSignalConnectionsFragment.kt | 60 ++++++++++++++++ .../my/AllSignalConnectionsRowItem.kt | 70 +++++++++++++++++++ .../settings/my/MyStorySettingsFragment.kt | 22 +++--- .../settings/my/MyStorySettingsRepository.kt | 14 +++- .../settings/my/MyStorySettingsState.kt | 3 +- .../settings/my/MyStorySettingsViewModel.kt | 2 + ...toryMembershipBottomSheetDialogFragment.kt | 13 ++++ .../ChooseInitialMyStoryMembershipState.kt | 6 +- .../all_signal_connections_row_item.xml | 63 +++++++++++++++++ ...e_initial_my_story_membership_fragment.xml | 56 ++++++++++++--- .../view_all_signal_connections_fragment.xml | 30 ++++++++ app/src/main/res/values/strings.xml | 7 ++ app/src/main/res/values/styles.xml | 5 ++ 20 files changed, 408 insertions(+), 27 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/stories/settings/connections/ViewAllSignalConnectionsFragment.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/AllSignalConnectionsRowItem.kt create mode 100644 app/src/main/res/layout/all_signal_connections_row_item.xml create mode 100644 app/src/main/res/layout/view_all_signal_connections_fragment.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/FromTextView.java b/app/src/main/java/org/thoughtcrime/securesms/components/FromTextView.java index 42a6f2c930..f16c9a068c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/FromTextView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/FromTextView.java @@ -11,6 +11,7 @@ import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; +import org.signal.core.util.BreakIteratorCompat; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.emoji.SimpleEmojiTextView; @@ -19,6 +20,7 @@ import org.thoughtcrime.securesms.util.SpanUtil; import org.thoughtcrime.securesms.util.ViewUtil; +import java.util.Iterator; import java.util.Objects; public class FromTextView extends SimpleEmojiTextView { diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchConfiguration.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchConfiguration.kt index 1061b55e39..962b6f6d44 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchConfiguration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchConfiguration.kt @@ -52,7 +52,8 @@ class ContactSearchConfiguration private constructor( val includeSelf: Boolean, val transportType: TransportType, override val includeHeader: Boolean, - override val expandConfig: ExpandConfig? = null + override val expandConfig: ExpandConfig? = null, + val includeLetterHeaders: Boolean = false ) : Section(SectionKey.INDIVIDUALS) /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchData.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchData.kt index 6ec9937934..f7ba55ba20 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchData.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchData.kt @@ -23,7 +23,11 @@ sealed class ContactSearchData(val contactSearchKey: ContactSearchKey) { /** * A row displaying a known recipient. */ - data class KnownRecipient(val recipient: Recipient, val shortSummary: Boolean = false) : ContactSearchData(ContactSearchKey.RecipientSearchKey.KnownRecipient(recipient.id)) + data class KnownRecipient( + val recipient: Recipient, + val shortSummary: Boolean = false, + val headerLetter: String? = null + ) : ContactSearchData(ContactSearchKey.RecipientSearchKey.KnownRecipient(recipient.id)) /** * A row containing a title for a given section diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchItems.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchItems.kt index bc640c45ca..c575d72d82 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchItems.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchItems.kt @@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.components.AvatarImageView import org.thoughtcrime.securesms.components.FromTextView import org.thoughtcrime.securesms.components.menu.ActionItem import org.thoughtcrime.securesms.components.menu.SignalContextMenu +import org.thoughtcrime.securesms.contacts.LetterHeaderDecoration import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.recipients.Recipient @@ -226,7 +227,10 @@ object ContactSearchItems { } } - private class KnownRecipientViewHolder(itemView: View, displayCheckBox: Boolean, onClick: RecipientClickListener) : BaseRecipientViewHolder(itemView, displayCheckBox, onClick) { + private class KnownRecipientViewHolder(itemView: View, displayCheckBox: Boolean, onClick: RecipientClickListener) : BaseRecipientViewHolder(itemView, displayCheckBox, onClick), LetterHeaderDecoration.LetterHeaderItem { + + private var headerLetter: String? = null + override fun isSelected(model: RecipientModel): Boolean = model.isSelected override fun getData(model: RecipientModel): ContactSearchData.KnownRecipient = model.knownRecipient override fun getRecipient(model: RecipientModel): Recipient = model.knownRecipient.recipient @@ -235,10 +239,16 @@ object ContactSearchItems { if (model.shortSummary && recipient.isGroup) { val count = recipient.participantIds.size - number.setText(context.resources.getQuantityString(R.plurals.ContactSearchItems__group_d_members, count, count)) + number.text = context.resources.getQuantityString(R.plurals.ContactSearchItems__group_d_members, count, count) } else { super.bindNumberField(model) } + + headerLetter = model.knownRecipient.headerLetter + } + + override fun getHeaderLetter(): String? { + return headerLetter } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSource.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSource.kt index fc2e9071d4..580cc12d02 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSource.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSource.kt @@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.keyvalue.StorySend import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId import java.util.concurrent.TimeUnit /** @@ -129,6 +130,13 @@ class ContactSearchPagedDataSource( } } + private fun getNonGroupHeaderLetterMap(section: ContactSearchConfiguration.Section.Individuals, query: String?): Map { + return when (section.transportType) { + ContactSearchConfiguration.TransportType.PUSH -> contactSearchPagedDataSourceRepository.querySignalContactLetterHeaders(query, section.includeSelf) + else -> error("This has only been implemented for push recipients.") + } + } + private fun getStoriesSearchIterator(query: String?): ContactSearchIterator { return CursorSearchIterator(contactSearchPagedDataSourceRepository.getStories(query)) } @@ -193,6 +201,12 @@ class ContactSearchPagedDataSource( } private fun getNonGroupContactsData(section: ContactSearchConfiguration.Section.Individuals, query: String?, startIndex: Int, endIndex: Int): List { + val headerMap: Map = if (section.includeLetterHeaders) { + getNonGroupHeaderLetterMap(section, query) + } else { + emptyMap() + } + return getNonGroupSearchIterator(section, query).use { records -> readContactData( records = records, @@ -201,7 +215,8 @@ class ContactSearchPagedDataSource( startIndex = startIndex, endIndex = endIndex, recordMapper = { - ContactSearchData.KnownRecipient(contactSearchPagedDataSourceRepository.getRecipientFromRecipientCursor(it)) + val recipient = contactSearchPagedDataSourceRepository.getRecipientFromRecipientCursor(it) + ContactSearchData.KnownRecipient(recipient, headerLetter = headerMap[recipient.id]) } ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSourceRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSourceRepository.kt index e5552ffa54..2f9439d6c7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSourceRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSourceRepository.kt @@ -36,6 +36,10 @@ open class ContactSearchPagedDataSourceRepository( return contactRepository.querySignalContacts(query ?: "", includeSelf) } + open fun querySignalContactLetterHeaders(query: String?, includeSelf: Boolean): Map { + return SignalDatabase.recipients.querySignalContactLetterHeaders(query ?: "", includeSelf) + } + open fun queryNonSignalContacts(query: String?): Cursor? { return contactRepository.queryNonSignalContacts(query ?: "") } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt index 76d314f434..90d4eca404 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt @@ -3112,6 +3112,44 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy) } + fun querySignalContactLetterHeaders(inputQuery: String, includeSelf: Boolean): Map { + val searchSelection = ContactSearchSelection.Builder() + .withRegistered(true) + .withGroups(false) + .excludeId(if (includeSelf) null else Recipient.self().id) + .withSearchQuery(inputQuery) + .build() + + return readableDatabase.query( + """ + SELECT + _id, + UPPER(SUBSTR($SORT_NAME, 0, 2)) AS letter_header + FROM ( + SELECT ${SEARCH_PROJECTION.joinToString(", ")} + FROM recipient + WHERE ${searchSelection.where} + ORDER BY $SORT_NAME, $SYSTEM_JOINED_NAME, $SEARCH_PROFILE_NAME, $PHONE + ) + GROUP BY letter_header + """.trimIndent(), + searchSelection.args + ).use { cursor -> + if (cursor.count == 0) { + emptyMap() + } else { + val resultsMap = mutableMapOf() + while (cursor.moveToNext()) { + cursor.requireString("letter_header")?.let { + resultsMap[RecipientId.from(cursor.requireLong(ID))] = it + } + } + + resultsMap + } + } + } + fun getNonSignalContacts(): Cursor? { val searchSelection = ContactSearchSelection.Builder().withNonRegistered(true) .withGroups(false) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/connections/ViewAllSignalConnectionsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/connections/ViewAllSignalConnectionsFragment.kt new file mode 100644 index 0000000000..e8979c68a5 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/connections/ViewAllSignalConnectionsFragment.kt @@ -0,0 +1,60 @@ +package org.thoughtcrime.securesms.stories.settings.connections + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.ViewBinderDelegate +import org.thoughtcrime.securesms.components.WrapperDialogFragment +import org.thoughtcrime.securesms.contacts.LetterHeaderDecoration +import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration +import org.thoughtcrime.securesms.contacts.paged.ContactSearchMediator +import org.thoughtcrime.securesms.databinding.ViewAllSignalConnectionsFragmentBinding +import org.thoughtcrime.securesms.groups.SelectionLimits + +class ViewAllSignalConnectionsFragment : Fragment(R.layout.view_all_signal_connections_fragment) { + + private val binding by ViewBinderDelegate(ViewAllSignalConnectionsFragmentBinding::bind) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + binding.recycler.addItemDecoration(LetterHeaderDecoration(requireContext()) { false }) + binding.toolbar.setNavigationOnClickListener { + requireActivity().onBackPressedDispatcher.onBackPressed() + } + + ContactSearchMediator( + fragment = this, + recyclerView = binding.recycler, + selectionLimits = SelectionLimits(0, 0), + displayCheckBox = false, + mapStateToConfiguration = { getConfiguration() }, + performSafetyNumberChecks = false + ) + } + + private fun getConfiguration(): ContactSearchConfiguration { + return ContactSearchConfiguration.build { + addSection( + ContactSearchConfiguration.Section.Individuals( + includeHeader = false, + includeSelf = false, + includeLetterHeaders = true, + transportType = ContactSearchConfiguration.TransportType.PUSH + ) + ) + } + } + + class Dialog : WrapperDialogFragment() { + override fun getWrappedFragment(): Fragment { + return ViewAllSignalConnectionsFragment() + } + + companion object { + fun show(fragmentManager: FragmentManager) { + Dialog().show(fragmentManager, null) + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/AllSignalConnectionsRowItem.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/AllSignalConnectionsRowItem.kt new file mode 100644 index 0000000000..3e89bcb97a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/AllSignalConnectionsRowItem.kt @@ -0,0 +1,70 @@ +package org.thoughtcrime.securesms.stories.settings.my + +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.databinding.AllSignalConnectionsRowItemBinding +import org.thoughtcrime.securesms.util.adapter.mapping.BindingFactory +import org.thoughtcrime.securesms.util.adapter.mapping.BindingViewHolder +import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter +import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel +import org.thoughtcrime.securesms.util.visible + +/** + * AllSignalConnections privacy setting row item with "View" support + */ +object AllSignalConnectionsRowItem { + + private const val IS_CHECKED = 0 + private const val IS_COUNT = 1 + + fun register(mappingAdapter: MappingAdapter) { + mappingAdapter.registerFactory(Model::class.java, BindingFactory(::ViewHolder, AllSignalConnectionsRowItemBinding::inflate)) + } + + class Model( + val isChecked: Boolean, + val count: Int, + val onRowClicked: () -> Unit, + val onViewClicked: () -> Unit + ) : MappingModel { + + override fun areItemsTheSame(newItem: Model): Boolean = true + + override fun areContentsTheSame(newItem: Model): Boolean = isChecked == newItem.isChecked && count == newItem.count + + override fun getChangePayload(newItem: Model): Any? { + val isCheckedDifferent = isChecked != newItem.isChecked + val isCountDifferent = count != newItem.count + + return when { + isCheckedDifferent && !isCountDifferent -> IS_CHECKED + !isCheckedDifferent && isCountDifferent -> IS_COUNT + else -> null + } + } + } + + private class ViewHolder(binding: AllSignalConnectionsRowItemBinding) : BindingViewHolder(binding) { + override fun bind(model: Model) { + binding.root.setOnClickListener { model.onRowClicked() } + binding.view.setOnClickListener { model.onViewClicked() } + + when { + payload.contains(IS_COUNT) -> presentCount(model.count) + payload.contains(IS_CHECKED) -> presentSelected(model.isChecked) + else -> { + presentCount(model.count) + presentSelected(model.isChecked) + } + } + } + + private fun presentCount(count: Int) { + binding.count.visible = count > 0 + binding.count.text = context.resources.getQuantityString(R.plurals.MyStorySettingsFragment__viewers, count, count) + } + + private fun presentSelected(isChecked: Boolean) { + binding.radio.isChecked = isChecked + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/MyStorySettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/MyStorySettingsFragment.kt index 984b878b4b..d741d1e749 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/MyStorySettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/MyStorySettingsFragment.kt @@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode +import org.thoughtcrime.securesms.stories.settings.connections.ViewAllSignalConnectionsFragment import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter import org.thoughtcrime.securesms.util.navigation.safeNavigate @@ -38,6 +39,7 @@ class MyStorySettingsFragment : DSLSettingsFragment( } override fun bindAdapter(adapter: MappingAdapter) { + AllSignalConnectionsRowItem.register(adapter) viewModel.state.observe(viewLifecycleOwner) { state -> adapter.submitList(getConfiguration(state).toMappingModelList()) } @@ -47,14 +49,18 @@ class MyStorySettingsFragment : DSLSettingsFragment( return configure { sectionHeaderPref(R.string.MyStorySettingsFragment__who_can_view_this_story) - radioPref( - title = DSLSettingsText.from(R.string.MyStorySettingsFragment__all_signal_connections), - summary = DSLSettingsText.from(R.string.MyStorySettingsFragment__share_with_all_connections), - isChecked = state.myStoryPrivacyState.privacyMode == DistributionListPrivacyMode.ALL, - onClick = { - lifecycleDisposable += viewModel.setMyStoryPrivacyMode(DistributionListPrivacyMode.ALL) - .subscribe() - } + customPref( + AllSignalConnectionsRowItem.Model( + isChecked = state.myStoryPrivacyState.privacyMode == DistributionListPrivacyMode.ALL, + count = state.allSignalConnectionsCount, + onRowClicked = { + lifecycleDisposable += viewModel.setMyStoryPrivacyMode(DistributionListPrivacyMode.ALL) + .subscribe() + }, + onViewClicked = { + ViewAllSignalConnectionsFragment.Dialog.show(parentFragmentManager) + } + ) ) val exceptText = if (state.myStoryPrivacyState.privacyMode == DistributionListPrivacyMode.ALL_EXCEPT) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/MyStorySettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/MyStorySettingsRepository.kt index 9cc63d3f0e..714f79496a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/MyStorySettingsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/MyStorySettingsRepository.kt @@ -22,11 +22,15 @@ class MyStorySettingsRepository { } fun observeChooseInitialPrivacy(): Observable { - return Single.fromCallable { SignalDatabase.distributionLists.getRecipientId(DistributionListId.MY_STORY)!! } + return Single + .fromCallable { SignalDatabase.distributionLists.getRecipientId(DistributionListId.MY_STORY)!! } .subscribeOn(Schedulers.io()) .flatMapObservable { recipientId -> - Recipient.observable(recipientId) + val allSignalConnectionsCount = getAllSignalConnectionsCount().toObservable() + val stateWithoutCount = Recipient.observable(recipientId) .flatMap { Observable.just(ChooseInitialMyStoryMembershipState(recipientId = recipientId, privacyState = getStoryPrivacyState())) } + + Observable.combineLatest(allSignalConnectionsCount, stateWithoutCount) { count, state -> state.copy(allSignalConnectionsCount = count) } } } @@ -50,6 +54,12 @@ class MyStorySettingsRepository { }.subscribeOn(Schedulers.io()) } + fun getAllSignalConnectionsCount(): Single { + return Single.fromCallable { + SignalDatabase.recipients.getSignalContactsCount(false) + }.subscribeOn(Schedulers.io()) + } + @WorkerThread private fun getStoryPrivacyState(): MyStoryPrivacyState { val privacyData: DistributionListPrivacyData = SignalDatabase.distributionLists.getPrivacyData(DistributionListId.MY_STORY) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/MyStorySettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/MyStorySettingsState.kt index 1640e4212c..ef63a19cc7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/MyStorySettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/MyStorySettingsState.kt @@ -2,5 +2,6 @@ package org.thoughtcrime.securesms.stories.settings.my data class MyStorySettingsState( val myStoryPrivacyState: MyStoryPrivacyState = MyStoryPrivacyState(), - val areRepliesAndReactionsEnabled: Boolean = false + val areRepliesAndReactionsEnabled: Boolean = false, + val allSignalConnectionsCount: Int = 0 ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/MyStorySettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/MyStorySettingsViewModel.kt index cb6060d269..0cbf79d4ae 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/MyStorySettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/MyStorySettingsViewModel.kt @@ -25,6 +25,8 @@ class MyStorySettingsViewModel @JvmOverloads constructor(private val repository: .subscribe { myStoryPrivacyState -> store.update { it.copy(myStoryPrivacyState = myStoryPrivacyState) } } disposables += repository.getRepliesAndReactionsEnabled() .subscribe { repliesAndReactionsEnabled -> store.update { it.copy(areRepliesAndReactionsEnabled = repliesAndReactionsEnabled) } } + disposables += repository.getAllSignalConnectionsCount() + .subscribe { allSignalConnectionsCount -> store.update { it.copy(allSignalConnectionsCount = allSignalConnectionsCount) } } } fun setRepliesAndReactionsEnabled(repliesAndReactionsEnabled: Boolean) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipBottomSheetDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipBottomSheetDialogFragment.kt index d61caef1ad..a0163fdd97 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipBottomSheetDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipBottomSheetDialogFragment.kt @@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialog import org.thoughtcrime.securesms.components.WrapperDialogFragment import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.stories.settings.connections.ViewAllSignalConnectionsFragment import org.thoughtcrime.securesms.stories.settings.select.BaseStoryRecipientSelectionFragment import org.thoughtcrime.securesms.util.BottomSheetUtil import org.thoughtcrime.securesms.util.LifecycleDisposable @@ -42,9 +43,12 @@ class ChooseInitialMyStoryMembershipBottomSheetDialogFragment : private lateinit var allExceptRadio: MaterialRadioButton private lateinit var onlyWitRadio: MaterialRadioButton + private lateinit var allCount: TextView private lateinit var allExceptCount: TextView private lateinit var onlyWithCount: TextView + private lateinit var allView: View + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.choose_initial_my_story_membership_fragment, container, false) } @@ -58,9 +62,15 @@ class ChooseInitialMyStoryMembershipBottomSheetDialogFragment : allExceptRadio = view.findViewById(R.id.choose_initial_my_story_all_signal_connnections_except_radio) onlyWitRadio = view.findViewById(R.id.choose_initial_my_story_only_share_with_radio) + allCount = view.findViewById(R.id.choose_initial_my_story_all_signal_connnections_count) allExceptCount = view.findViewById(R.id.choose_initial_my_story_all_signal_connnections_except_count) onlyWithCount = view.findViewById(R.id.choose_initial_my_story_only_share_with_count) + allView = view.findViewById(R.id.choose_initial_my_story_all_signal_connnections_view) + allView.setOnClickListener { + ViewAllSignalConnectionsFragment.Dialog.show(parentFragmentManager) + } + val save = view.findViewById(R.id.choose_initial_my_story_save).apply { isEnabled = false } @@ -76,6 +86,9 @@ class ChooseInitialMyStoryMembershipBottomSheetDialogFragment : allExceptCount.visible = allExceptRadio.isChecked onlyWithCount.visible = onlyWitRadio.isChecked + allCount.visible = state.allSignalConnectionsCount > 0 + allCount.text = resources.getQuantityString(R.plurals.MyStorySettingsFragment__viewers, state.allSignalConnectionsCount, state.allSignalConnectionsCount) + when (state.privacyState.privacyMode) { DistributionListPrivacyMode.ALL_EXCEPT -> allExceptCount.text = resources.getQuantityString(R.plurals.MyStorySettingsFragment__d_people_excluded, state.privacyState.connectionCount, state.privacyState.connectionCount) DistributionListPrivacyMode.ONLY_WITH -> onlyWithCount.text = resources.getQuantityString(R.plurals.MyStorySettingsFragment__d_people, state.privacyState.connectionCount, state.privacyState.connectionCount) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipState.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipState.kt index d284a8a103..14ae833224 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipState.kt @@ -3,4 +3,8 @@ package org.thoughtcrime.securesms.stories.settings.privacy import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.stories.settings.my.MyStoryPrivacyState -data class ChooseInitialMyStoryMembershipState(val recipientId: RecipientId? = null, val privacyState: MyStoryPrivacyState = MyStoryPrivacyState()) +data class ChooseInitialMyStoryMembershipState( + val recipientId: RecipientId? = null, + val privacyState: MyStoryPrivacyState = MyStoryPrivacyState(), + val allSignalConnectionsCount: Int = 0 +) diff --git a/app/src/main/res/layout/all_signal_connections_row_item.xml b/app/src/main/res/layout/all_signal_connections_row_item.xml new file mode 100644 index 0000000000..dbb652ff9b --- /dev/null +++ b/app/src/main/res/layout/all_signal_connections_row_item.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/choose_initial_my_story_membership_fragment.xml b/app/src/main/res/layout/choose_initial_my_story_membership_fragment.xml index 6885439424..ce2b826e01 100644 --- a/app/src/main/res/layout/choose_initial_my_story_membership_fragment.xml +++ b/app/src/main/res/layout/choose_initial_my_story_membership_fragment.xml @@ -1,10 +1,10 @@ + android:layout_height="wrap_content" + tools:viewBindingIgnore="true"> - + android:paddingStart="@dimen/dsl_settings_gutter"> + android:clickable="false" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + android:text="@string/ChooseInitialMyStoryMembershipFragment__all_signal_connections" + app:layout_constrainedWidth="true" + app:layout_constraintBottom_toTopOf="@+id/choose_initial_my_story_all_signal_connnections_count" + app:layout_constraintEnd_toStartOf="@id/choose_initial_my_story_all_signal_connnections_view" + app:layout_constraintStart_toEndOf="@id/choose_initial_my_story_all_signal_connnections_radio" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_chainStyle="packed" /> - + + + + + + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_chainStyle="packed" /> + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_chainStyle="packed" /> + + + + + + + \ 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 94d567e9a9..cde8c9f325 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4849,6 +4849,13 @@ Delete My Story + + + %1$d viewer + %1$d viewers + + + View Who can view this story diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index c51ef60fdb..85b7d58ffb 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -17,6 +17,11 @@ + +