mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-14 22:13:19 +01:00
Compare commits
203 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
daf87915d6 | ||
|
|
06996540cd | ||
|
|
58ad3c746a | ||
|
|
a7ebe41570 | ||
|
|
b6cc702107 | ||
|
|
9163c0ca4d | ||
|
|
18290c1301 | ||
|
|
347abe14ae | ||
|
|
eba55755ff | ||
|
|
7043558657 | ||
|
|
3aefd3bdc6 | ||
|
|
d6eb675fd0 | ||
|
|
ae90b2ecd9 | ||
|
|
9d593bcaff | ||
|
|
62ed823e42 | ||
|
|
a53479e50d | ||
|
|
91140c41fd | ||
|
|
68f567b0b7 | ||
|
|
501e169210 | ||
|
|
09b818b048 | ||
|
|
7b3897cac6 | ||
|
|
64239962fc | ||
|
|
dac3a332d7 | ||
|
|
83bbcd0618 | ||
|
|
c7c0374c11 | ||
|
|
847f3bf08c | ||
|
|
d02c610237 | ||
|
|
8007045ca8 | ||
|
|
901b4b469d | ||
|
|
fa50696815 | ||
|
|
be035456f7 | ||
|
|
252a4afa79 | ||
|
|
f5f56536bc | ||
|
|
9e89d688f1 | ||
|
|
2bb94089f7 | ||
|
|
3fc386d4a3 | ||
|
|
3779dfd290 | ||
|
|
a5f766a333 | ||
|
|
9f40bfc645 | ||
|
|
919f03522a | ||
|
|
8aa6d0bbca | ||
|
|
4304ae2a96 | ||
|
|
b4a9189068 | ||
|
|
ec6448bd1b | ||
|
|
8c5811581e | ||
|
|
4b4d3d33b1 | ||
|
|
dd6c39f7eb | ||
|
|
b246e62504 | ||
|
|
ba08399d35 | ||
|
|
3f1bb7eac7 | ||
|
|
a2a10fb0c1 | ||
|
|
e45eabc714 | ||
|
|
138dae0484 | ||
|
|
893725e304 | ||
|
|
2cfe321274 | ||
|
|
050dcb3eb1 | ||
|
|
6ce01c6b0e | ||
|
|
d2f44fee87 | ||
|
|
1228da8665 | ||
|
|
479632d6a8 | ||
|
|
619d2997f6 | ||
|
|
c5e795b176 | ||
|
|
8b7b184224 | ||
|
|
48d26beb77 | ||
|
|
3d1895500c | ||
|
|
e442c27555 | ||
|
|
c3d61bece1 | ||
|
|
49853b2cca | ||
|
|
cd838c4bee | ||
|
|
2e50699a2d | ||
|
|
fe97c969ae | ||
|
|
c70a8d48a8 | ||
|
|
322ea97377 | ||
|
|
e3a402394f | ||
|
|
16b4b3b6b7 | ||
|
|
cd98ccbf00 | ||
|
|
eecb18b436 | ||
|
|
d13a803dcd | ||
|
|
bd03f21cdf | ||
|
|
b46d891183 | ||
|
|
54191433e0 | ||
|
|
462fcdce16 | ||
|
|
f68bb2dc88 | ||
|
|
fe70637140 | ||
|
|
1028d293a0 | ||
|
|
74c6e76808 | ||
|
|
8e880fe117 | ||
|
|
6525662071 | ||
|
|
94d07f7012 | ||
|
|
e3297ab593 | ||
|
|
3ff7f89ef6 | ||
|
|
ac1165c8fd | ||
|
|
69153cf339 | ||
|
|
852541c361 | ||
|
|
399a613c25 | ||
|
|
003c1082a9 | ||
|
|
885588db86 | ||
|
|
90a356b29d | ||
|
|
597623d23a | ||
|
|
2028afc941 | ||
|
|
915580ddd3 | ||
|
|
9432cca14a | ||
|
|
4e07ac0300 | ||
|
|
ad21c349cd | ||
|
|
383da335d8 | ||
|
|
ebdffc171e | ||
|
|
721b70b7b7 | ||
|
|
556bcda58a | ||
|
|
4cb5bd9edd | ||
|
|
193f6460b0 | ||
|
|
f8d8c8af2d | ||
|
|
efac6990c8 | ||
|
|
250ac481c8 | ||
|
|
44bfa514a5 | ||
|
|
74cedf99d8 | ||
|
|
4c81c321be | ||
|
|
d00fbcd886 | ||
|
|
416f80e745 | ||
|
|
6805826472 | ||
|
|
ce5d234186 | ||
|
|
c95c6e6ef0 | ||
|
|
904f8da8af | ||
|
|
645e9bf16a | ||
|
|
35235509ca | ||
|
|
021330a25d | ||
|
|
6613d5fccb | ||
|
|
9d6e7560f0 | ||
|
|
09e36e0ed8 | ||
|
|
8dde5ccd2e | ||
|
|
f1ed2156e3 | ||
|
|
40b9a60f6c | ||
|
|
59a135a1db | ||
|
|
0123c17e7e | ||
|
|
ac36eeb84d | ||
|
|
143b2b5bd5 | ||
|
|
6006c047d8 | ||
|
|
94d5fe3e43 | ||
|
|
e0ba8a1d60 | ||
|
|
2f8b0ff3a8 | ||
|
|
4700846fad | ||
|
|
6ddf2ab5f8 | ||
|
|
545a26ff04 | ||
|
|
f0f6b80f43 | ||
|
|
0227af199b | ||
|
|
970f5f2480 | ||
|
|
13d0d25f77 | ||
|
|
b64f3a48bf | ||
|
|
86ea3e8572 | ||
|
|
f15a67c8b2 | ||
|
|
659ae75a20 | ||
|
|
0d686b2f44 | ||
|
|
0d611cf4c9 | ||
|
|
6afeb45f43 | ||
|
|
d81616d23c | ||
|
|
6ea63f3e34 | ||
|
|
af52765821 | ||
|
|
acbab9e736 | ||
|
|
5bce2884a7 | ||
|
|
b92998be13 | ||
|
|
1339929de4 | ||
|
|
b0cd27e203 | ||
|
|
65e7c4c053 | ||
|
|
8d8519b52e | ||
|
|
9c95cfd64b | ||
|
|
b0a903b17d | ||
|
|
855b315067 | ||
|
|
aa7b61ecb1 | ||
|
|
c9795141df | ||
|
|
1aed82d5b7 | ||
|
|
752ed93b6f | ||
|
|
de3088f706 | ||
|
|
2608e9165c | ||
|
|
1e0e165eaf | ||
|
|
eff90aaa64 | ||
|
|
77078e1844 | ||
|
|
5929021166 | ||
|
|
8317e2e055 | ||
|
|
eb1cf8d62f | ||
|
|
f6ecb572b1 | ||
|
|
8b9fc30b97 | ||
|
|
d65954c26f | ||
|
|
8a0e260061 | ||
|
|
bb608dbfa7 | ||
|
|
ec5a7e1e48 | ||
|
|
6251dad6e0 | ||
|
|
3982f5a4db | ||
|
|
a8f8760a11 | ||
|
|
fb571ffdbf | ||
|
|
dc2956d05b | ||
|
|
85b19bfe23 | ||
|
|
5b04107447 | ||
|
|
7a5790a6ce | ||
|
|
9d3f4ffa08 | ||
|
|
bc2d4a0415 | ||
|
|
cc346351f7 | ||
|
|
fcc6032ee0 | ||
|
|
ecb040ce98 | ||
|
|
2f9692a1a0 | ||
|
|
042ab95738 | ||
|
|
13be8d511c | ||
|
|
7bdfec77ca | ||
|
|
bc176b8c50 | ||
|
|
68c0307b73 |
2
.github/workflows/diffuse.yml
vendored
2
.github/workflows/diffuse.yml
vendored
@@ -8,7 +8,7 @@ permissions:
|
||||
pull-requests: write # to comment on PR
|
||||
|
||||
env:
|
||||
NDK_VERSION: '27.2.12479018'
|
||||
NDK_VERSION: '28.0.13004108'
|
||||
|
||||
jobs:
|
||||
assemble-base:
|
||||
|
||||
@@ -21,8 +21,8 @@ plugins {
|
||||
|
||||
apply(from = "static-ips.gradle.kts")
|
||||
|
||||
val canonicalVersionCode = 1535
|
||||
val canonicalVersionName = "7.39.4"
|
||||
val canonicalVersionCode = 1540
|
||||
val canonicalVersionName = "7.41.1"
|
||||
val currentHotfixVersion = 0
|
||||
val maxHotfixVersions = 100
|
||||
|
||||
@@ -370,6 +370,7 @@ android {
|
||||
buildConfigField("boolean", "MANAGES_APP_UPDATES", "true")
|
||||
buildConfigField("String", "APK_UPDATE_MANIFEST_URL", "\"${apkUpdateManifestUrl}\"")
|
||||
buildConfigField("String", "BUILD_DISTRIBUTION_TYPE", "\"nightly\"")
|
||||
buildConfigField("boolean", "MESSAGE_BACKUP_RESTORE_ENABLED", "true")
|
||||
}
|
||||
|
||||
create("prod") {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -21,8 +21,6 @@ import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isNull
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.mockkStatic
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.runBlocking
|
||||
@@ -42,12 +40,10 @@ import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.net.SignalNetwork
|
||||
import org.thoughtcrime.securesms.testing.CoroutineDispatcherRule
|
||||
import org.thoughtcrime.securesms.testing.InAppPaymentsRule
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
@@ -72,11 +68,6 @@ class MessageBackupsCheckoutActivityTest {
|
||||
coEvery { AppDependencies.billingApi.queryProduct() } returns BillingProduct(price = FiatMoney(BigDecimal.ONE, Currency.getInstance("USD")))
|
||||
coEvery { AppDependencies.billingApi.launchBillingFlow(any()) } returns Unit
|
||||
|
||||
mockkObject(SignalNetwork)
|
||||
every { SignalNetwork.archive } returns mockk {
|
||||
every { triggerBackupIdReservation(any(), any(), any()) } returns NetworkResult.Success(Unit)
|
||||
}
|
||||
|
||||
mockkStatic(RemoteConfig::class)
|
||||
every { RemoteConfig.messageBackups } returns true
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import io.mockk.every
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -20,15 +20,13 @@ import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.testing.Delete
|
||||
import org.thoughtcrime.securesms.testing.Get
|
||||
import org.thoughtcrime.securesms.testing.InAppPaymentsRule
|
||||
import org.thoughtcrime.securesms.testing.RxTestSchedulerRule
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.actions.RecyclerViewScrollToBottomAction
|
||||
import org.thoughtcrime.securesms.testing.success
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||
import java.math.BigDecimal
|
||||
@@ -118,32 +116,28 @@ class CheckoutFlowActivityTest__RecurringDonations {
|
||||
InAppPaymentsRepository.setSubscriber(subscriber)
|
||||
SignalStore.inAppPayments.setRecurringDonationCurrency(currency)
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Get("/v1/subscription/${subscriber.subscriberId.serialize()}") {
|
||||
MockResponse().success(
|
||||
ActiveSubscription(
|
||||
ActiveSubscription.Subscription(
|
||||
200,
|
||||
currency.currencyCode,
|
||||
BigDecimal.ONE,
|
||||
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
|
||||
true,
|
||||
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
|
||||
false,
|
||||
"active",
|
||||
"STRIPE",
|
||||
"CARD",
|
||||
false
|
||||
),
|
||||
null
|
||||
)
|
||||
AppDependencies.donationsApi.apply {
|
||||
every { getSubscription(subscriber.subscriberId) } returns NetworkResult.Success(
|
||||
ActiveSubscription(
|
||||
ActiveSubscription.Subscription(
|
||||
200,
|
||||
currency.currencyCode,
|
||||
BigDecimal.ONE,
|
||||
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
|
||||
true,
|
||||
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
|
||||
false,
|
||||
"active",
|
||||
"STRIPE",
|
||||
"CARD",
|
||||
false
|
||||
),
|
||||
null
|
||||
)
|
||||
},
|
||||
Delete("/v1/subscription/${subscriber.subscriberId.serialize()}") {
|
||||
Thread.sleep(10000)
|
||||
MockResponse().success()
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
every { deleteSubscription(subscriber.subscriberId) } returns NetworkResult.Success(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initialisePendingSubscription() {
|
||||
@@ -160,27 +154,25 @@ class CheckoutFlowActivityTest__RecurringDonations {
|
||||
InAppPaymentsRepository.setSubscriber(subscriber)
|
||||
SignalStore.inAppPayments.setRecurringDonationCurrency(currency)
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Get("/v1/subscription/${subscriber.subscriberId.serialize()}") {
|
||||
MockResponse().success(
|
||||
ActiveSubscription(
|
||||
ActiveSubscription.Subscription(
|
||||
200,
|
||||
currency.currencyCode,
|
||||
BigDecimal.ONE,
|
||||
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
|
||||
false,
|
||||
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
|
||||
false,
|
||||
"incomplete",
|
||||
"STRIPE",
|
||||
"CARD",
|
||||
false
|
||||
),
|
||||
null
|
||||
)
|
||||
AppDependencies.donationsApi.apply {
|
||||
every { getSubscription(subscriber.subscriberId) } returns NetworkResult.Success(
|
||||
ActiveSubscription(
|
||||
ActiveSubscription.Subscription(
|
||||
200,
|
||||
currency.currencyCode,
|
||||
BigDecimal.ONE,
|
||||
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
|
||||
false,
|
||||
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
|
||||
false,
|
||||
"incomplete",
|
||||
"STRIPE",
|
||||
"CARD",
|
||||
false
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,16 +6,26 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.deleteAll
|
||||
import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderId
|
||||
import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderRecord
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.storage.SignalChatFolderRecord
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import java.util.UUID
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ChatFolderRecord as RemoteChatFolderRecord
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ChatFolderTablesTest {
|
||||
@@ -31,15 +41,19 @@ class ChatFolderTablesTest {
|
||||
private lateinit var folder2: ChatFolderRecord
|
||||
private lateinit var folder3: ChatFolderRecord
|
||||
|
||||
private lateinit var recipientIds: List<RecipientId>
|
||||
|
||||
private var aliceThread: Long = 0
|
||||
private var bobThread: Long = 0
|
||||
private var charlieThread: Long = 0
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
alice = harness.others[1]
|
||||
bob = harness.others[2]
|
||||
charlie = harness.others[3]
|
||||
recipientIds = createRecipients(5)
|
||||
|
||||
alice = recipientIds[0]
|
||||
bob = recipientIds[1]
|
||||
charlie = recipientIds[2]
|
||||
|
||||
aliceThread = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(alice))
|
||||
bobThread = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(bob))
|
||||
@@ -48,32 +62,40 @@ class ChatFolderTablesTest {
|
||||
folder1 = ChatFolderRecord(
|
||||
id = 2,
|
||||
name = "folder1",
|
||||
position = 1,
|
||||
position = 0,
|
||||
includedChats = listOf(aliceThread, bobThread),
|
||||
excludedChats = listOf(charlieThread),
|
||||
showUnread = true,
|
||||
showMutedChats = true,
|
||||
showIndividualChats = true,
|
||||
folderType = ChatFolderRecord.FolderType.CUSTOM
|
||||
folderType = ChatFolderRecord.FolderType.CUSTOM,
|
||||
chatFolderId = ChatFolderId.generate(),
|
||||
storageServiceId = StorageId.forChatFolder(byteArrayOf(1, 2, 3))
|
||||
)
|
||||
|
||||
folder2 = ChatFolderRecord(
|
||||
name = "folder2",
|
||||
position = 2,
|
||||
includedChats = listOf(bobThread),
|
||||
showUnread = true,
|
||||
showMutedChats = true,
|
||||
showIndividualChats = true,
|
||||
folderType = ChatFolderRecord.FolderType.INDIVIDUAL
|
||||
folderType = ChatFolderRecord.FolderType.INDIVIDUAL,
|
||||
chatFolderId = ChatFolderId.generate(),
|
||||
storageServiceId = StorageId.forChatFolder(byteArrayOf(2, 3, 4))
|
||||
)
|
||||
|
||||
folder3 = ChatFolderRecord(
|
||||
name = "folder3",
|
||||
position = 3,
|
||||
includedChats = listOf(bobThread),
|
||||
excludedChats = listOf(aliceThread, charlieThread),
|
||||
showUnread = true,
|
||||
showMutedChats = true,
|
||||
showGroupChats = true,
|
||||
folderType = ChatFolderRecord.FolderType.GROUP
|
||||
folderType = ChatFolderRecord.FolderType.GROUP,
|
||||
chatFolderId = ChatFolderId.generate(),
|
||||
storageServiceId = StorageId.forChatFolder(byteArrayOf(3, 4, 5))
|
||||
)
|
||||
|
||||
SignalDatabase.chatFolders.writableDatabase.deleteAll(ChatFolderTables.ChatFolderTable.TABLE_NAME)
|
||||
@@ -83,7 +105,7 @@ class ChatFolderTablesTest {
|
||||
@Test
|
||||
fun givenChatFolder_whenIGetFolder_thenIExpectFolderWithChats() {
|
||||
SignalDatabase.chatFolders.createFolder(folder1)
|
||||
val actualFolders = SignalDatabase.chatFolders.getChatFolders()
|
||||
val actualFolders = SignalDatabase.chatFolders.getCurrentChatFolders()
|
||||
|
||||
assertEquals(listOf(folder1), actualFolders)
|
||||
}
|
||||
@@ -91,7 +113,7 @@ class ChatFolderTablesTest {
|
||||
@Test
|
||||
fun givenChatFolder_whenIUpdateFolder_thenIExpectUpdatedFolderWithChats() {
|
||||
SignalDatabase.chatFolders.createFolder(folder2)
|
||||
val folder = SignalDatabase.chatFolders.getChatFolders().first()
|
||||
val folder = SignalDatabase.chatFolders.getCurrentChatFolders().first()
|
||||
val updatedFolder = folder.copy(
|
||||
name = "updatedFolder2",
|
||||
position = 1,
|
||||
@@ -100,7 +122,7 @@ class ChatFolderTablesTest {
|
||||
)
|
||||
SignalDatabase.chatFolders.updateFolder(updatedFolder)
|
||||
|
||||
val actualFolder = SignalDatabase.chatFolders.getChatFolders().first()
|
||||
val actualFolder = SignalDatabase.chatFolders.getCurrentChatFolders().first()
|
||||
|
||||
assertEquals(updatedFolder, actualFolder)
|
||||
}
|
||||
@@ -109,11 +131,77 @@ class ChatFolderTablesTest {
|
||||
fun givenADeletedChatFolder_whenIGetFolders_thenIExpectAListWithoutThatFolder() {
|
||||
SignalDatabase.chatFolders.createFolder(folder1)
|
||||
SignalDatabase.chatFolders.createFolder(folder2)
|
||||
val folders = SignalDatabase.chatFolders.getChatFolders()
|
||||
val folders = SignalDatabase.chatFolders.getCurrentChatFolders()
|
||||
SignalDatabase.chatFolders.deleteChatFolder(folders.last())
|
||||
|
||||
val actualFolders = SignalDatabase.chatFolders.getChatFolders()
|
||||
val actualFolders = SignalDatabase.chatFolders.getCurrentChatFolders()
|
||||
|
||||
assertEquals(listOf(folder1), actualFolders)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenChatFolders_whenIUpdateTheirStorageSyncIds_thenIExpectAnUpdatedList() {
|
||||
val existingMap = SignalDatabase.chatFolders.getStorageSyncIdsMap()
|
||||
existingMap.forEach { (id, _) ->
|
||||
SignalDatabase.chatFolders.applyStorageIdUpdate(id, StorageId.forChatFolder(StorageSyncHelper.generateKey()))
|
||||
}
|
||||
val updatedMap = SignalDatabase.chatFolders.getStorageSyncIdsMap()
|
||||
|
||||
existingMap.forEach { (id, storageId) ->
|
||||
assertNotEquals(storageId, updatedMap[id])
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenARemoteFolder_whenIInsertLocally_thenIExpectAListWithThatFolder() {
|
||||
val remoteRecord =
|
||||
SignalChatFolderRecord(
|
||||
folder1.storageServiceId!!,
|
||||
RemoteChatFolderRecord(
|
||||
identifier = UuidUtil.toByteArray(folder1.chatFolderId.uuid).toByteString(),
|
||||
name = folder1.name,
|
||||
position = folder1.position,
|
||||
showOnlyUnread = folder1.showUnread,
|
||||
showMutedChats = folder1.showMutedChats,
|
||||
includeAllIndividualChats = folder1.showIndividualChats,
|
||||
includeAllGroupChats = folder1.showGroupChats,
|
||||
folderType = RemoteChatFolderRecord.FolderType.CUSTOM,
|
||||
deletedAtTimestampMs = folder1.deletedTimestampMs,
|
||||
includedRecipients = listOf(
|
||||
RemoteChatFolderRecord.Recipient(RemoteChatFolderRecord.Recipient.Contact(Recipient.resolved(alice).serviceId.get().toString())),
|
||||
RemoteChatFolderRecord.Recipient(RemoteChatFolderRecord.Recipient.Contact(Recipient.resolved(bob).serviceId.get().toString()))
|
||||
),
|
||||
excludedRecipients = listOf(
|
||||
RemoteChatFolderRecord.Recipient(RemoteChatFolderRecord.Recipient.Contact(Recipient.resolved(charlie).serviceId.get().toString()))
|
||||
)
|
||||
|
||||
)
|
||||
)
|
||||
|
||||
SignalDatabase.chatFolders.insertChatFolderFromStorageSync(remoteRecord)
|
||||
val actualFolders = SignalDatabase.chatFolders.getCurrentChatFolders()
|
||||
|
||||
assertEquals(listOf(folder1), actualFolders)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenADeletedChatFolder_whenIGetPositions_thenIExpectPositionsToStillBeConsecutive() {
|
||||
SignalDatabase.chatFolders.createFolder(folder1)
|
||||
SignalDatabase.chatFolders.createFolder(folder2)
|
||||
SignalDatabase.chatFolders.createFolder(folder3)
|
||||
|
||||
val folders = SignalDatabase.chatFolders.getCurrentChatFolders()
|
||||
SignalDatabase.chatFolders.deleteChatFolder(folders[1])
|
||||
|
||||
val actualFolders = SignalDatabase.chatFolders.getCurrentChatFolders()
|
||||
actualFolders.forEachIndexed { index, folder ->
|
||||
assertEquals(folder.position, index)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createRecipients(count: Int): List<RecipientId> {
|
||||
return (1..count).map {
|
||||
SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,10 +26,13 @@ import org.thoughtcrime.securesms.testing.runSync
|
||||
import org.thoughtcrime.securesms.testing.success
|
||||
import org.whispersystems.signalservice.api.SignalServiceDataStore
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender
|
||||
import org.whispersystems.signalservice.api.archive.ArchiveApi
|
||||
import org.whispersystems.signalservice.api.attachment.AttachmentApi
|
||||
import org.whispersystems.signalservice.api.donations.DonationsApi
|
||||
import org.whispersystems.signalservice.api.keys.KeysApi
|
||||
import org.whispersystems.signalservice.api.message.MessageApi
|
||||
import org.whispersystems.signalservice.api.push.TrustStore
|
||||
import org.whispersystems.signalservice.api.websocket.SignalWebSocket
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
|
||||
@@ -94,6 +97,7 @@ class InstrumentationApplicationDependencyProvider(val application: Application,
|
||||
networkInterceptors = emptyList(),
|
||||
dns = Optional.of(SignalServiceNetworkAccess.DNS),
|
||||
signalProxy = Optional.empty(),
|
||||
systemHttpProxy = Optional.empty(),
|
||||
zkGroupServerPublicParams = Base64.decode(BuildConfig.ZKGROUP_SERVER_PUBLIC_PARAMS),
|
||||
genericServerPublicParams = Base64.decode(BuildConfig.GENERIC_SERVER_PUBLIC_PARAMS),
|
||||
backupServerPublicParams = Base64.decode(BuildConfig.BACKUP_SERVER_PUBLIC_PARAMS),
|
||||
@@ -122,6 +126,14 @@ class InstrumentationApplicationDependencyProvider(val application: Application,
|
||||
return recipientCache
|
||||
}
|
||||
|
||||
override fun provideArchiveApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket, pushServiceSocket: PushServiceSocket): ArchiveApi {
|
||||
return mockk()
|
||||
}
|
||||
|
||||
override fun provideDonationsApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket): DonationsApi {
|
||||
return mockk()
|
||||
}
|
||||
|
||||
override fun provideSignalServiceMessageSender(
|
||||
protocolStore: SignalServiceDataStore,
|
||||
pushServiceSocket: PushServiceSocket,
|
||||
|
||||
@@ -6,10 +6,11 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import io.mockk.every
|
||||
import org.junit.rules.ExternalResource
|
||||
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.util.JsonUtils
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
|
||||
|
||||
/**
|
||||
@@ -23,29 +24,25 @@ class InAppPaymentsRule : ExternalResource() {
|
||||
}
|
||||
|
||||
private fun initialiseConfigurationResponse() {
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Get("/v1/subscription/configuration") {
|
||||
val assets = InstrumentationRegistry.getInstrumentation().context.resources.assets
|
||||
assets.open("inAppPaymentsTests/configuration.json").use { stream ->
|
||||
MockResponse().success(JsonUtils.fromJson(stream, SubscriptionsConfiguration::class.java))
|
||||
}
|
||||
}
|
||||
)
|
||||
val assets = InstrumentationRegistry.getInstrumentation().context.resources.assets
|
||||
val response = assets.open("inAppPaymentsTests/configuration.json").use { stream ->
|
||||
NetworkResult.Success(JsonUtils.fromJson(stream, SubscriptionsConfiguration::class.java))
|
||||
}
|
||||
|
||||
AppDependencies.donationsApi.apply {
|
||||
every { getDonationsConfiguration(any()) } returns response
|
||||
}
|
||||
}
|
||||
|
||||
private fun initialisePutSubscription() {
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Put("/v1/subscription/") {
|
||||
MockResponse().success()
|
||||
}
|
||||
)
|
||||
AppDependencies.donationsApi.apply {
|
||||
every { putSubscription(any()) } returns NetworkResult.Success(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initialiseSetArchiveBackupId() {
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Put("/v1/archives/backupid") {
|
||||
MockResponse().success()
|
||||
}
|
||||
)
|
||||
AppDependencies.archiveApi.apply {
|
||||
every { triggerBackupIdReservation(any(), any(), any()) } returns NetworkResult.Success(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ private fun Content(
|
||||
Scaffolds.Settings(
|
||||
title = "Conversation Test Springboard",
|
||||
onNavigationClick = onBackPressed,
|
||||
navigationIconPainter = rememberVectorPainter(ImageVector.vectorResource(id = R.drawable.symbol_arrow_left_24))
|
||||
navigationIconPainter = rememberVectorPainter(ImageVector.vectorResource(id = R.drawable.symbol_arrow_start_24))
|
||||
) {
|
||||
Column(modifier = Modifier.padding(it)) {
|
||||
Rows.TextRow(
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
android:supportsRtl="true"
|
||||
android:resizeableActivity="true"
|
||||
android:fullBackupOnly="false"
|
||||
android:enableOnBackInvokedCallback="false"
|
||||
android:allowBackup="true"
|
||||
android:backupAgent=".absbackup.SignalBackupAgent"
|
||||
android:theme="@style/TextSecure.LightTheme"
|
||||
@@ -894,6 +895,10 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".stickers.StickerManagementActivityV2"
|
||||
android:exported="false"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity android:name=".logsubmit.SubmitDebugLogActivity"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
@@ -1069,8 +1074,10 @@
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".MainActivity"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:resizeableActivity="true"
|
||||
android:exported="false"/>
|
||||
|
||||
@@ -1148,6 +1155,10 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".groups.ui.incommon.GroupsInCommonActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar" />
|
||||
|
||||
<service
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,7 @@ import org.signal.core.util.logging.Log;
|
||||
import org.signal.core.util.logging.Scrubber;
|
||||
import org.signal.core.util.tracing.Tracer;
|
||||
import org.signal.glide.SignalGlideCodecs;
|
||||
import org.signal.libsignal.net.ChatServiceException;
|
||||
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.thoughtcrime.securesms.apkupdate.ApkUpdateRefreshListener;
|
||||
@@ -102,6 +103,7 @@ import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.AppForegroundObserver;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
import org.thoughtcrime.securesms.util.ConversationUtil;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig;
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
||||
@@ -188,6 +190,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
|
||||
.addBlocking("tracer", this::initializeTracer)
|
||||
.addNonBlocking(() -> RegistrationUtil.maybeMarkRegistrationComplete())
|
||||
.addNonBlocking(() -> Glide.get(this))
|
||||
.addNonBlocking(ConversationUtil::refreshRecipientShortcuts)
|
||||
.addNonBlocking(this::cleanAvatarStorage)
|
||||
.addNonBlocking(this::initializeRevealableMessageManager)
|
||||
.addNonBlocking(this::initializePendingRetryReceiptManager)
|
||||
@@ -363,7 +366,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
|
||||
e = e.getCause();
|
||||
}
|
||||
|
||||
if (wasWrapped && (e instanceof SocketException || e instanceof InterruptedException || e instanceof InterruptedIOException)) {
|
||||
if (wasWrapped && (e instanceof SocketException || e instanceof InterruptedException || e instanceof InterruptedIOException || e instanceof ChatServiceException)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.transition.TransitionInflater;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -26,6 +25,7 @@ import com.bumptech.glide.request.RequestListener;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.target.Target;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
import com.github.chrisbanes.photoview.PhotoView;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.avatar.fallback.FallbackAvatar;
|
||||
@@ -46,6 +46,12 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
|
||||
|
||||
private static final String RECIPIENT_ID_EXTRA = "recipient_id";
|
||||
|
||||
private static final int ZOOM_TRANSITION_DURATION = 300;
|
||||
|
||||
private static final float ZOOM_LEVEL_MIN = 1.0f;
|
||||
private static final float SMALL_IMAGES_ZOOM_LEVEL_MID = 3.0f;
|
||||
private static final float SMALL_IMAGES_ZOOM_LEVEL_MAX = 8.0f;
|
||||
|
||||
public static @NonNull Intent intentFromRecipientId(@NonNull Context context,
|
||||
@NonNull RecipientId recipientId)
|
||||
{
|
||||
@@ -78,7 +84,10 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
EmojiTextView title = findViewById(R.id.title);
|
||||
ImageView avatar = findViewById(R.id.avatar);
|
||||
PhotoView avatar = findViewById(R.id.avatar);
|
||||
avatar.setZoomTransitionDuration(ZOOM_TRANSITION_DURATION);
|
||||
avatar.setScaleLevels(ZOOM_LEVEL_MIN, SMALL_IMAGES_ZOOM_LEVEL_MID, SMALL_IMAGES_ZOOM_LEVEL_MAX);
|
||||
|
||||
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
@@ -134,7 +143,7 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
|
||||
|
||||
FullscreenHelper fullscreenHelper = new FullscreenHelper(this);
|
||||
|
||||
findViewById(android.R.id.content).setOnClickListener(v -> fullscreenHelper.toggleUiVisibility());
|
||||
avatar.setOnClickListener(v -> fullscreenHelper.toggleUiVisibility());
|
||||
|
||||
fullscreenHelper.configureToolbarLayout(findViewById(R.id.toolbar_cutout_spacer), toolbar);
|
||||
|
||||
|
||||
@@ -17,9 +17,11 @@ public interface BindableConversationListItem extends Unbindable {
|
||||
@NonNull ThreadRecord thread,
|
||||
@NonNull RequestManager requestManager, @NonNull Locale locale,
|
||||
@NonNull Set<Long> typingThreads,
|
||||
@NonNull ConversationSet selectedConversations);
|
||||
@NonNull ConversationSet selectedConversations,
|
||||
long activeThreadId);
|
||||
|
||||
void setSelectedConversations(@NonNull ConversationSet conversations);
|
||||
void setActiveThreadId(long activeThreadId);
|
||||
void updateTypingIndicator(@NonNull Set<Long> typingThreads);
|
||||
void updateTimestamp();
|
||||
}
|
||||
|
||||
@@ -5,35 +5,62 @@
|
||||
|
||||
package org.thoughtcrime.securesms
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
import android.widget.Toast
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.BoxWithConstraintsScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.displayCutoutPadding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
|
||||
import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective
|
||||
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.compose.AndroidFragment
|
||||
import androidx.fragment.compose.rememberFragmentState
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.getSerializableCompat
|
||||
import org.signal.donations.StripeApi
|
||||
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar.show
|
||||
import org.thoughtcrime.securesms.calls.log.CallLogFilter
|
||||
import org.thoughtcrime.securesms.calls.new.NewCallActivity
|
||||
import org.thoughtcrime.securesms.components.ConnectivityWarningBottomSheet
|
||||
import org.thoughtcrime.securesms.components.DebugLogsPromptDialogFragment
|
||||
import org.thoughtcrime.securesms.components.DeviceSpecificNotificationBottomSheet
|
||||
@@ -43,36 +70,56 @@ import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity.Co
|
||||
import org.thoughtcrime.securesms.components.settings.app.notifications.manual.NotificationProfileSelectionFragment
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationFragment
|
||||
import org.thoughtcrime.securesms.conversation.v2.MotionEventRelay
|
||||
import org.thoughtcrime.securesms.conversation.v2.ShareDataTimestampViewModel
|
||||
import org.thoughtcrime.securesms.conversationlist.RelinkDevicesReminderBottomSheetFragment
|
||||
import org.thoughtcrime.securesms.conversationlist.RestoreCompleteBottomSheetDialog
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
|
||||
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceExitActivity
|
||||
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity
|
||||
import org.thoughtcrime.securesms.main.MainActivityListHostFragment
|
||||
import org.thoughtcrime.securesms.main.MainNavigationDestination
|
||||
import org.thoughtcrime.securesms.main.MainBottomChrome
|
||||
import org.thoughtcrime.securesms.main.MainBottomChromeCallback
|
||||
import org.thoughtcrime.securesms.main.MainBottomChromeState
|
||||
import org.thoughtcrime.securesms.main.MainContentLayoutData
|
||||
import org.thoughtcrime.securesms.main.MainMegaphoneState
|
||||
import org.thoughtcrime.securesms.main.MainNavigationBar
|
||||
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation
|
||||
import org.thoughtcrime.securesms.main.MainNavigationListLocation
|
||||
import org.thoughtcrime.securesms.main.MainNavigationRail
|
||||
import org.thoughtcrime.securesms.main.MainNavigationViewModel
|
||||
import org.thoughtcrime.securesms.main.MainToolbar
|
||||
import org.thoughtcrime.securesms.main.MainToolbarCallback
|
||||
import org.thoughtcrime.securesms.main.MainToolbarMode
|
||||
import org.thoughtcrime.securesms.main.MainToolbarViewModel
|
||||
import org.thoughtcrime.securesms.main.NavigationBarSpacerCompat
|
||||
import org.thoughtcrime.securesms.main.SnackbarState
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil
|
||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphone
|
||||
import org.thoughtcrime.securesms.megaphone.MegaphoneActionController
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones
|
||||
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor
|
||||
import org.thoughtcrime.securesms.notifications.VitalsViewModel
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.profiles.manage.UsernameEditFragment
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
import org.thoughtcrime.securesms.stories.settings.StorySettingsActivity
|
||||
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabRepository
|
||||
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsFragment
|
||||
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel
|
||||
import org.thoughtcrime.securesms.util.AppForegroundObserver
|
||||
import org.thoughtcrime.securesms.util.AppStartup
|
||||
import org.thoughtcrime.securesms.util.CachedInflater
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
import org.thoughtcrime.securesms.util.SplashScreenUtil
|
||||
import org.thoughtcrime.securesms.util.WindowUtil
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
import org.thoughtcrime.securesms.window.AppScaffold
|
||||
import org.thoughtcrime.securesms.window.WindowSizeClass
|
||||
|
||||
class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, MainNavigator.NavigatorProvider {
|
||||
|
||||
@@ -87,23 +134,23 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun clearTopAndOpenTab(context: Context, startingTab: MainNavigationDestination): Intent {
|
||||
fun clearTopAndOpenTab(context: Context, startingTab: MainNavigationListLocation): Intent {
|
||||
return clearTop(context).putExtra(KEY_STARTING_TAB, startingTab)
|
||||
}
|
||||
}
|
||||
|
||||
private val dynamicTheme = DynamicNoActionBarTheme()
|
||||
private val navigator = MainNavigator(this)
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
private lateinit var mediaController: VoiceNoteMediaController
|
||||
private lateinit var navigator: MainNavigator
|
||||
|
||||
override val voiceNoteMediaController: VoiceNoteMediaController
|
||||
get() = mediaController
|
||||
|
||||
private val conversationListTabsViewModel: ConversationListTabsViewModel by viewModel {
|
||||
val startingTab = intent.extras?.getSerializableCompat(KEY_STARTING_TAB, MainNavigationDestination::class.java)
|
||||
ConversationListTabsViewModel(startingTab ?: MainNavigationDestination.CHATS, ConversationListTabRepository())
|
||||
private val mainNavigationViewModel: MainNavigationViewModel by viewModel {
|
||||
val startingTab = intent.extras?.getSerializableCompat(KEY_STARTING_TAB, MainNavigationListLocation::class.java)
|
||||
MainNavigationViewModel(startingTab ?: MainNavigationListLocation.CHATS)
|
||||
}
|
||||
|
||||
private val vitalsViewModel: VitalsViewModel by viewModel {
|
||||
@@ -118,57 +165,192 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
|
||||
private val toolbarViewModel: MainToolbarViewModel by viewModels()
|
||||
private val toolbarCallback = ToolbarCallback()
|
||||
private val shareDataTimestampViewModel: ShareDataTimestampViewModel by viewModels()
|
||||
|
||||
private val motionEventRelay: MotionEventRelay by viewModels()
|
||||
|
||||
private var onFirstRender = false
|
||||
|
||||
private val mainBottomChromeCallback = BottomChromeCallback()
|
||||
private val megaphoneActionController = MainMegaphoneActionController()
|
||||
private val mainNavigationCallback = MainNavigationCallback()
|
||||
|
||||
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
|
||||
return motionEventRelay.offer(ev) || super.dispatchTouchEvent(ev)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
enableEdgeToEdge()
|
||||
AppStartup.getInstance().onCriticalRenderEventStart()
|
||||
|
||||
enableEdgeToEdge(
|
||||
navigationBarStyle = if (DynamicTheme.isDarkTheme(this)) {
|
||||
SystemBarStyle.dark(0)
|
||||
} else {
|
||||
SystemBarStyle.light(0, 0)
|
||||
}
|
||||
)
|
||||
|
||||
super.onCreate(savedInstanceState, ready)
|
||||
conversationListTabsViewModel
|
||||
navigator = MainNavigator(this, mainNavigationViewModel)
|
||||
|
||||
setContent {
|
||||
val navState = rememberFragmentState()
|
||||
val listHostState = rememberFragmentState()
|
||||
val detailLocation by navigator.viewModel.detailLocation.collectAsStateWithLifecycle()
|
||||
AppForegroundObserver.addListener(object : AppForegroundObserver.Listener {
|
||||
override fun onForeground() {
|
||||
mainNavigationViewModel.getNextMegaphone()
|
||||
}
|
||||
})
|
||||
|
||||
LaunchedEffect(detailLocation) {
|
||||
if (detailLocation is MainNavigationDetailLocation.Conversation) {
|
||||
startActivity((detailLocation as MainNavigationDetailLocation.Conversation).intent)
|
||||
overridePendingTransition(R.anim.slide_from_end, R.anim.fade_scale_out)
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
mainNavigationViewModel.navigationEvents.collectLatest {
|
||||
when (it) {
|
||||
MainNavigationViewModel.NavigationEvent.STORY_CAMERA_FIRST -> {
|
||||
mainBottomChromeCallback.onCameraClick(MainNavigationListLocation.STORIES)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AppScaffold(
|
||||
bottomNavContent = {
|
||||
AndroidFragment(
|
||||
clazz = ConversationListTabsFragment::class.java,
|
||||
fragmentState = navState
|
||||
)
|
||||
},
|
||||
navRailContent = {
|
||||
AndroidFragment(
|
||||
clazz = ConversationListTabsFragment::class.java,
|
||||
fragmentState = navState
|
||||
)
|
||||
}
|
||||
) {
|
||||
Column {
|
||||
val state by toolbarViewModel.state.collectAsStateWithLifecycle()
|
||||
shareDataTimestampViewModel.setTimestampFromActivityCreation(savedInstanceState, intent)
|
||||
|
||||
SignalTheme(isDarkMode = DynamicTheme.isDarkTheme(LocalContext.current)) {
|
||||
MainToolbar(
|
||||
state = state,
|
||||
callback = toolbarCallback
|
||||
)
|
||||
setContent {
|
||||
val listHostState = rememberFragmentState()
|
||||
val detailLocation by mainNavigationViewModel.detailLocationRequests.collectAsStateWithLifecycle()
|
||||
val snackbar by mainNavigationViewModel.snackbar.collectAsStateWithLifecycle()
|
||||
val mainToolbarState by toolbarViewModel.state.collectAsStateWithLifecycle()
|
||||
val megaphone by mainNavigationViewModel.megaphone.collectAsStateWithLifecycle()
|
||||
val mainNavigationState by mainNavigationViewModel.mainNavigationState.collectAsStateWithLifecycle()
|
||||
|
||||
val isNavigationVisible = remember(mainToolbarState.mode) {
|
||||
mainToolbarState.mode == MainToolbarMode.FULL
|
||||
}
|
||||
|
||||
val mainBottomChromeState = remember(mainToolbarState.destination, snackbar, mainToolbarState.mode, megaphone) {
|
||||
MainBottomChromeState(
|
||||
destination = mainToolbarState.destination,
|
||||
snackbarState = snackbar,
|
||||
mainToolbarMode = mainToolbarState.mode,
|
||||
megaphoneState = MainMegaphoneState(
|
||||
megaphone = megaphone,
|
||||
mainToolbarMode = mainToolbarState.mode
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
|
||||
val contentLayoutData = MainContentLayoutData.rememberContentLayoutData()
|
||||
|
||||
MainContainer {
|
||||
val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator<Any>(
|
||||
scaffoldDirective = calculatePaneScaffoldDirective(
|
||||
currentWindowAdaptiveInfo()
|
||||
).copy(
|
||||
maxHorizontalPartitions = if (windowSizeClass.isSplitPane()) 2 else 1,
|
||||
horizontalPartitionSpacerSize = contentLayoutData.partitionWidth,
|
||||
defaultPanePreferredWidth = contentLayoutData.rememberDefaultPanePreferredWidth(maxWidth)
|
||||
)
|
||||
)
|
||||
|
||||
LaunchedEffect(detailLocation) {
|
||||
if (detailLocation is MainNavigationDetailLocation.Conversation) {
|
||||
if (SignalStore.internal.largeScreenUi) {
|
||||
scaffoldNavigator.navigateTo(ThreePaneScaffoldRole.Primary, detailLocation)
|
||||
} else {
|
||||
startActivity((detailLocation as MainNavigationDetailLocation.Conversation).intent)
|
||||
}
|
||||
}
|
||||
|
||||
AndroidFragment(
|
||||
clazz = MainActivityListHostFragment::class.java,
|
||||
fragmentState = listHostState,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
mainNavigationViewModel.goTo(MainNavigationDetailLocation.Empty)
|
||||
}
|
||||
|
||||
AppScaffold(
|
||||
navigator = scaffoldNavigator,
|
||||
bottomNavContent = {
|
||||
if (isNavigationVisible) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.clip(contentLayoutData.navigationBarShape)
|
||||
.background(color = SignalTheme.colors.colorSurface2)
|
||||
) {
|
||||
MainNavigationBar(
|
||||
state = mainNavigationState,
|
||||
onDestinationSelected = mainNavigationCallback
|
||||
)
|
||||
|
||||
if (!windowSizeClass.isSplitPane()) {
|
||||
NavigationBarSpacerCompat()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
navRailContent = {
|
||||
if (isNavigationVisible) {
|
||||
MainNavigationRail(
|
||||
state = mainNavigationState,
|
||||
mainFloatingActionButtonsCallback = mainBottomChromeCallback,
|
||||
onDestinationSelected = mainNavigationCallback
|
||||
)
|
||||
}
|
||||
},
|
||||
listContent = {
|
||||
val listContainerColor = if (windowSizeClass.isMedium()) {
|
||||
SignalTheme.colors.colorSurface1
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surface
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = contentLayoutData.listPaddingStart)
|
||||
.fillMaxSize()
|
||||
.background(listContainerColor)
|
||||
.clip(contentLayoutData.shape)
|
||||
) {
|
||||
MainToolbar(
|
||||
state = mainToolbarState,
|
||||
callback = toolbarCallback
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
AndroidFragment(
|
||||
clazz = MainActivityListHostFragment::class.java,
|
||||
fragmentState = listHostState,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
|
||||
MainBottomChrome(
|
||||
state = mainBottomChromeState,
|
||||
callback = mainBottomChromeCallback,
|
||||
megaphoneActionController = megaphoneActionController,
|
||||
modifier = Modifier.align(Alignment.BottomCenter)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
detailContent = {
|
||||
when (val destination = scaffoldNavigator.currentDestination?.contentKey) {
|
||||
is MainNavigationDetailLocation.Conversation -> {
|
||||
val fragmentState = key(destination) { rememberFragmentState() }
|
||||
AndroidFragment(
|
||||
clazz = ConversationFragment::class.java,
|
||||
fragmentState = fragmentState,
|
||||
arguments = requireNotNull(destination.intent.extras) { "Handed null Conversation intent arguments." },
|
||||
modifier = Modifier
|
||||
.padding(end = contentLayoutData.detailPaddingEnd)
|
||||
.clip(contentLayoutData.shape)
|
||||
.background(color = MaterialTheme.colorScheme.surface)
|
||||
.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
paneExpansionDragHandle = if (contentLayoutData.hasDragHandle()) {
|
||||
{ }
|
||||
} else null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,11 +373,37 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
|
||||
handleDeepLinkIntent(intent)
|
||||
CachedInflater.from(this).clear()
|
||||
updateNavigationBarColor()
|
||||
|
||||
lifecycleDisposable += vitalsViewModel.vitalsState.subscribe(this::presentVitalsState)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MainContainer(content: @Composable BoxWithConstraintsScope.() -> Unit) {
|
||||
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
|
||||
|
||||
SignalTheme(isDarkMode = DynamicTheme.isDarkTheme(this)) {
|
||||
val backgroundColor = if (windowSizeClass.isCompact()) {
|
||||
MaterialTheme.colorScheme.surface
|
||||
} else {
|
||||
SignalTheme.colors.colorSurface1
|
||||
}
|
||||
|
||||
val modifier = if (windowSizeClass.isSplitPane()) {
|
||||
Modifier.systemBarsPadding().displayCutoutPadding()
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.background(color = backgroundColor)
|
||||
.then(modifier)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getIntent(): Intent {
|
||||
return super.getIntent().setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
}
|
||||
@@ -205,14 +413,14 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
handleDeepLinkIntent(intent)
|
||||
|
||||
val extras = intent.extras ?: return
|
||||
val startingTab = extras.getSerializableCompat(KEY_STARTING_TAB, MainNavigationDestination::class.java)
|
||||
val startingTab = extras.getSerializableCompat(KEY_STARTING_TAB, MainNavigationListLocation::class.java)
|
||||
|
||||
when (startingTab) {
|
||||
MainNavigationDestination.CHATS -> conversationListTabsViewModel.onChatsSelected()
|
||||
MainNavigationDestination.CALLS -> conversationListTabsViewModel.onCallsSelected()
|
||||
MainNavigationDestination.STORIES -> {
|
||||
MainNavigationListLocation.CHATS -> mainNavigationViewModel.onChatsSelected()
|
||||
MainNavigationListLocation.CALLS -> mainNavigationViewModel.onCallsSelected()
|
||||
MainNavigationListLocation.STORIES -> {
|
||||
if (Stories.isFeatureEnabled()) {
|
||||
conversationListTabsViewModel.onStoriesSelected()
|
||||
mainNavigationViewModel.onStoriesSelected()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,9 +458,8 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
.show()
|
||||
}
|
||||
|
||||
updateNavigationBarColor()
|
||||
|
||||
vitalsViewModel.checkSlowNotificationHeuristics()
|
||||
mainNavigationViewModel.refreshNavigationBarState()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
@@ -260,10 +467,8 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
SplashScreenUtil.setSplashScreenThemeIfNecessary(this, SignalStore.settings.theme)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (!navigator.onBackPressed()) {
|
||||
super.onBackPressed()
|
||||
}
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray, deviceId: Int) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
@@ -271,6 +476,20 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
if (requestCode == MainNavigator.REQUEST_CONFIG_CHANGES && resultCode == RESULT_CONFIG_CHANGED) {
|
||||
recreate()
|
||||
}
|
||||
|
||||
if (resultCode == RESULT_OK && requestCode == CreateSvrPinActivity.REQUEST_NEW_PIN) {
|
||||
mainNavigationViewModel.setSnackbar(SnackbarState(message = getString(R.string.ConfirmKbsPinFragment__pin_created)))
|
||||
mainNavigationViewModel.onMegaphoneCompleted(Megaphones.Event.PINS_FOR_ALL)
|
||||
}
|
||||
|
||||
if (resultCode == RESULT_OK && requestCode == UsernameEditFragment.REQUEST_CODE) {
|
||||
val snackbarString = getString(R.string.ConversationListFragment_username_recovered_toast, SignalStore.account.username)
|
||||
mainNavigationViewModel.setSnackbar(
|
||||
SnackbarState(
|
||||
message = snackbarString
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFirstRender() {
|
||||
@@ -282,6 +501,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
}
|
||||
|
||||
private fun handleDeepLinkIntent(intent: Intent) {
|
||||
handleConversationIntent(intent)
|
||||
handleGroupLinkInIntent(intent)
|
||||
handleProxyInIntent(intent)
|
||||
handleSignalMeIntent(intent)
|
||||
@@ -289,10 +509,6 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
handleDonateReturnIntent(intent)
|
||||
}
|
||||
|
||||
private fun updateNavigationBarColor() {
|
||||
WindowUtil.setNavigationBarColor(this, ContextCompat.getColor(this, R.color.signal_colorSurface2))
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
private fun presentVitalsState(state: VitalsViewModel.State) {
|
||||
when (state) {
|
||||
@@ -306,6 +522,12 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleConversationIntent(intent: Intent) {
|
||||
if (ConversationIntents.isConversationIntent(intent)) {
|
||||
mainNavigationViewModel.goTo(MainNavigationDetailLocation.Conversation(intent))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleGroupLinkInIntent(intent: Intent) {
|
||||
intent.data?.let { data ->
|
||||
CommunicationActions.handlePotentialGroupLinkUrl(this, data.toString())
|
||||
@@ -418,4 +640,96 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
toolbarViewModel.setShowNotificationProfilesTooltip(false)
|
||||
}
|
||||
}
|
||||
|
||||
inner class BottomChromeCallback : MainBottomChromeCallback {
|
||||
override fun onNewChatClick() {
|
||||
startActivity(Intent(this@MainActivity, NewConversationActivity::class.java))
|
||||
}
|
||||
|
||||
override fun onNewCallClick() {
|
||||
startActivity(NewCallActivity.createIntent(this@MainActivity))
|
||||
}
|
||||
|
||||
override fun onCameraClick(destination: MainNavigationListLocation) {
|
||||
val onGranted = {
|
||||
startActivity(
|
||||
MediaSelectionActivity.camera(
|
||||
context = this@MainActivity,
|
||||
isStory = destination == MainNavigationListLocation.STORIES
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (CameraXUtil.isSupported()) {
|
||||
onGranted()
|
||||
} else {
|
||||
Permissions.with(this@MainActivity)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_and_video_allow_camera), R.drawable.symbol_camera_24)
|
||||
.withPermanentDenialDialog(
|
||||
getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos),
|
||||
null,
|
||||
R.string.CameraXFragment_allow_access_camera,
|
||||
R.string.CameraXFragment_to_capture_photos_videos,
|
||||
supportFragmentManager
|
||||
)
|
||||
.onAllGranted(onGranted)
|
||||
.onAnyDenied { Toast.makeText(this@MainActivity, R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show() }
|
||||
.execute()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMegaphoneVisible(megaphone: Megaphone) {
|
||||
mainNavigationViewModel.onMegaphoneVisible(megaphone)
|
||||
}
|
||||
|
||||
override fun onSnackbarDismissed() {
|
||||
mainNavigationViewModel.setSnackbar(null)
|
||||
}
|
||||
}
|
||||
|
||||
inner class MainMegaphoneActionController : MegaphoneActionController {
|
||||
override fun onMegaphoneNavigationRequested(intent: Intent) {
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onMegaphoneNavigationRequested(intent: Intent, requestCode: Int) {
|
||||
startActivityForResult(intent, requestCode)
|
||||
}
|
||||
|
||||
override fun onMegaphoneToastRequested(string: String) {
|
||||
mainNavigationViewModel.setSnackbar(
|
||||
SnackbarState(
|
||||
message = string
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun getMegaphoneActivity(): Activity {
|
||||
return this@MainActivity
|
||||
}
|
||||
|
||||
override fun onMegaphoneSnooze(event: Megaphones.Event) {
|
||||
mainNavigationViewModel.onMegaphoneSnoozed(event)
|
||||
}
|
||||
|
||||
override fun onMegaphoneCompleted(event: Megaphones.Event) {
|
||||
mainNavigationViewModel.onMegaphoneCompleted(event)
|
||||
}
|
||||
|
||||
override fun onMegaphoneDialogFragmentRequested(dialogFragment: DialogFragment) {
|
||||
dialogFragment.show(supportFragmentManager, "megaphone_dialog")
|
||||
}
|
||||
}
|
||||
|
||||
private inner class MainNavigationCallback : (MainNavigationListLocation) -> Unit {
|
||||
override fun invoke(location: MainNavigationListLocation) {
|
||||
when (location) {
|
||||
MainNavigationListLocation.CHATS -> mainNavigationViewModel.onChatsSelected()
|
||||
MainNavigationListLocation.CALLS -> mainNavigationViewModel.onCallsSelected()
|
||||
MainNavigationListLocation.STORIES -> mainNavigationViewModel.onStoriesSelected()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,9 @@ package org.thoughtcrime.securesms;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.lifecycle.viewmodel.internal.ViewModelProviders;
|
||||
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
|
||||
@@ -27,25 +23,16 @@ public class MainNavigator {
|
||||
|
||||
private final AppCompatActivity activity;
|
||||
private final LifecycleDisposable lifecycleDisposable;
|
||||
private final MainNavigationViewModel viewModel;
|
||||
|
||||
private MainNavigationViewModel viewModel;
|
||||
|
||||
public MainNavigator(@NonNull AppCompatActivity activity) {
|
||||
public MainNavigator(@NonNull AppCompatActivity activity, @NonNull MainNavigationViewModel viewModel) {
|
||||
this.activity = activity;
|
||||
this.lifecycleDisposable = new LifecycleDisposable();
|
||||
this.viewModel = viewModel;
|
||||
|
||||
lifecycleDisposable.bindTo(activity);
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public @NonNull MainNavigationViewModel getViewModel() {
|
||||
if (viewModel == null) {
|
||||
viewModel = new ViewModelProvider(activity).get(MainNavigationViewModel.class);
|
||||
}
|
||||
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
public static MainNavigator get(@NonNull Activity activity) {
|
||||
if (!(activity instanceof MainActivity)) {
|
||||
throw new IllegalArgumentException("Activity must be an instance of MainActivity!");
|
||||
@@ -54,20 +41,6 @@ public class MainNavigator {
|
||||
return ((NavigatorProvider) activity).getNavigator();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the back pressed was handled in our own custom way, false if it should be given
|
||||
* to the system to do the default behavior.
|
||||
*/
|
||||
public boolean onBackPressed() {
|
||||
Fragment fragment = getFragmentManager().findFragmentById(R.id.fragment_container);
|
||||
|
||||
if (fragment instanceof BackHandler) {
|
||||
return ((BackHandler) fragment).onBackPressed();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void goToConversation(@NonNull RecipientId recipientId, long threadId, int distributionType, int startingPosition) {
|
||||
Disposable disposable = ConversationIntents.createBuilder(activity, recipientId, threadId)
|
||||
.map(builder -> builder.withDistributionType(distributionType)
|
||||
|
||||
@@ -9,12 +9,18 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.lifecycle.map
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.database.model.ProfileAvatarFileDetails
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.NameUtil
|
||||
|
||||
@Composable
|
||||
fun AvatarImage(
|
||||
@@ -28,15 +34,24 @@ fun AvatarImage(
|
||||
.background(color = Color.Red, shape = CircleShape)
|
||||
)
|
||||
} else {
|
||||
val context = LocalContext.current
|
||||
val state = recipient.live().liveData.map { AvatarImageState(NameUtil.getAbbreviation(it.getDisplayName(context)), it, AvatarHelper.getAvatarFileDetails(context, it.id)) }.observeAsState().value ?: return
|
||||
|
||||
AndroidView(
|
||||
factory = ::AvatarImageView,
|
||||
modifier = modifier.background(color = Color.Transparent, shape = CircleShape)
|
||||
) {
|
||||
if (useProfile) {
|
||||
it.setAvatarUsingProfile(recipient)
|
||||
it.setAvatarUsingProfile(state.self)
|
||||
} else {
|
||||
it.setAvatar(recipient)
|
||||
it.setAvatar(state.self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class AvatarImageState(
|
||||
val displayName: String?,
|
||||
val self: Recipient,
|
||||
val avatarFileDetails: ProfileAvatarFileDetails
|
||||
)
|
||||
|
||||
@@ -183,6 +183,14 @@ object ImportSkips {
|
||||
return log(sentTimestamp, "Failed to find a threadId for the provided chatId. ChatId in backup: $chatId")
|
||||
}
|
||||
|
||||
fun chatFolderIdNotFound(): String {
|
||||
return log(0, "Failed to parse chatFolderId for the provided chat folder.")
|
||||
}
|
||||
|
||||
fun notificationProfileIdNotFound(): String {
|
||||
return log(0, "Failed to parse notificationProfileId for the provided notification profile.")
|
||||
}
|
||||
|
||||
private fun log(sentTimestamp: Long, message: String): String {
|
||||
return "[SKIP][$sentTimestamp] $message"
|
||||
}
|
||||
|
||||
@@ -6,10 +6,13 @@
|
||||
package org.thoughtcrime.securesms.backup.v2.processor
|
||||
|
||||
import androidx.core.content.contentValuesOf
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.signal.core.util.insertInto
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.backup.v2.ExportState
|
||||
import org.thoughtcrime.securesms.backup.v2.ImportSkips
|
||||
import org.thoughtcrime.securesms.backup.v2.ImportState
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.ChatFolder
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||
@@ -19,6 +22,8 @@ import org.thoughtcrime.securesms.database.ChatFolderTables.ChatFolderMembership
|
||||
import org.thoughtcrime.securesms.database.ChatFolderTables.ChatFolderTable
|
||||
import org.thoughtcrime.securesms.database.ChatFolderTables.MembershipType
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.ChatFolder as ChatFolderProto
|
||||
|
||||
/**
|
||||
@@ -31,7 +36,7 @@ object ChatFolderProcessor {
|
||||
fun export(db: SignalDatabase, exportState: ExportState, emitter: BackupFrameEmitter) {
|
||||
val folders = db
|
||||
.chatFoldersTable
|
||||
.getChatFolders()
|
||||
.getCurrentChatFolders()
|
||||
.sortedBy { it.position }
|
||||
|
||||
if (folders.isEmpty()) {
|
||||
@@ -66,6 +71,12 @@ object ChatFolderProcessor {
|
||||
}
|
||||
|
||||
fun import(chatFolder: ChatFolderProto, importState: ImportState) {
|
||||
val chatFolderUuid = UuidUtil.parseOrNull(chatFolder.id)
|
||||
if (chatFolderUuid == null) {
|
||||
ImportSkips.chatFolderIdNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
val chatFolderId = SignalDatabase
|
||||
.writableDatabase
|
||||
.insertInto(ChatFolderTable.TABLE_NAME)
|
||||
@@ -76,7 +87,9 @@ object ChatFolderProcessor {
|
||||
ChatFolderTable.SHOW_MUTED to chatFolder.showMutedChats,
|
||||
ChatFolderTable.SHOW_INDIVIDUAL to chatFolder.includeAllIndividualChats,
|
||||
ChatFolderTable.SHOW_GROUPS to chatFolder.includeAllGroupChats,
|
||||
ChatFolderTable.FOLDER_TYPE to chatFolder.folderType.toLocal().value
|
||||
ChatFolderTable.FOLDER_TYPE to chatFolder.folderType.toLocal().value,
|
||||
ChatFolderTable.CHAT_FOLDER_ID to chatFolderUuid.toString(),
|
||||
ChatFolderTable.STORAGE_SERVICE_ID to Base64.encodeWithPadding(StorageSyncHelper.generateKey())
|
||||
)
|
||||
.run()
|
||||
|
||||
@@ -110,7 +123,8 @@ private fun ChatFolderRecord.toBackupFrame(includedRecipientIds: List<Long>, exc
|
||||
else -> throw IllegalStateException("Only ALL or CUSTOM should be in the db")
|
||||
},
|
||||
includedRecipientIds = includedRecipientIds,
|
||||
excludedRecipientIds = excludedRecipientIds
|
||||
excludedRecipientIds = excludedRecipientIds,
|
||||
id = UuidUtil.toByteArray(this.chatFolderId.uuid).toByteString()
|
||||
)
|
||||
|
||||
return Frame(chatFolder = chatFolder)
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.processor
|
||||
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.core.util.insertInto
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.toInt
|
||||
import org.thoughtcrime.securesms.backup.v2.ExportState
|
||||
import org.thoughtcrime.securesms.backup.v2.ImportSkips
|
||||
import org.thoughtcrime.securesms.backup.v2.ImportState
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter
|
||||
@@ -20,7 +22,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.serialize
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import java.lang.IllegalStateException
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import java.time.DayOfWeek
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.NotificationProfile as NotificationProfileProto
|
||||
|
||||
@@ -41,6 +43,12 @@ object NotificationProfileProcessor {
|
||||
}
|
||||
|
||||
fun import(profile: NotificationProfileProto, importState: ImportState) {
|
||||
val notificationProfileUuid = UuidUtil.parseOrNull(profile.id)
|
||||
if (notificationProfileUuid == null) {
|
||||
ImportSkips.notificationProfileIdNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
val profileId = SignalDatabase
|
||||
.writableDatabase
|
||||
.insertInto(NotificationProfileTable.TABLE_NAME)
|
||||
@@ -50,7 +58,8 @@ object NotificationProfileProcessor {
|
||||
NotificationProfileTable.COLOR to (AvatarColor.fromColor(profile.color) ?: AvatarColor.random()).serialize(),
|
||||
NotificationProfileTable.CREATED_AT to profile.createdAtMs,
|
||||
NotificationProfileTable.ALLOW_ALL_CALLS to profile.allowAllCalls.toInt(),
|
||||
NotificationProfileTable.ALLOW_ALL_MENTIONS to profile.allowAllMentions.toInt()
|
||||
NotificationProfileTable.ALLOW_ALL_MENTIONS to profile.allowAllMentions.toInt(),
|
||||
NotificationProfileTable.NOTIFICATION_PROFILE_ID to notificationProfileUuid.toString()
|
||||
)
|
||||
.run()
|
||||
|
||||
@@ -89,6 +98,7 @@ object NotificationProfileProcessor {
|
||||
|
||||
private fun NotificationProfile.toBackupFrame(includeRecipient: (RecipientId) -> Boolean): Frame {
|
||||
val profile = NotificationProfileProto(
|
||||
id = UuidUtil.toByteArray(this.notificationProfileId.uuid).toByteString(),
|
||||
name = this.name,
|
||||
emoji = this.emoji.takeIf { it.isNotBlank() },
|
||||
color = this.color.colorInt(),
|
||||
|
||||
@@ -28,16 +28,11 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -47,6 +42,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.os.BundleCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.signal.core.ui.compose.BottomSheets
|
||||
@@ -56,25 +52,20 @@ import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import org.thoughtcrime.securesms.billing.launchManageBackupsSubscription
|
||||
import org.thoughtcrime.securesms.billing.upgrade.UpgradeToPaidTierBottomSheet
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
|
||||
import org.thoughtcrime.securesms.jobs.BackupRestoreMediaJob
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
/**
|
||||
* Notifies the user of an issue with their backup.
|
||||
*/
|
||||
class BackupAlertBottomSheet : UpgradeToPaidTierBottomSheet() {
|
||||
class BackupAlertBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
|
||||
override val peekHeightPercentage: Float = 0.75f
|
||||
|
||||
@@ -82,8 +73,12 @@ class BackupAlertBottomSheet : UpgradeToPaidTierBottomSheet() {
|
||||
private const val ARG_ALERT = "alert"
|
||||
|
||||
@JvmStatic
|
||||
fun create(backupAlert: BackupAlert): BackupAlertBottomSheet {
|
||||
return BackupAlertBottomSheet().apply {
|
||||
fun create(backupAlert: BackupAlert): DialogFragment {
|
||||
return if (backupAlert is BackupAlert.MediaBackupsAreOff) {
|
||||
MediaBackupsAreOffBottomSheet()
|
||||
} else {
|
||||
BackupAlertBottomSheet()
|
||||
}.apply {
|
||||
arguments = bundleOf(ARG_ALERT to backupAlert)
|
||||
}
|
||||
}
|
||||
@@ -94,34 +89,20 @@ class BackupAlertBottomSheet : UpgradeToPaidTierBottomSheet() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun UpgradeSheetContent(
|
||||
paidBackupType: MessageBackupsType.Paid,
|
||||
freeBackupType: MessageBackupsType.Free,
|
||||
isSubscribeEnabled: Boolean,
|
||||
onSubscribeClick: () -> Unit
|
||||
) {
|
||||
var pricePerMonth by remember { mutableStateOf("-") }
|
||||
val resources = LocalContext.current.resources
|
||||
|
||||
LaunchedEffect(paidBackupType.pricePerMonth) {
|
||||
pricePerMonth = FiatMoneyUtil.format(resources, paidBackupType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
}
|
||||
|
||||
val performPrimaryAction = remember(onSubscribeClick) {
|
||||
createPrimaryAction(onSubscribeClick)
|
||||
override fun SheetContent() {
|
||||
val performPrimaryAction = remember(backupAlert) {
|
||||
createPrimaryAction()
|
||||
}
|
||||
|
||||
BackupAlertSheetContent(
|
||||
backupAlert = backupAlert,
|
||||
isSubscribeEnabled = isSubscribeEnabled,
|
||||
mediaTtl = paidBackupType.mediaTtl,
|
||||
onPrimaryActionClick = performPrimaryAction,
|
||||
onSecondaryActionClick = this::performSecondaryAction
|
||||
)
|
||||
}
|
||||
|
||||
@Stable
|
||||
private fun createPrimaryAction(onSubscribeClick: () -> Unit): () -> Unit = {
|
||||
private fun createPrimaryAction(): () -> Unit = {
|
||||
when (backupAlert) {
|
||||
is BackupAlert.CouldNotCompleteBackup -> {
|
||||
BackupMessagesJob.enqueue()
|
||||
@@ -129,9 +110,7 @@ class BackupAlertBottomSheet : UpgradeToPaidTierBottomSheet() {
|
||||
}
|
||||
|
||||
BackupAlert.FailedToRenew -> launchManageBackupsSubscription()
|
||||
is BackupAlert.MediaBackupsAreOff -> {
|
||||
onSubscribeClick()
|
||||
}
|
||||
is BackupAlert.MediaBackupsAreOff -> error("Use MediaBackupsAreOffBottomSheet instead.")
|
||||
|
||||
BackupAlert.MediaWillBeDeletedToday -> {
|
||||
performFullMediaDownload()
|
||||
@@ -152,7 +131,7 @@ class BackupAlertBottomSheet : UpgradeToPaidTierBottomSheet() {
|
||||
when (backupAlert) {
|
||||
is BackupAlert.CouldNotCompleteBackup -> Unit
|
||||
BackupAlert.FailedToRenew -> Unit
|
||||
is BackupAlert.MediaBackupsAreOff -> Unit
|
||||
is BackupAlert.MediaBackupsAreOff -> error("Use MediaBackupsAreOffBottomSheet instead.")
|
||||
BackupAlert.MediaWillBeDeletedToday -> {
|
||||
displayLastChanceDialog()
|
||||
}
|
||||
@@ -212,11 +191,8 @@ class BackupAlertBottomSheet : UpgradeToPaidTierBottomSheet() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BackupAlertSheetContent(
|
||||
fun BackupAlertSheetContent(
|
||||
backupAlert: BackupAlert,
|
||||
pricePerMonth: String = "",
|
||||
isSubscribeEnabled: Boolean = true,
|
||||
mediaTtl: Duration,
|
||||
onPrimaryActionClick: () -> Unit = {},
|
||||
onSecondaryActionClick: () -> Unit = {}
|
||||
) {
|
||||
@@ -231,7 +207,8 @@ private fun BackupAlertSheetContent(
|
||||
Spacer(modifier = Modifier.size(26.dp))
|
||||
|
||||
when (backupAlert) {
|
||||
BackupAlert.FailedToRenew, is BackupAlert.MediaBackupsAreOff -> {
|
||||
is BackupAlert.MediaBackupsAreOff -> error("Use MediaBackupsAreOffBottomSheet instead.")
|
||||
BackupAlert.FailedToRenew -> {
|
||||
Box {
|
||||
Image(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.image_signal_backups),
|
||||
@@ -276,29 +253,27 @@ private fun BackupAlertSheetContent(
|
||||
)
|
||||
|
||||
BackupAlert.FailedToRenew -> PaymentProcessingBody()
|
||||
is BackupAlert.MediaBackupsAreOff -> MediaBackupsAreOffBody(backupAlert.endOfPeriodSeconds, mediaTtl)
|
||||
BackupAlert.MediaWillBeDeletedToday -> MediaWillBeDeletedTodayBody()
|
||||
is BackupAlert.DiskFull -> DiskFullBody(requiredSpace = backupAlert.requiredSpace)
|
||||
BackupAlert.BackupFailed -> BackupFailedBody()
|
||||
BackupAlert.CouldNotRedeemBackup -> CouldNotRedeemBackup()
|
||||
is BackupAlert.MediaBackupsAreOff -> error("Use MediaBackupsAreOffBottomSheet instead.")
|
||||
}
|
||||
|
||||
val secondaryActionResource = rememberSecondaryActionResource(backupAlert = backupAlert)
|
||||
val padBottom = if (secondaryActionResource > 0) 16.dp else 56.dp
|
||||
|
||||
Buttons.LargeTonal(
|
||||
enabled = isSubscribeEnabled,
|
||||
onClick = onPrimaryActionClick,
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minWidth = 220.dp)
|
||||
.padding(bottom = padBottom)
|
||||
) {
|
||||
Text(text = primaryActionString(backupAlert = backupAlert, pricePerMonth = pricePerMonth))
|
||||
Text(text = primaryActionString(backupAlert = backupAlert))
|
||||
}
|
||||
|
||||
if (secondaryActionResource > 0) {
|
||||
TextButton(
|
||||
enabled = isSubscribeEnabled,
|
||||
onClick = onSecondaryActionClick,
|
||||
modifier = Modifier.padding(bottom = 32.dp)
|
||||
) {
|
||||
@@ -381,28 +356,6 @@ private fun PaymentProcessingBody() {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MediaBackupsAreOffBody(
|
||||
endOfPeriodSeconds: Long,
|
||||
mediaTtl: Duration
|
||||
) {
|
||||
val daysUntilDeletion = remember { endOfPeriodSeconds.days + mediaTtl }.inWholeDays.toInt()
|
||||
|
||||
Text(
|
||||
text = pluralStringResource(id = R.plurals.BackupAlertBottomSheet__your_backup_plan_has_expired, daysUntilDeletion, daysUntilDeletion),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 24.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.BackupAlertBottomSheet__you_can_begin_paying_for_backups_again),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 36.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MediaWillBeDeletedTodayBody() {
|
||||
Text(
|
||||
@@ -473,13 +426,12 @@ private fun titleString(backupAlert: BackupAlert): String {
|
||||
|
||||
@Composable
|
||||
private fun primaryActionString(
|
||||
backupAlert: BackupAlert,
|
||||
pricePerMonth: String
|
||||
backupAlert: BackupAlert
|
||||
): String {
|
||||
return when (backupAlert) {
|
||||
is BackupAlert.CouldNotCompleteBackup -> stringResource(R.string.BackupAlertBottomSheet__back_up_now)
|
||||
BackupAlert.FailedToRenew -> stringResource(R.string.BackupAlertBottomSheet__manage_subscription)
|
||||
is BackupAlert.MediaBackupsAreOff -> stringResource(R.string.BackupAlertBottomSheet__subscribe_for_s_month, pricePerMonth)
|
||||
is BackupAlert.MediaBackupsAreOff -> error("Not supported.")
|
||||
BackupAlert.MediaWillBeDeletedToday -> stringResource(R.string.BackupAlertBottomSheet__download_media_now)
|
||||
is BackupAlert.DiskFull -> stringResource(R.string.BackupAlertBottomSheet__got_it)
|
||||
is BackupAlert.BackupFailed -> stringResource(R.string.BackupAlertBottomSheet__check_for_update)
|
||||
@@ -493,7 +445,7 @@ private fun rememberSecondaryActionResource(backupAlert: BackupAlert): Int {
|
||||
when (backupAlert) {
|
||||
is BackupAlert.CouldNotCompleteBackup -> R.string.BackupAlertBottomSheet__try_later
|
||||
BackupAlert.FailedToRenew -> R.string.BackupAlertBottomSheet__not_now
|
||||
is BackupAlert.MediaBackupsAreOff -> R.string.BackupAlertBottomSheet__not_now
|
||||
is BackupAlert.MediaBackupsAreOff -> error("Not supported.")
|
||||
BackupAlert.MediaWillBeDeletedToday -> R.string.BackupAlertBottomSheet__dont_download_media
|
||||
is BackupAlert.DiskFull -> R.string.BackupAlertBottomSheet__skip_restore
|
||||
is BackupAlert.BackupFailed -> R.string.BackupAlertBottomSheet__learn_more
|
||||
@@ -507,8 +459,7 @@ private fun rememberSecondaryActionResource(backupAlert: BackupAlert): Int {
|
||||
private fun BackupAlertSheetContentPreviewGeneric() {
|
||||
Previews.BottomSheetPreview {
|
||||
BackupAlertSheetContent(
|
||||
backupAlert = BackupAlert.CouldNotCompleteBackup(daysSinceLastBackup = 7),
|
||||
mediaTtl = 60.days
|
||||
backupAlert = BackupAlert.CouldNotCompleteBackup(daysSinceLastBackup = 7)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -518,20 +469,7 @@ private fun BackupAlertSheetContentPreviewGeneric() {
|
||||
private fun BackupAlertSheetContentPreviewPayment() {
|
||||
Previews.BottomSheetPreview {
|
||||
BackupAlertSheetContent(
|
||||
backupAlert = BackupAlert.FailedToRenew,
|
||||
mediaTtl = 60.days
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun BackupAlertSheetContentPreviewMedia() {
|
||||
Previews.BottomSheetPreview {
|
||||
BackupAlertSheetContent(
|
||||
backupAlert = BackupAlert.MediaBackupsAreOff(endOfPeriodSeconds = System.currentTimeMillis().milliseconds.inWholeSeconds),
|
||||
pricePerMonth = "$2.99",
|
||||
mediaTtl = 60.days
|
||||
backupAlert = BackupAlert.FailedToRenew
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -541,8 +479,7 @@ private fun BackupAlertSheetContentPreviewMedia() {
|
||||
private fun BackupAlertSheetContentPreviewDelete() {
|
||||
Previews.BottomSheetPreview {
|
||||
BackupAlertSheetContent(
|
||||
backupAlert = BackupAlert.MediaWillBeDeletedToday,
|
||||
mediaTtl = 60.days
|
||||
backupAlert = BackupAlert.MediaWillBeDeletedToday
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -552,8 +489,7 @@ private fun BackupAlertSheetContentPreviewDelete() {
|
||||
private fun BackupAlertSheetContentPreviewDiskFull() {
|
||||
Previews.BottomSheetPreview {
|
||||
BackupAlertSheetContent(
|
||||
backupAlert = BackupAlert.DiskFull(requiredSpace = "12GB"),
|
||||
mediaTtl = 60.days
|
||||
backupAlert = BackupAlert.DiskFull(requiredSpace = "12GB")
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -563,8 +499,7 @@ private fun BackupAlertSheetContentPreviewDiskFull() {
|
||||
private fun BackupAlertSheetContentPreviewBackupFailed() {
|
||||
Previews.BottomSheetPreview {
|
||||
BackupAlertSheetContent(
|
||||
backupAlert = BackupAlert.BackupFailed,
|
||||
mediaTtl = 60.days
|
||||
backupAlert = BackupAlert.BackupFailed
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -574,8 +509,7 @@ private fun BackupAlertSheetContentPreviewBackupFailed() {
|
||||
private fun BackupAlertSheetContentPreviewCouldNotRedeemBackup() {
|
||||
Previews.BottomSheetPreview {
|
||||
BackupAlertSheetContent(
|
||||
backupAlert = BackupAlert.CouldNotRedeemBackup,
|
||||
mediaTtl = 60.days
|
||||
backupAlert = BackupAlert.CouldNotRedeemBackup
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.os.BundleCompat
|
||||
import org.signal.core.ui.R
|
||||
import org.signal.core.ui.compose.BottomSheets
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.util.gibiBytes
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import org.thoughtcrime.securesms.billing.upgrade.UpgradeToPaidTierBottomSheet
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class MediaBackupsAreOffBottomSheet : UpgradeToPaidTierBottomSheet() {
|
||||
|
||||
companion object {
|
||||
private const val ARG_ALERT = "alert"
|
||||
}
|
||||
|
||||
private val backupAlert: BackupAlert by lazy(LazyThreadSafetyMode.NONE) {
|
||||
BundleCompat.getParcelable(requireArguments(), ARG_ALERT, BackupAlert::class.java)!!
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun UpgradeSheetContent(
|
||||
paidBackupType: MessageBackupsType.Paid,
|
||||
freeBackupType: MessageBackupsType.Free,
|
||||
isSubscribeEnabled: Boolean,
|
||||
onSubscribeClick: () -> Unit
|
||||
) {
|
||||
SheetContent(
|
||||
backupAlert as BackupAlert.MediaBackupsAreOff,
|
||||
paidBackupType,
|
||||
isSubscribeEnabled,
|
||||
onSubscribeClick,
|
||||
onNotNowClick = { dismissAllowingStateLoss() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SheetContent(
|
||||
mediaBackupsAreOff: BackupAlert.MediaBackupsAreOff,
|
||||
paidBackupType: MessageBackupsType.Paid,
|
||||
isSubscribeEnabled: Boolean,
|
||||
onSubscribeClick: () -> Unit,
|
||||
onNotNowClick: () -> Unit
|
||||
) {
|
||||
val resources = LocalContext.current.resources
|
||||
val pricePerMonth = remember(paidBackupType) {
|
||||
FiatMoneyUtil.format(resources, paidBackupType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = dimensionResource(id = R.dimen.gutter))
|
||||
) {
|
||||
BottomSheets.Handle()
|
||||
|
||||
Spacer(modifier = Modifier.size(26.dp))
|
||||
|
||||
Box {
|
||||
Image(
|
||||
imageVector = ImageVector.vectorResource(id = org.thoughtcrime.securesms.R.drawable.image_signal_backups),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.padding(2.dp)
|
||||
)
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(org.thoughtcrime.securesms.R.drawable.symbol_error_circle_fill_24),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.align(Alignment.TopEnd)
|
||||
)
|
||||
}
|
||||
|
||||
val daysUntilDeletion = remember(mediaBackupsAreOff.endOfPeriodSeconds, paidBackupType.mediaTtl) {
|
||||
((System.currentTimeMillis().milliseconds - mediaBackupsAreOff.endOfPeriodSeconds.seconds) + paidBackupType.mediaTtl).inWholeDays.toInt()
|
||||
}
|
||||
|
||||
Text(
|
||||
text = pluralStringResource(id = org.thoughtcrime.securesms.R.plurals.BackupAlertBottomSheet__your_backup_plan_has_expired, daysUntilDeletion, daysUntilDeletion),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 24.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(id = org.thoughtcrime.securesms.R.string.BackupAlertBottomSheet__you_can_begin_paying_for_backups_again),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 36.dp)
|
||||
)
|
||||
|
||||
Buttons.LargeTonal(
|
||||
enabled = isSubscribeEnabled,
|
||||
onClick = onSubscribeClick,
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minWidth = 220.dp)
|
||||
.padding(bottom = 16.dp)
|
||||
) {
|
||||
Text(text = stringResource(org.thoughtcrime.securesms.R.string.BackupAlertBottomSheet__subscribe_for_s_month, pricePerMonth))
|
||||
}
|
||||
|
||||
TextButton(
|
||||
enabled = isSubscribeEnabled,
|
||||
onClick = onNotNowClick,
|
||||
modifier = Modifier.padding(bottom = 32.dp)
|
||||
) {
|
||||
Text(text = stringResource(id = org.thoughtcrime.securesms.R.string.BackupAlertBottomSheet__not_now))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun BackupAlertSheetContentPreviewMedia() {
|
||||
Previews.BottomSheetPreview {
|
||||
SheetContent(
|
||||
mediaBackupsAreOff = BackupAlert.MediaBackupsAreOff(endOfPeriodSeconds = System.currentTimeMillis().milliseconds.inWholeSeconds),
|
||||
paidBackupType = MessageBackupsType.Paid(
|
||||
pricePerMonth = FiatMoney(BigDecimal.ONE, Currency.getInstance("USD")),
|
||||
mediaTtl = 30.days,
|
||||
storageAllowanceBytes = 1.gibiBytes.inWholeBytes
|
||||
),
|
||||
isSubscribeEnabled = true,
|
||||
onSubscribeClick = {},
|
||||
onNotNowClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,6 @@ import org.signal.core.util.billing.BillingPurchaseResult
|
||||
import org.signal.core.util.concurrent.SignalDispatchers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatValue
|
||||
@@ -85,10 +84,15 @@ class MessageBackupsFlowViewModel(
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
val availableBackupTypes = withContext(SignalDispatchers.IO) {
|
||||
BackupRepository.getAvailableBackupsTypes(
|
||||
if (!RemoteConfig.messageBackups) emptyList() else listOf(MessageBackupTier.FREE, MessageBackupTier.PAID)
|
||||
)
|
||||
val availableBackupTypes = try {
|
||||
withContext(SignalDispatchers.IO) {
|
||||
BackupRepository.getAvailableBackupsTypes(
|
||||
if (!RemoteConfig.messageBackups) emptyList() else listOf(MessageBackupTier.FREE, MessageBackupTier.PAID)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to download available backup types.", e)
|
||||
emptyList()
|
||||
}
|
||||
|
||||
internalStateFlow.update {
|
||||
@@ -133,12 +137,12 @@ class MessageBackupsFlowViewModel(
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Failed to handle purchase.", e)
|
||||
InAppPaymentsRepository.handlePipelineError(
|
||||
inAppPaymentId = id,
|
||||
donationErrorSource = DonationErrorSource.BACKUPS,
|
||||
paymentSourceType = PaymentSourceType.GooglePlayBilling,
|
||||
error = e
|
||||
)
|
||||
withContext(SignalDispatchers.IO) {
|
||||
InAppPaymentsRepository.handlePipelineError(
|
||||
inAppPaymentId = id,
|
||||
error = e
|
||||
)
|
||||
}
|
||||
|
||||
internalStateFlow.update {
|
||||
it.copy(
|
||||
|
||||
@@ -39,7 +39,7 @@ fun MessageBackupsKeyEducationScreen(
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
title = "",
|
||||
navigationIconPainter = painterResource(R.drawable.symbol_arrow_left_24),
|
||||
navigationIconPainter = painterResource(R.drawable.symbol_arrow_start_24),
|
||||
onNavigationClick = onNavigationClick
|
||||
) {
|
||||
Column(
|
||||
|
||||
@@ -55,7 +55,7 @@ fun MessageBackupsKeyRecordScreen(
|
||||
|
||||
Scaffolds.Settings(
|
||||
title = "",
|
||||
navigationIconPainter = painterResource(R.drawable.symbol_arrow_left_24),
|
||||
navigationIconPainter = painterResource(R.drawable.symbol_arrow_start_24),
|
||||
onNavigationClick = onNavigationClick
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
|
||||
@@ -87,7 +87,7 @@ fun MessageBackupsKeyVerifyScreen(
|
||||
|
||||
Scaffolds.Settings(
|
||||
title = stringResource(R.string.MessageBackupsKeyVerifyScreen__confirm_your_backup_key),
|
||||
navigationIconPainter = painterResource(R.drawable.symbol_arrow_left_24),
|
||||
navigationIconPainter = painterResource(R.drawable.symbol_arrow_start_24),
|
||||
onNavigationClick = onNavigationClick
|
||||
) { paddingValues ->
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.fonts.SignalSymbols
|
||||
import org.thoughtcrime.securesms.fonts.SignalSymbols.SignalSymbol
|
||||
import org.thoughtcrime.securesms.fonts.SignalSymbols.signalSymbolText
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.util.ByteUnit
|
||||
import java.math.BigDecimal
|
||||
@@ -82,7 +82,7 @@ fun MessageBackupsTypeSelectionScreen(
|
||||
Scaffolds.Settings(
|
||||
title = "",
|
||||
onNavigationClick = onNavigationClick,
|
||||
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_left_24)
|
||||
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_start_24)
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -260,11 +260,10 @@ fun MessageBackupsTypeBlock(
|
||||
) {
|
||||
if (isCurrent) {
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
SignalSymbol(weight = SignalSymbols.Weight.REGULAR, glyph = SignalSymbols.Glyph.CHECKMARK)
|
||||
append(" ")
|
||||
append(stringResource(R.string.MessageBackupsTypeSelectionScreen__current_plan))
|
||||
},
|
||||
text = signalSymbolText(
|
||||
text = stringResource(R.string.MessageBackupsTypeSelectionScreen__current_plan),
|
||||
glyphStart = SignalSymbols.Glyph.CHECK
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.padding(bottom = 12.dp)
|
||||
|
||||
@@ -15,7 +15,6 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.getParcelableCompat
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
@@ -58,10 +57,6 @@ class GiftFlowConfirmationFragment :
|
||||
EmojiSearchFragment.Callback,
|
||||
InAppPaymentCheckoutDelegate.Callback {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(GiftFlowConfirmationFragment::class.java)
|
||||
}
|
||||
|
||||
private val viewModel: GiftFlowViewModel by viewModels(
|
||||
ownerProducer = { requireActivity() }
|
||||
)
|
||||
@@ -118,7 +113,7 @@ class GiftFlowConfirmationFragment :
|
||||
lifecycleDisposable += viewModel.insertInAppPayment().subscribe { inAppPayment ->
|
||||
findNavController().safeNavigate(
|
||||
GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToGatewaySelectorBottomSheet(
|
||||
inAppPayment
|
||||
inAppPayment.id
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -266,8 +261,7 @@ class GiftFlowConfirmationFragment :
|
||||
findNavController().safeNavigate(
|
||||
GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToStripePaymentInProgressFragment(
|
||||
InAppPaymentProcessorAction.PROCESS_NEW_IN_APP_PAYMENT,
|
||||
inAppPayment,
|
||||
inAppPayment.type
|
||||
inAppPayment.id
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -276,15 +270,14 @@ class GiftFlowConfirmationFragment :
|
||||
findNavController().safeNavigate(
|
||||
GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToPaypalPaymentInProgressFragment(
|
||||
InAppPaymentProcessorAction.PROCESS_NEW_IN_APP_PAYMENT,
|
||||
inAppPayment,
|
||||
inAppPayment.type
|
||||
inAppPayment.id
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun navigateToCreditCardForm(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
findNavController().safeNavigate(
|
||||
GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToCreditCardFragment(inAppPayment)
|
||||
GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToCreditCardFragment(inAppPayment.id)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.paged.ChatType;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
@@ -38,6 +39,7 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
|
||||
private BlockedUsersViewModel viewModel;
|
||||
private View container;
|
||||
|
||||
private final LifecycleDisposable lifecycleDisposable = new LifecycleDisposable();
|
||||
|
||||
@@ -57,7 +59,7 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
ContactFilterView contactFilterView = findViewById(R.id.contact_filter_edit_text);
|
||||
View container = findViewById(R.id.fragment_container);
|
||||
container = findViewById(R.id.fragment_container);
|
||||
|
||||
toolbar.setNavigationOnClickListener(unused -> onBackPressed());
|
||||
contactFilterView.setOnFilterChangedListener(query -> {
|
||||
@@ -99,11 +101,41 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
|
||||
|
||||
@Override
|
||||
public void onBeforeContactSelected(boolean isFromUnknownSearchKey, @NonNull Optional<RecipientId> recipientId, String number, @NonNull Optional<ChatType> chatType, @NonNull Consumer<Boolean> callback) {
|
||||
final String displayName = recipientId.map(id -> Recipient.resolved(id).getDisplayName(this)).orElse(number);
|
||||
Optional<Recipient> resolvedRecipient = recipientId.map(Recipient::resolved);
|
||||
|
||||
AlertDialog confirmationDialog = new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.BlockedUsersActivity__block_user)
|
||||
.setMessage(getString(R.string.BlockedUserActivity__s_will_not_be_able_to, displayName))
|
||||
final String displayName = resolvedRecipient
|
||||
.map(r -> r.getDisplayName(this))
|
||||
.orElse(number);
|
||||
|
||||
boolean isSelf = resolvedRecipient
|
||||
.map(Recipient::isSelf)
|
||||
.orElseGet(() -> Optional.ofNullable(number)
|
||||
.map(Recipient::external)
|
||||
.map(Recipient::isSelf)
|
||||
.orElse(false));
|
||||
|
||||
if (isSelf) {
|
||||
Snackbar.make(container, getString(R.string.BlockedUsersActivity__cannot_block_yourself), Snackbar.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
|
||||
|
||||
if (resolvedRecipient.isPresent() && resolvedRecipient.get().isGroup()) {
|
||||
Recipient recipient = resolvedRecipient.get();
|
||||
if (SignalDatabase.groups().isActive(recipient.requireGroupId())) {
|
||||
builder.setTitle(getString(R.string.BlockUnblockDialog_block_and_leave_s, displayName));
|
||||
builder.setMessage(R.string.BlockUnblockDialog_you_will_no_longer_receive_messages_or_updates);
|
||||
} else {
|
||||
builder.setTitle(getString(R.string.BlockUnblockDialog_block_s, displayName));
|
||||
builder.setMessage(R.string.BlockUnblockDialog_group_members_wont_be_able_to_add_you);
|
||||
}
|
||||
} else {
|
||||
builder.setTitle(R.string.BlockedUsersActivity__block_user);
|
||||
builder.setMessage(getString(R.string.BlockedUserActivity__s_will_not_be_able_to, displayName));
|
||||
}
|
||||
|
||||
AlertDialog confirmationDialog = builder
|
||||
.setPositiveButton(R.string.BlockedUsersActivity__block, (dialog, which) -> {
|
||||
if (recipientId.isPresent()) {
|
||||
viewModel.block(recipientId.get());
|
||||
|
||||
@@ -18,6 +18,8 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -242,6 +244,7 @@ private fun CreateCallLinkBottomSheetContent(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentSize(Alignment.Center)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
BottomSheets.Handle(modifier = Modifier.align(Alignment.CenterHorizontally))
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -287,7 +289,11 @@ private fun CallLinkDetails(
|
||||
return@Settings
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.padding(paddingValues)) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
SignalCallRow(
|
||||
callLink = state.callLink,
|
||||
callLinkPeekInfo = state.peekInfo,
|
||||
|
||||
@@ -9,20 +9,15 @@ import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.app.SharedElementCallback
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.transition.TransitionInflater
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.kotlin.Flowables
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.DimensionUnit
|
||||
@@ -31,9 +26,7 @@ import org.signal.core.util.concurrent.addTo
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.MainNavigator
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar
|
||||
import org.thoughtcrime.securesms.calls.links.details.CallLinkDetailsActivity
|
||||
import org.thoughtcrime.securesms.calls.new.NewCallActivity
|
||||
import org.thoughtcrime.securesms.components.ProgressCardDialogFragment
|
||||
import org.thoughtcrime.securesms.components.ScrollToPositionDelegate
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
@@ -50,19 +43,19 @@ import org.thoughtcrime.securesms.conversationlist.chatfilter.FilterLerp
|
||||
import org.thoughtcrime.securesms.conversationlist.chatfilter.FilterPullState
|
||||
import org.thoughtcrime.securesms.databinding.CallLogFragmentBinding
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.main.MainNavigationDestination
|
||||
import org.thoughtcrime.securesms.main.MainNavigationListLocation
|
||||
import org.thoughtcrime.securesms.main.MainNavigationViewModel
|
||||
import org.thoughtcrime.securesms.main.MainToolbarMode
|
||||
import org.thoughtcrime.securesms.main.MainToolbarViewModel
|
||||
import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder
|
||||
import org.thoughtcrime.securesms.main.SnackbarState
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.doAfterNextLayout
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import java.util.Objects
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Call Log tab.
|
||||
@@ -89,12 +82,10 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
private lateinit var signalBottomActionBarController: SignalBottomActionBarController
|
||||
|
||||
private val viewModel: CallLogViewModel by activityViewModels()
|
||||
private val tabsViewModel: ConversationListTabsViewModel by viewModels(ownerProducer = { requireActivity() })
|
||||
private val mainToolbarViewModel: MainToolbarViewModel by activityViewModels()
|
||||
private val mainNavigationViewModel: MainNavigationViewModel by activityViewModels()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
initializeSharedElementTransition()
|
||||
|
||||
viewLifecycleOwner.lifecycle.addObserver(conversationUpdateTick)
|
||||
viewLifecycleOwner.lifecycle.addObserver(viewModel.callLogPeekHelper)
|
||||
|
||||
@@ -150,9 +141,6 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
this.callLogAdapter = callLogAdapter
|
||||
|
||||
requireListener<Material3OnScrollHelperBinder>().bindScrollHelper(binding.recycler, viewLifecycleOwner)
|
||||
binding.fab.setOnClickListener {
|
||||
startActivity(NewCallActivity.createIntent(requireContext()))
|
||||
}
|
||||
|
||||
binding.pullView.setPillText(R.string.CallLogFragment__filtered_by_missed)
|
||||
|
||||
@@ -180,7 +168,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (!closeSearchIfOpen()) {
|
||||
tabsViewModel.onChatsSelected()
|
||||
mainNavigationViewModel.onChatsSelected()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,29 +192,9 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
callLogAdapter?.onTimestampTick()
|
||||
}
|
||||
|
||||
private fun initializeSharedElementTransition() {
|
||||
ViewCompat.setTransitionName(binding.fab, "new_convo_fab")
|
||||
ViewCompat.setTransitionName(binding.fabSharedElementTarget, "camera_fab")
|
||||
|
||||
sharedElementEnterTransition = TransitionInflater.from(requireContext()).inflateTransition(R.transition.change_transform_fabs)
|
||||
setEnterSharedElementCallback(object : SharedElementCallback() {
|
||||
override fun onSharedElementStart(sharedElementNames: MutableList<String>?, sharedElements: MutableList<View>?, sharedElementSnapshots: MutableList<View>?) {
|
||||
if (sharedElementNames?.contains("camera_fab") == true) {
|
||||
this@CallLogFragment.binding.fab.setImageResource(R.drawable.symbol_edit_24)
|
||||
disposables += Single.timer(200, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy {
|
||||
this@CallLogFragment.binding.fab.setImageResource(R.drawable.symbol_phone_plus_24)
|
||||
this@CallLogFragment.binding.fabSharedElementTarget.alpha = 0f
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun initializeTapToScrollToTop(scrollToPositionDelegate: ScrollToPositionDelegate) {
|
||||
disposables += tabsViewModel.tabClickEvents
|
||||
.filter { it == MainNavigationDestination.CALLS }
|
||||
disposables += mainNavigationViewModel.tabClickEvents
|
||||
.filter { it == MainNavigationListLocation.CALLS }
|
||||
.subscribeBy(onNext = {
|
||||
scrollToPositionDelegate.resetScrollPosition()
|
||||
})
|
||||
@@ -363,14 +331,22 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
|
||||
override fun onStartAudioCallClicked(recipient: Recipient) {
|
||||
CommunicationActions.startVoiceCall(this, recipient) {
|
||||
YouAreAlreadyInACallSnackbar.show(requireView())
|
||||
mainNavigationViewModel.setSnackbar(
|
||||
SnackbarState(
|
||||
getString(R.string.CommunicationActions__you_are_already_in_a_call)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartVideoCallClicked(recipient: Recipient, canUserBeginCall: Boolean) {
|
||||
if (canUserBeginCall) {
|
||||
CommunicationActions.startVideoCall(this, recipient) {
|
||||
YouAreAlreadyInACallSnackbar.show(requireView())
|
||||
mainNavigationViewModel.setSnackbar(
|
||||
SnackbarState(
|
||||
getString(R.string.CommunicationActions__you_are_already_in_a_call)
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
ConversationDialogs.displayCannotStartGroupCallDueToPermissionsDialog(requireContext())
|
||||
@@ -461,13 +437,12 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
}
|
||||
|
||||
CallLogDeletionResult.Success -> {
|
||||
Snackbar
|
||||
.make(
|
||||
binding.root,
|
||||
snackbarMessage,
|
||||
Snackbar.LENGTH_SHORT
|
||||
mainNavigationViewModel.setSnackbar(
|
||||
SnackbarState(
|
||||
message = snackbarMessage,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
.show()
|
||||
)
|
||||
}
|
||||
|
||||
is CallLogDeletionResult.UnknownFailure -> {
|
||||
@@ -488,14 +463,12 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
val actionMode = (requireActivity() as AppCompatActivity).startSupportActionMode(callback)
|
||||
requireListener<Callback>().onMultiSelectStarted()
|
||||
signalBottomActionBarController.setVisibility(true)
|
||||
binding.fab.visible = false
|
||||
return actionMode
|
||||
}
|
||||
|
||||
override fun onActionModeWillEnd() {
|
||||
requireListener<Callback>().onMultiSelectFinished()
|
||||
signalBottomActionBarController.setVisibility(false)
|
||||
binding.fab.visible = true
|
||||
}
|
||||
|
||||
override fun getResources(): Resources = resources
|
||||
|
||||
@@ -46,7 +46,7 @@ import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||
import org.thoughtcrime.securesms.util.NameUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -62,12 +62,13 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
|
||||
private final RequestListener<Drawable> redownloadRequestListener = new RedownloadRequestListener();
|
||||
|
||||
private int size;
|
||||
private boolean inverted;
|
||||
private OnClickListener listener;
|
||||
private boolean blurred;
|
||||
private ChatColors chatColors;
|
||||
private FixedSizeTarget fixedSizeTarget;
|
||||
private int size;
|
||||
private boolean inverted;
|
||||
private OnClickListener listener;
|
||||
private boolean blurred;
|
||||
private ChatColors chatColors;
|
||||
private String initials;
|
||||
private FixedSizeTarget fixedSizeTarget;
|
||||
|
||||
private @Nullable RecipientContactPhoto recipientContactPhoto;
|
||||
private @NonNull Drawable unknownRecipientDrawable;
|
||||
@@ -100,6 +101,7 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
unknownRecipientDrawable = new FallbackAvatarDrawable(context, new FallbackAvatar.Resource.Person(AvatarColor.UNKNOWN)).circleCrop();
|
||||
blurred = false;
|
||||
chatColors = null;
|
||||
initials = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -123,12 +125,7 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
* Shows self as the actual profile picture.
|
||||
*/
|
||||
public void setRecipient(@NonNull Recipient recipient, boolean quickContactEnabled) {
|
||||
if (recipient.isSelf()) {
|
||||
setAvatar(Glide.with(this), null, quickContactEnabled);
|
||||
AvatarUtil.loadIconIntoImageView(recipient, this);
|
||||
} else {
|
||||
setAvatar(Glide.with(this), recipient, quickContactEnabled);
|
||||
}
|
||||
setAvatar(Glide.with(this), recipient, quickContactEnabled, recipient.isSelf());
|
||||
}
|
||||
|
||||
public AvatarOptions.Builder buildOptions() {
|
||||
@@ -177,10 +174,12 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
|
||||
boolean shouldBlur = recipient.getShouldBlurAvatar();
|
||||
ChatColors chatColors = recipient.getChatColors();
|
||||
String initials = NameUtil.getAbbreviation(recipient.getDisplayName(getContext()));
|
||||
|
||||
if (!photo.equals(recipientContactPhoto) || shouldBlur != blurred || !Objects.equals(chatColors, this.chatColors)) {
|
||||
if (!photo.equals(recipientContactPhoto) || shouldBlur != blurred || !Objects.equals(chatColors, this.chatColors) || !Objects.equals(initials, this.initials)) {
|
||||
requestManager.clear(this);
|
||||
this.chatColors = chatColors;
|
||||
this.chatColors = chatColors;
|
||||
this.initials = initials;
|
||||
recipientContactPhoto = photo;
|
||||
|
||||
FallbackAvatarProvider activeFallbackPhotoProvider = this.fallbackAvatarProvider;
|
||||
@@ -189,7 +188,7 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
@Override
|
||||
public @NonNull FallbackAvatar getFallbackAvatar(@NonNull Recipient recipient) {
|
||||
if (recipient.isSelf()) {
|
||||
return new FallbackAvatar.Resource.Person(recipient.getAvatarColor());
|
||||
return FallbackAvatar.forTextOrDefault(recipient.getDisplayName(getContext()), recipient.getAvatarColor());
|
||||
}
|
||||
|
||||
return FallbackAvatarProvider.super.getFallbackAvatar(recipient);
|
||||
|
||||
@@ -15,12 +15,10 @@ import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
|
||||
import com.airbnb.lottie.LottieAnimationView;
|
||||
import com.airbnb.lottie.LottieProperty;
|
||||
@@ -283,9 +281,9 @@ public class ConversationItemFooter extends ConstraintLayout {
|
||||
|
||||
private void presentDate(@NonNull MessageRecord messageRecord, @NonNull Locale locale, @NonNull ConversationItemDisplayMode displayMode) {
|
||||
dateView.forceLayout();
|
||||
if (MessageRecordUtil.isScheduled(messageRecord)) {
|
||||
if (MessageRecordUtil.isScheduled(messageRecord)) {
|
||||
dateView.setText(DateUtils.getOnlyTimeString(getContext(), ((MmsMessageRecord) messageRecord).getScheduledDate()));
|
||||
} else if (messageRecord.isMediaPending()) {
|
||||
} else if (messageRecord.isMediaPending() && messageRecord.isOutgoing() && !messageRecord.isSent()) {
|
||||
dateView.setText(null);
|
||||
} else if (messageRecord.isFailed()) {
|
||||
int errorMsg;
|
||||
|
||||
@@ -187,7 +187,7 @@ public class DocumentView extends FrameLayout {
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (!slide.isPendingDownload() && !slide.isInProgress() && viewListener != null) {
|
||||
if (slide.hasDocument() && slide.getUri()!=null && viewListener != null) {
|
||||
viewListener.onClick(v, slide);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,6 +220,9 @@ public class InputPanel extends ConstraintLayout
|
||||
@NonNull QuoteModel.Type quoteType)
|
||||
{
|
||||
this.quoteView.setQuote(requestManager, id, author, body, false, attachments, null, quoteType);
|
||||
if (listener != null) {
|
||||
this.quoteView.setOnClickListener(v -> listener.onQuoteClicked(id, author.getId()));
|
||||
}
|
||||
|
||||
int originalHeight = this.quoteView.getVisibility() == VISIBLE ? this.quoteView.getMeasuredHeight()
|
||||
: 0;
|
||||
@@ -784,13 +787,17 @@ public class InputPanel extends ConstraintLayout
|
||||
}
|
||||
|
||||
private void updateVisibility() {
|
||||
if (hideForGroupState || hideForBlockedState || hideForSearch || hideForSelection || hideForMessageRequestState) {
|
||||
if (isHidden()) {
|
||||
setVisibility(GONE);
|
||||
} else {
|
||||
setVisibility(VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isHidden() {
|
||||
return hideForGroupState || hideForBlockedState || hideForSearch || hideForSelection || hideForMessageRequestState;
|
||||
}
|
||||
|
||||
public @Nullable MessageRecord getEditMessage() {
|
||||
return messageToEdit;
|
||||
}
|
||||
@@ -813,6 +820,7 @@ public class InputPanel extends ConstraintLayout
|
||||
void onStickerSuggestionSelected(@NonNull StickerRecord sticker);
|
||||
void onQuoteChanged(long id, @NonNull RecipientId author);
|
||||
void onQuoteCleared();
|
||||
void onQuoteClicked(long quoteId, RecipientId authorId);
|
||||
void onEnterEditMode();
|
||||
void onExitEditMode();
|
||||
void onQuickCameraToggleClicked();
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.Surface
|
||||
import android.view.View
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.Guideline
|
||||
import androidx.core.content.withStyledAttributes
|
||||
@@ -63,25 +64,63 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor(
|
||||
private val displayMetrics = DisplayMetrics()
|
||||
private var overridingKeyboard: Boolean = false
|
||||
private var previousKeyboardHeight: Int = 0
|
||||
private var applyRootInsets: Boolean = false
|
||||
|
||||
private var insets: WindowInsetsCompat? = null
|
||||
private var windowTypes: Int = InsetAwareConstraintLayout.windowTypes
|
||||
|
||||
private val windowInsetsListener = androidx.core.view.OnApplyWindowInsetsListener { _, insets ->
|
||||
this.insets = insets
|
||||
applyInsets(windowInsets = insets.getInsets(windowTypes), keyboardInsets = insets.getInsets(keyboardType))
|
||||
insets
|
||||
}
|
||||
|
||||
val isKeyboardShowing: Boolean
|
||||
get() = previousKeyboardHeight > 0
|
||||
|
||||
init {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(this) { _, windowInsetsCompat ->
|
||||
applyInsets(windowInsets = windowInsetsCompat.getInsets(windowTypes), keyboardInsets = windowInsetsCompat.getInsets(keyboardType))
|
||||
windowInsetsCompat
|
||||
}
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(insetTarget(), windowInsetsListener)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(insetTarget(), null)
|
||||
}
|
||||
|
||||
init {
|
||||
if (attrs != null) {
|
||||
context.withStyledAttributes(attrs, R.styleable.InsetAwareConstraintLayout) {
|
||||
applyRootInsets = getBoolean(R.styleable.InsetAwareConstraintLayout_applyRootInsets, false)
|
||||
|
||||
if (getBoolean(R.styleable.InsetAwareConstraintLayout_animateKeyboardChanges, false)) {
|
||||
ViewCompat.setWindowInsetsAnimationCallback(this@InsetAwareConstraintLayout, keyboardAnimator)
|
||||
ViewCompat.setWindowInsetsAnimationCallback(insetTarget(), keyboardAnimator)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun insetTarget(): View = if (applyRootInsets) rootView else this
|
||||
|
||||
/**
|
||||
* Specifies whether or not window insets should be accounted for when applying
|
||||
* insets. This is useful when choosing whether to display the content in this
|
||||
* constraint layout as a full-window view or as a framed view.
|
||||
*/
|
||||
fun setUseWindowTypes(useWindowTypes: Boolean) {
|
||||
windowTypes = if (useWindowTypes) {
|
||||
InsetAwareConstraintLayout.windowTypes
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
if (insets != null) {
|
||||
applyInsets(insets!!.getInsets(windowTypes), insets!!.getInsets(keyboardType))
|
||||
}
|
||||
}
|
||||
|
||||
fun addKeyboardStateListener(listener: KeyboardStateListener) {
|
||||
keyboardStateListeners += listener
|
||||
}
|
||||
@@ -115,10 +154,12 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor(
|
||||
|
||||
if (keyboardInsets.bottom > 0) {
|
||||
setKeyboardHeight(keyboardInsets.bottom)
|
||||
if (!keyboardAnimator.animating) {
|
||||
keyboardGuideline?.setGuidelineEnd(keyboardInsets.bottom)
|
||||
} else {
|
||||
keyboardAnimator.endingGuidelineEnd = keyboardInsets.bottom
|
||||
if (!overridingKeyboard) {
|
||||
if (!keyboardAnimator.animating) {
|
||||
keyboardGuideline?.setGuidelineEnd(keyboardInsets.bottom)
|
||||
} else {
|
||||
keyboardAnimator.endingGuidelineEnd = keyboardInsets.bottom
|
||||
}
|
||||
}
|
||||
} else if (!overridingKeyboard) {
|
||||
if (!keyboardAnimator.animating) {
|
||||
@@ -153,6 +194,7 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor(
|
||||
protected fun resetKeyboardGuideline() {
|
||||
clearKeyboardGuidelineOverride()
|
||||
keyboardGuideline?.setGuidelineEnd(navigationBarGuideline.guidelineEnd)
|
||||
keyboardAnimator.endingGuidelineEnd = navigationBarGuideline.guidelineEnd
|
||||
}
|
||||
|
||||
private fun getKeyboardHeight(): Int {
|
||||
|
||||
@@ -47,6 +47,7 @@ import org.thoughtcrime.securesms.components.transfercontrols.TransferControlVie
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
||||
@@ -488,7 +489,12 @@ public class ThumbnailView extends FrameLayout {
|
||||
|
||||
transferControlViewStub.setVisibility(View.GONE);
|
||||
|
||||
RequestBuilder<Drawable> request = requestManager.load(new DecryptableUri(uri))
|
||||
Object glideModel = uri;
|
||||
if (PartAuthority.isLocalUri(uri)) {
|
||||
glideModel = new DecryptableUri(uri);
|
||||
}
|
||||
|
||||
RequestBuilder<Drawable> request = requestManager.load(glideModel)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE)
|
||||
.listener(listener);
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.exifinterface.media.ExifInterface;
|
||||
|
||||
import com.bumptech.glide.RequestManager;
|
||||
@@ -80,6 +81,12 @@ public class ZoomingImageView extends FrameLayout {
|
||||
this.subsamplingImageView.setOnClickListener(v -> ZoomingImageView.this.callOnClick());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
|
||||
this.gifView.setOnLongClickListener(l);
|
||||
this.subsamplingImageView.setOnLongClickListener(l);
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public void setImageUri(@NonNull RequestManager requestManager, @NonNull Uri uri, @NonNull String contentType, @NonNull Runnable onMediaReady) {
|
||||
if (MediaUtil.isGif(contentType)) {
|
||||
|
||||
@@ -112,6 +112,12 @@ public class EmojiPageView extends RecyclerView implements VariationSelectorList
|
||||
addItemDecoration(new EmojiItemDecoration(allowVariations, drawable));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchTouchEvent(MotionEvent ev) {
|
||||
getParent().requestDisallowInterceptTouchEvent(true);
|
||||
return super.dispatchTouchEvent(ev);
|
||||
}
|
||||
|
||||
public void presentForEmojiKeyboard() {
|
||||
setPadding(getPaddingLeft(),
|
||||
getPaddingTop(),
|
||||
|
||||
@@ -181,7 +181,7 @@ private fun AppSettingsContent(
|
||||
Scaffolds.Settings(
|
||||
title = stringResource(R.string.text_secure_normal__menu_settings),
|
||||
navigationContentDescription = stringResource(R.string.CallScreenTopBar__go_back),
|
||||
navigationIconPainter = painterResource(R.drawable.symbol_arrow_left_24),
|
||||
navigationIconPainter = painterResource(R.drawable.symbol_arrow_start_24),
|
||||
onNavigationClick = callbacks::onNavigationClick
|
||||
) { contentPadding ->
|
||||
Column(
|
||||
|
||||
@@ -125,6 +125,7 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences_chats__transfer_account),
|
||||
summary = DSLSettingsText.from(R.string.preferences_chats__transfer_account_to_a_new_android_device),
|
||||
isEnabled = state.isDeprecatedOrUnregistered(),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_accountSettingsFragment_to_oldDeviceTransferActivity)
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ private fun BackupsSettingsContent(
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
title = stringResource(R.string.preferences_chats__backups),
|
||||
navigationIconPainter = painterResource(R.drawable.symbol_arrow_left_24),
|
||||
navigationIconPainter = painterResource(R.drawable.symbol_arrow_start_24),
|
||||
onNavigationClick = onNavigationClick
|
||||
) { paddingValues ->
|
||||
LazyColumn(
|
||||
|
||||
@@ -16,6 +16,8 @@ import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.rx3.asFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.concurrent.SignalDispatchers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
@@ -48,30 +50,34 @@ class BackupsSettingsViewModel : ViewModel() {
|
||||
.filter { it }
|
||||
.drop(1)
|
||||
.collect {
|
||||
refreshState()
|
||||
Log.d(TAG, "Triggering refresh from internet reconnect.")
|
||||
loadEnabledState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshState() {
|
||||
Log.d(TAG, "Refreshing state.")
|
||||
loadEnabledState()
|
||||
Log.d(TAG, "Refreshing state from manual call.")
|
||||
viewModelScope.launch {
|
||||
loadEnabledState()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadEnabledState() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
private suspend fun loadEnabledState() {
|
||||
withContext(SignalDispatchers.IO) {
|
||||
if (!RemoteConfig.messageBackups || !AppDependencies.billingApi.isApiAvailable()) {
|
||||
Log.w(TAG, "Paid backups are not available on this device.")
|
||||
internalStateFlow.update { it.copy(enabledState = BackupsSettingsState.EnabledState.NotAvailable, showBackupTierInternalOverride = false) }
|
||||
return@launch
|
||||
}
|
||||
} else {
|
||||
val enabledState = when (SignalStore.backup.backupTier) {
|
||||
MessageBackupTier.FREE -> getEnabledStateForFreeTier()
|
||||
MessageBackupTier.PAID -> getEnabledStateForPaidTier()
|
||||
null -> getEnabledStateForNoTier()
|
||||
}
|
||||
|
||||
val enabledState = when (SignalStore.backup.backupTier) {
|
||||
MessageBackupTier.FREE -> getEnabledStateForFreeTier()
|
||||
MessageBackupTier.PAID -> getEnabledStateForPaidTier()
|
||||
null -> getEnabledStateForNoTier()
|
||||
Log.d(TAG, "Found enabled state $enabledState. Updating UI state.")
|
||||
internalStateFlow.update { it.copy(enabledState = enabledState, showBackupTierInternalOverride = RemoteConfig.internalUser, backupTierInternalOverride = SignalStore.backup.backupTierInternalOverride) }
|
||||
}
|
||||
|
||||
internalStateFlow.update { it.copy(enabledState = enabledState, showBackupTierInternalOverride = RemoteConfig.internalUser, backupTierInternalOverride = SignalStore.backup.backupTierInternalOverride) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,10 +88,14 @@ class BackupsSettingsViewModel : ViewModel() {
|
||||
|
||||
private suspend fun getEnabledStateForFreeTier(): BackupsSettingsState.EnabledState {
|
||||
return try {
|
||||
Log.d(TAG, "Attempting to grab enabled state for free tier.")
|
||||
val backupType = BackupRepository.getBackupsType(MessageBackupTier.FREE)!!
|
||||
|
||||
Log.d(TAG, "Retrieved backup type. Returning active state...")
|
||||
BackupsSettingsState.EnabledState.Active(
|
||||
expiresAt = 0.seconds,
|
||||
lastBackupAt = SignalStore.backup.lastBackupTime.milliseconds,
|
||||
type = BackupRepository.getBackupsType(MessageBackupTier.FREE)!!
|
||||
type = backupType
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to build enabled state.", e)
|
||||
@@ -95,8 +105,13 @@ class BackupsSettingsViewModel : ViewModel() {
|
||||
|
||||
private suspend fun getEnabledStateForPaidTier(): BackupsSettingsState.EnabledState {
|
||||
return try {
|
||||
Log.d(TAG, "Attempting to grab enabled state for paid tier.")
|
||||
val backupType = BackupRepository.getBackupsType(MessageBackupTier.PAID) as MessageBackupsType.Paid
|
||||
|
||||
Log.d(TAG, "Retrieved backup type. Grabbing active subscription...")
|
||||
val activeSubscription = RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP).getOrThrow()
|
||||
|
||||
Log.d(TAG, "Retrieved subscription. Active? ${activeSubscription.isActive}")
|
||||
if (activeSubscription.isActive) {
|
||||
BackupsSettingsState.EnabledState.Active(
|
||||
expiresAt = activeSubscription.activeSubscription.endOfCurrentPeriod.seconds,
|
||||
@@ -120,6 +135,7 @@ class BackupsSettingsViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
private fun getEnabledStateForNoTier(): BackupsSettingsState.EnabledState {
|
||||
Log.d(TAG, "Grabbing enabled state for no tier.")
|
||||
return if (SignalStore.uiHints.hasEverEnabledRemoteBackups) {
|
||||
BackupsSettingsState.EnabledState.Inactive
|
||||
} else {
|
||||
|
||||
@@ -31,8 +31,6 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.AlertDialogDefaults
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
@@ -99,7 +97,7 @@ import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.MessageBackupsCheckoutLauncher.createBackupsCheckoutLauncher
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.fonts.SignalSymbols
|
||||
import org.thoughtcrime.securesms.fonts.SignalSymbols.SignalSymbol
|
||||
import org.thoughtcrime.securesms.fonts.SignalSymbols.signalSymbolText
|
||||
import org.thoughtcrime.securesms.help.HelpFragment
|
||||
import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
@@ -363,7 +361,7 @@ private fun RemoteBackupsSettingsContent(
|
||||
Scaffolds.Settings(
|
||||
title = stringResource(id = R.string.RemoteBackupsSettingsFragment__signal_backups),
|
||||
onNavigationClick = contentCallbacks::onNavigationClick,
|
||||
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_left_24),
|
||||
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_start_24),
|
||||
snackbarHost = {
|
||||
Snackbars.Host(snackbarHostState = snackbarHostState)
|
||||
}
|
||||
@@ -687,14 +685,10 @@ private fun BackupCard(
|
||||
}
|
||||
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
if (backupState.isActive()) {
|
||||
SignalSymbol(SignalSymbols.Weight.REGULAR, SignalSymbols.Glyph.CHECKMARK)
|
||||
append(" ")
|
||||
}
|
||||
|
||||
append(title)
|
||||
},
|
||||
text = signalSymbolText(
|
||||
text = title,
|
||||
glyphStart = if (backupState.isActive()) SignalSymbols.Glyph.CHECK else null
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
@@ -1183,8 +1177,8 @@ private fun CircularProgressDialog(
|
||||
)
|
||||
) {
|
||||
Surface(
|
||||
shape = AlertDialogDefaults.shape,
|
||||
color = AlertDialogDefaults.containerColor
|
||||
shape = Dialogs.Defaults.shape,
|
||||
color = Dialogs.Defaults.containerColor
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
@@ -1206,17 +1200,16 @@ private fun BackupFrequencyDialog(
|
||||
onSelected: (BackupFrequency) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
AlertDialog(
|
||||
BasicAlertDialog(
|
||||
onDismissRequest = onDismiss
|
||||
) {
|
||||
Surface {
|
||||
Surface(
|
||||
color = Dialogs.Defaults.containerColor,
|
||||
shape = Dialogs.Defaults.shape,
|
||||
shadowElevation = Dialogs.Defaults.TonalElevation
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = AlertDialogDefaults.containerColor,
|
||||
shape = AlertDialogDefaults.shape
|
||||
)
|
||||
.fillMaxWidth()
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_frequency),
|
||||
|
||||
@@ -46,11 +46,11 @@ import org.whispersystems.signalservice.api.push.ServiceIdType
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity
|
||||
import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity
|
||||
import org.whispersystems.signalservice.internal.push.MismatchedDevices
|
||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage
|
||||
import org.whispersystems.signalservice.internal.push.SyncMessage
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
import org.whispersystems.signalservice.internal.push.WhoAmIResponse
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException
|
||||
import java.io.IOException
|
||||
import java.security.MessageDigest
|
||||
import java.security.SecureRandom
|
||||
@@ -270,9 +270,11 @@ class ChangeNumberRepository(
|
||||
result = SignalNetwork.account.changeNumber(request)
|
||||
}
|
||||
|
||||
val possibleError = result.getCause() as? MismatchedDevicesException
|
||||
if (possibleError != null) {
|
||||
messageSender.handleChangeNumberMismatchDevices(possibleError.mismatchedDevices)
|
||||
if (result is NetworkResult.StatusCodeError && result.code == 409) {
|
||||
val mismatchedDevices: MismatchedDevices? = result.parseJsonBody()
|
||||
if (mismatchedDevices != null) {
|
||||
messageSender.handleChangeNumberMismatchDevices(mismatchedDevices)
|
||||
}
|
||||
attempts++
|
||||
} else {
|
||||
completed = true
|
||||
|
||||
@@ -9,13 +9,10 @@ import org.thoughtcrime.securesms.pin.SvrWrongPinException
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegistrationResult
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.SvrNoDataException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.IncorrectRegistrationRecoveryPasswordException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.MalformedRequestException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException
|
||||
import org.whispersystems.signalservice.api.svr.Svr3Credentials
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
import org.whispersystems.signalservice.internal.push.LockedException
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket.RegistrationLockFailure
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
|
||||
/**
|
||||
@@ -29,27 +26,29 @@ sealed class ChangeNumberResult(cause: Throwable?) : RegistrationResult(cause) {
|
||||
is NetworkResult.ApplicationError -> UnknownError(networkResult.throwable)
|
||||
is NetworkResult.NetworkError -> UnknownError(networkResult.exception)
|
||||
is NetworkResult.StatusCodeError -> {
|
||||
when (val cause = networkResult.exception) {
|
||||
is IncorrectRegistrationRecoveryPasswordException -> IncorrectRecoveryPassword(cause)
|
||||
is AuthorizationFailedException -> AuthorizationFailed(cause)
|
||||
is MalformedRequestException -> MalformedRequest(cause)
|
||||
is RateLimitException -> createRateLimitProcessor(cause)
|
||||
is LockedException -> RegistrationLocked(cause = cause, timeRemaining = cause.timeRemaining, svr2Credentials = cause.svr2Credentials, svr3Credentials = cause.svr3Credentials)
|
||||
else -> {
|
||||
if (networkResult.code == 422) {
|
||||
ValidationError(cause)
|
||||
when (networkResult.code) {
|
||||
403 -> IncorrectRecoveryPassword(networkResult.exception)
|
||||
401 -> AuthorizationFailed(networkResult.exception)
|
||||
400 -> MalformedRequest(networkResult.exception)
|
||||
429 -> createRateLimitProcessor(networkResult.exception, networkResult.header("retry-after")?.toLongOrNull())
|
||||
423 -> {
|
||||
val registrationLockFailure: RegistrationLockFailure? = networkResult.parseJsonBody()
|
||||
if (registrationLockFailure != null) {
|
||||
RegistrationLocked(cause = networkResult.exception, timeRemaining = registrationLockFailure.timeRemaining, svr2Credentials = registrationLockFailure.svr2Credentials, svr3Credentials = registrationLockFailure.svr3Credentials)
|
||||
} else {
|
||||
UnknownError(cause)
|
||||
UnknownError(networkResult.exception)
|
||||
}
|
||||
}
|
||||
422 -> ValidationError(networkResult.exception)
|
||||
else -> UnknownError(networkResult.exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createRateLimitProcessor(exception: RateLimitException): ChangeNumberResult {
|
||||
return if (exception.retryAfterMilliseconds.isPresent) {
|
||||
RateLimited(exception, exception.retryAfterMilliseconds.get())
|
||||
private fun createRateLimitProcessor(exception: NonSuccessfulResponseCodeException, retryAfter: Long?): ChangeNumberResult {
|
||||
return if (retryAfter != null) {
|
||||
RateLimited(exception, retryAfter)
|
||||
} else {
|
||||
AttemptsExhausted(exception)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.chats.folders
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* UUID wrapper class for chat folders, used in storage service
|
||||
*/
|
||||
@Parcelize
|
||||
data class ChatFolderId(val uuid: UUID) : Parcelable {
|
||||
|
||||
companion object {
|
||||
fun from(id: String): ChatFolderId {
|
||||
return ChatFolderId(UuidUtil.parseOrThrow(id))
|
||||
}
|
||||
|
||||
fun from(uuid: UUID): ChatFolderId {
|
||||
return ChatFolderId(uuid)
|
||||
}
|
||||
|
||||
fun generate(): ChatFolderId {
|
||||
return ChatFolderId(UUID.randomUUID())
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return uuid.toString()
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.chats.folders
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
|
||||
/**
|
||||
* Represents an entry in the [org.thoughtcrime.securesms.database.ChatFolderTables].
|
||||
@@ -17,7 +19,12 @@ data class ChatFolderRecord(
|
||||
val showMutedChats: Boolean = true,
|
||||
val showIndividualChats: Boolean = false,
|
||||
val showGroupChats: Boolean = false,
|
||||
val folderType: FolderType = FolderType.CUSTOM
|
||||
val folderType: FolderType = FolderType.CUSTOM,
|
||||
val chatFolderId: ChatFolderId = ChatFolderId.generate(),
|
||||
@IgnoredOnParcel
|
||||
val storageServiceId: StorageId? = null,
|
||||
val storageServiceProto: ByteArray? = null,
|
||||
val deletedTimestampMs: Long = 0
|
||||
) : Parcelable {
|
||||
enum class FolderType(val value: Int) {
|
||||
/** Folder containing all chats */
|
||||
|
||||
@@ -50,6 +50,7 @@ import org.signal.core.ui.compose.DropdownMenus
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.ui.compose.copied.androidx.compose.DragAndDropEvent
|
||||
import org.signal.core.ui.compose.copied.androidx.compose.DraggableItem
|
||||
import org.signal.core.ui.compose.copied.androidx.compose.dragContainer
|
||||
import org.signal.core.ui.compose.copied.androidx.compose.rememberDragDropState
|
||||
@@ -100,7 +101,13 @@ class ChatFoldersFragment : ComposeFragment() {
|
||||
onDeleteDismissed = {
|
||||
viewModel.showDeleteDialog(false)
|
||||
},
|
||||
onPositionUpdated = { fromIndex, toIndex -> viewModel.updatePosition(fromIndex, toIndex) }
|
||||
onDragAndDropEvent = { event ->
|
||||
when (event) {
|
||||
is DragAndDropEvent.OnItemMove -> viewModel.updateItemPosition(event.fromIndex, event.toIndex)
|
||||
is DragAndDropEvent.OnItemDrop -> viewModel.saveItemPositions()
|
||||
is DragAndDropEvent.OnDragCancel -> {}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -116,15 +123,12 @@ fun FoldersScreen(
|
||||
onDeleteClicked: (ChatFolderRecord) -> Unit = {},
|
||||
onDeleteConfirmed: () -> Unit = {},
|
||||
onDeleteDismissed: () -> Unit = {},
|
||||
onPositionUpdated: (Int, Int) -> Unit = { _, _ -> }
|
||||
onDragAndDropEvent: (DragAndDropEvent) -> Unit = {}
|
||||
) {
|
||||
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
|
||||
val isRtl = ViewUtil.isRtl(LocalContext.current)
|
||||
val listState = rememberLazyListState()
|
||||
val dragDropState =
|
||||
rememberDragDropState(listState, includeHeader = true, includeFooter = true) { fromIndex, toIndex ->
|
||||
onPositionUpdated(fromIndex, toIndex)
|
||||
}
|
||||
val dragDropState = rememberDragDropState(listState, includeHeader = true, includeFooter = true, onEvent = onDragAndDropEvent)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (!SignalStore.uiHints.hasSeenChatFoldersEducationSheet) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.settings.app.chats.folders
|
||||
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
|
||||
/**
|
||||
* Repository for chat folders that handles creation, deletion, listing, etc.,
|
||||
@@ -9,7 +10,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
||||
object ChatFoldersRepository {
|
||||
|
||||
fun getCurrentFolders(): List<ChatFolderRecord> {
|
||||
return SignalDatabase.chatFolders.getChatFolders()
|
||||
return SignalDatabase.chatFolders.getCurrentChatFolders()
|
||||
}
|
||||
|
||||
fun getUnreadCountAndMutedStatusForFolders(folders: List<ChatFolderRecord>): HashMap<Long, Pair<Int, Boolean>> {
|
||||
@@ -25,6 +26,7 @@ object ChatFoldersRepository {
|
||||
)
|
||||
|
||||
SignalDatabase.chatFolders.createFolder(updatedFolder)
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
}
|
||||
|
||||
fun updateFolder(folder: ChatFolderRecord, includedRecipients: Set<Recipient>, excludedRecipients: Set<Recipient>) {
|
||||
@@ -36,21 +38,29 @@ object ChatFoldersRepository {
|
||||
)
|
||||
|
||||
SignalDatabase.chatFolders.updateFolder(updatedFolder)
|
||||
scheduleSync(updatedFolder.id)
|
||||
}
|
||||
|
||||
fun deleteFolder(folder: ChatFolderRecord) {
|
||||
SignalDatabase.chatFolders.deleteChatFolder(folder)
|
||||
scheduleSync(folder.id)
|
||||
}
|
||||
|
||||
fun updatePositions(folders: List<ChatFolderRecord>) {
|
||||
SignalDatabase.chatFolders.updatePositions(folders)
|
||||
folders.forEach { scheduleSync(it.id) }
|
||||
}
|
||||
|
||||
fun getFolder(id: Long): ChatFolderRecord {
|
||||
return SignalDatabase.chatFolders.getChatFolder(id)
|
||||
return SignalDatabase.chatFolders.getChatFolder(id)!!
|
||||
}
|
||||
|
||||
fun getFolderCount(): Int {
|
||||
return SignalDatabase.chatFolders.getFolderCount()
|
||||
}
|
||||
|
||||
private fun scheduleSync(id: Long) {
|
||||
SignalDatabase.chatFolders.markNeedsSync(id)
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.util.swap
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.contacts.paged.ChatType
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
@@ -177,17 +178,22 @@ class ChatFoldersViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun updatePosition(fromIndex: Int, toIndex: Int) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val folders = state.value.folders.toMutableList().apply { add(toIndex, removeAt(fromIndex)) }
|
||||
val updatedFolders = folders.mapIndexed { index, chatFolderRecord ->
|
||||
chatFolderRecord.copy(position = index)
|
||||
}
|
||||
ChatFoldersRepository.updatePositions(updatedFolders)
|
||||
fun updateItemPosition(fromIndex: Int, toIndex: Int) {
|
||||
val folders = state.value.folders.swap(fromIndex, toIndex)
|
||||
val updatedFolders = folders.mapIndexed { index, chatFolderRecord ->
|
||||
chatFolderRecord.copy(position = index)
|
||||
}
|
||||
internalState.update {
|
||||
it.copy(folders = updatedFolders)
|
||||
}
|
||||
}
|
||||
|
||||
internalState.update {
|
||||
it.copy(folders = updatedFolders)
|
||||
}
|
||||
fun saveItemPositions() {
|
||||
val updatedFolders = state.value.folders.mapIndexed { index, chatFolderRecord ->
|
||||
chatFolderRecord.copy(position = index)
|
||||
}
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
ChatFoldersRepository.updatePositions(updatedFolders)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,12 +331,6 @@ class ChatFoldersViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun enableButton(): Boolean {
|
||||
return internalState.value.pendingIncludedRecipients.isNotEmpty() ||
|
||||
internalState.value.pendingChatTypes.isNotEmpty() ||
|
||||
internalState.value.pendingExcludedRecipients.isNotEmpty()
|
||||
}
|
||||
|
||||
fun hasChanges(): Boolean {
|
||||
val currentFolder = state.value.currentFolder
|
||||
val originalFolder = state.value.originalFolder
|
||||
@@ -356,4 +356,14 @@ class ChatFoldersViewModel : ViewModel() {
|
||||
fun hasEmptyName(): Boolean {
|
||||
return state.value.currentFolder.folderRecord.name.isEmpty()
|
||||
}
|
||||
|
||||
fun shouldSetInitialFolder(): Boolean {
|
||||
val original = state.value.originalFolder
|
||||
val current = state.value.currentFolder
|
||||
return original.folderRecord.id == current.folderRecord.id &&
|
||||
original.folderRecord.showIndividualChats == current.folderRecord.showIndividualChats &&
|
||||
original.folderRecord.showGroupChats == current.folderRecord.showGroupChats &&
|
||||
original.includedRecipients == current.includedRecipients &&
|
||||
original.excludedRecipients == current.excludedRecipients
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ class ChooseChatsFragment : LoggingFragment(), ContactSelectionListFragment.OnCo
|
||||
viewModel.savePendingChats()
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
updateEnabledButton()
|
||||
doneButton.isEnabled = false
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
@@ -120,7 +120,7 @@ class ChooseChatsFragment : LoggingFragment(), ContactSelectionListFragment.OnCo
|
||||
} else {
|
||||
callback.accept(false)
|
||||
}
|
||||
updateEnabledButton()
|
||||
doneButton.isEnabled = true
|
||||
}
|
||||
|
||||
override fun onContactDeselected(recipientId: Optional<RecipientId>, number: String?, chatType: Optional<ChatType>) {
|
||||
@@ -133,7 +133,7 @@ class ChooseChatsFragment : LoggingFragment(), ContactSelectionListFragment.OnCo
|
||||
} else if (chatType.isPresent) {
|
||||
viewModel.removeChatType(chatType.get())
|
||||
}
|
||||
updateEnabledButton()
|
||||
doneButton.isEnabled = true
|
||||
}
|
||||
|
||||
override fun onSelectionChanged() = Unit
|
||||
@@ -147,10 +147,6 @@ class ChooseChatsFragment : LoggingFragment(), ContactSelectionListFragment.OnCo
|
||||
ContactSelectionDisplayMode.FLAG_SELF
|
||||
}
|
||||
|
||||
private fun updateEnabledButton() {
|
||||
doneButton.isEnabled = viewModel.enableButton()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ChooseChatsFragment::class.java)
|
||||
private val KEY_INCLUDE_CHATS = "include_chats"
|
||||
|
||||
@@ -97,7 +97,7 @@ class CreateFoldersFragment : ComposeFragment() {
|
||||
val isNewFolder = state.originalFolder.folderRecord.id == -1L
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (state.originalFolder == state.currentFolder) {
|
||||
if (viewModel.shouldSetInitialFolder()) {
|
||||
viewModel.setCurrentFolderId(arguments?.getLong(KEY_FOLDER_ID) ?: -1)
|
||||
viewModel.addThreadsToFolder(arguments?.getLongArray(KEY_THREAD_IDS))
|
||||
}
|
||||
@@ -366,12 +366,12 @@ fun CreateFolderScreen(
|
||||
|
||||
Buttons.MediumTonal(
|
||||
colors = ButtonDefaults.filledTonalButtonColors(
|
||||
contentColor = if (state.currentFolder.folderRecord.name.isEmpty()) {
|
||||
contentColor = if (state.currentFolder.folderRecord.name.isBlank()) {
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
},
|
||||
containerColor = if (state.currentFolder.folderRecord.name.isEmpty()) {
|
||||
containerColor = if (state.currentFolder.folderRecord.name.isBlank()) {
|
||||
MaterialTheme.colorScheme.surfaceVariant
|
||||
} else {
|
||||
MaterialTheme.colorScheme.primaryContainer
|
||||
@@ -380,7 +380,7 @@ fun CreateFolderScreen(
|
||||
),
|
||||
enabled = hasChanges,
|
||||
onClick = {
|
||||
if (state.currentFolder.folderRecord.name.isEmpty()) {
|
||||
if (state.currentFolder.folderRecord.name.isBlank()) {
|
||||
onShowToast()
|
||||
} else {
|
||||
onCreateConfirmed()
|
||||
|
||||
@@ -109,7 +109,7 @@ private fun Content(
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
title = "One-time donation state",
|
||||
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_left_24),
|
||||
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_start_24),
|
||||
navigationContentDescription = null,
|
||||
onNavigationClick = onNavigationClick
|
||||
) {
|
||||
|
||||
@@ -163,6 +163,28 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
)
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(DSLSettingsText.from("App UI"))
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from("Enable new split pane UI."),
|
||||
summary = DSLSettingsText.from("Warning: Some bugs and non functional buttons are expected. App will restart."),
|
||||
isChecked = state.largeScreenUi,
|
||||
onClick = {
|
||||
viewModel.setUseLargeScreenUi(!state.largeScreenUi)
|
||||
AppUtil.restart(requireContext())
|
||||
}
|
||||
)
|
||||
|
||||
switchPref(
|
||||
isEnabled = state.largeScreenUi,
|
||||
title = DSLSettingsText.from("Force split pane UI on landscape phones."),
|
||||
summary = DSLSettingsText.from("This setting requires split pane UI to be enabled."),
|
||||
isChecked = state.forceSplitPaneOnCompactLandscape,
|
||||
onClick = {
|
||||
viewModel.setForceSplitPaneOnCompactLandscape(!state.forceSplitPaneOnCompactLandscape)
|
||||
}
|
||||
)
|
||||
|
||||
sectionHeaderPref(DSLSettingsText.from("Playgrounds"))
|
||||
|
||||
clickPref(
|
||||
|
||||
@@ -25,5 +25,7 @@ data class InternalSettingsState(
|
||||
val useConversationItemV2ForMedia: Boolean,
|
||||
val hasPendingOneTimeDonation: Boolean,
|
||||
val hevcEncoding: Boolean,
|
||||
val newCallingUi: Boolean
|
||||
val newCallingUi: Boolean,
|
||||
val largeScreenUi: Boolean,
|
||||
val forceSplitPaneOnCompactLandscape: Boolean
|
||||
)
|
||||
|
||||
@@ -166,7 +166,9 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
useConversationItemV2ForMedia = SignalStore.internal.useConversationItemV2Media,
|
||||
hasPendingOneTimeDonation = SignalStore.inAppPayments.getPendingOneTimeDonation() != null,
|
||||
hevcEncoding = SignalStore.internal.hevcEncoding,
|
||||
newCallingUi = SignalStore.internal.newCallingUi
|
||||
newCallingUi = SignalStore.internal.newCallingUi,
|
||||
largeScreenUi = SignalStore.internal.largeScreenUi,
|
||||
forceSplitPaneOnCompactLandscape = SignalStore.internal.forceSplitPaneOnCompactLandscape
|
||||
)
|
||||
|
||||
fun onClearOnboardingState() {
|
||||
@@ -182,6 +184,16 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setUseLargeScreenUi(largeScreenUi: Boolean) {
|
||||
SignalStore.internal.largeScreenUi = largeScreenUi
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setForceSplitPaneOnCompactLandscape(forceSplitPaneOnCompactLandscape: Boolean) {
|
||||
SignalStore.internal.forceSplitPaneOnCompactLandscape = forceSplitPaneOnCompactLandscape
|
||||
refresh()
|
||||
}
|
||||
|
||||
class Factory(private val repository: InternalSettingsRepository) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(modelClass.cast(InternalSettingsViewModel(repository)))
|
||||
|
||||
@@ -285,7 +285,7 @@ fun Tabs(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.symbol_arrow_left_24),
|
||||
painter = painterResource(R.drawable.symbol_arrow_start_24),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
contentDescription = null
|
||||
)
|
||||
|
||||
@@ -85,7 +85,7 @@ private fun Screen(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackPressed) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.symbol_arrow_left_24),
|
||||
painter = painterResource(R.drawable.symbol_arrow_start_24),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
contentDescription = null
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
@@ -48,6 +49,7 @@ import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.util.Hex
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.internal.storage.InternalStorageServicePlaygroundViewModel.OneOffEvent
|
||||
import org.thoughtcrime.securesms.components.settings.app.internal.storage.InternalStorageServicePlaygroundViewModel.StorageInsights
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.StorageForcePushJob
|
||||
@@ -70,6 +72,7 @@ class InternalStorageServicePlaygroundFragment : ComposeFragment() {
|
||||
override fun FragmentContent() {
|
||||
val manifest by viewModel.manifest
|
||||
val storageRecords by viewModel.storageRecords
|
||||
val storageInsights by viewModel.storageInsights
|
||||
val oneOffEvent by viewModel.oneOffEvents
|
||||
var forceSsreToggled by remember { mutableStateOf(SignalStore.internal.forceSsre2Capability) }
|
||||
|
||||
@@ -77,6 +80,7 @@ class InternalStorageServicePlaygroundFragment : ComposeFragment() {
|
||||
onBackPressed = { findNavController().popBackStack() },
|
||||
manifest = manifest,
|
||||
storageRecords = storageRecords,
|
||||
storageInsights = storageInsights,
|
||||
oneOffEvent = oneOffEvent,
|
||||
forceSsreCapability = forceSsreToggled,
|
||||
onForceSsreToggled = { checked ->
|
||||
@@ -93,6 +97,7 @@ class InternalStorageServicePlaygroundFragment : ComposeFragment() {
|
||||
fun Screen(
|
||||
manifest: SignalStorageManifest,
|
||||
storageRecords: List<SignalStorageRecord>,
|
||||
storageInsights: StorageInsights,
|
||||
forceSsreCapability: Boolean,
|
||||
oneOffEvent: OneOffEvent,
|
||||
onForceSsreToggled: (Boolean) -> Unit = {},
|
||||
@@ -110,7 +115,7 @@ fun Screen(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackPressed) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.symbol_arrow_left_24),
|
||||
painter = painterResource(R.drawable.symbol_arrow_start_24),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
contentDescription = null
|
||||
)
|
||||
@@ -143,6 +148,7 @@ fun Screen(
|
||||
1 -> ViewScreen(
|
||||
manifest = manifest,
|
||||
storageRecords = storageRecords,
|
||||
storageInsights = storageInsights,
|
||||
oneOffEvent = oneOffEvent
|
||||
)
|
||||
}
|
||||
@@ -160,7 +166,7 @@ fun ToolScreen(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
ActionRow("Enqueue StorageSyncJob", "Just a normal syncing operation.") {
|
||||
AppDependencies.jobManager.add(StorageSyncJob())
|
||||
AppDependencies.jobManager.add(StorageSyncJob.forLocalChange())
|
||||
}
|
||||
|
||||
ActionRow("Enqueue StorageForcePushJob", "Forces your local state over the remote.") {
|
||||
@@ -191,6 +197,7 @@ fun ToolScreen(
|
||||
fun ViewScreen(
|
||||
manifest: SignalStorageManifest,
|
||||
storageRecords: List<SignalStorageRecord>,
|
||||
storageInsights: StorageInsights,
|
||||
oneOffEvent: OneOffEvent
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
@@ -217,6 +224,10 @@ fun ViewScreen(
|
||||
ManifestRow(manifest)
|
||||
Dividers.Default()
|
||||
}
|
||||
item(key = "insights") {
|
||||
InsightsRow(storageInsights)
|
||||
Dividers.Default()
|
||||
}
|
||||
storageRecords.forEach { record ->
|
||||
item(key = Hex.toStringCondensed(record.id.raw)) {
|
||||
StorageRecordRow(record)
|
||||
@@ -236,10 +247,46 @@ private fun ManifestRow(manifest: SignalStorageManifest) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InsightsRow(insights: StorageInsights) {
|
||||
Column {
|
||||
ManifestItemRow("Total Manifest Size", insights.totalManifestSize.toUnitString())
|
||||
ManifestItemRow("Total Record Size", insights.totalRecordSize.toUnitString())
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
ManifestItemRow("Total Account Record Size", insights.totalAccountRecordSize.toUnitString())
|
||||
ManifestItemRow("Total Contact Record Size", insights.totalContactSize.toUnitString())
|
||||
ManifestItemRow("Total GroupV1 Record Size", insights.totalGroupV1Size.toUnitString())
|
||||
ManifestItemRow("Total GroupV2 Record Size", insights.totalGroupV2Size.toUnitString())
|
||||
ManifestItemRow("Total Call Link Record Size", insights.totalCallLinkSize.toUnitString())
|
||||
ManifestItemRow("Total Distribution List Record Size", insights.totalDistributionListSize.toUnitString())
|
||||
ManifestItemRow("Total Chat Folder Record Size", insights.totalChatFolderSize.toUnitString())
|
||||
ManifestItemRow("Total Unknown Record Size", insights.totalUnknownSize.toUnitString())
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
if (listOf(
|
||||
insights.totalContactSize,
|
||||
insights.totalGroupV1Size,
|
||||
insights.totalGroupV2Size,
|
||||
insights.totalAccountRecordSize,
|
||||
insights.totalCallLinkSize,
|
||||
insights.totalDistributionListSize,
|
||||
insights.totalChatFolderSize
|
||||
).sumOf { it.bytes } != insights.totalRecordSize.bytes
|
||||
) {
|
||||
Text("Mismatch! Sum of record sizes does not match our total record size!")
|
||||
} else {
|
||||
Text("Everything adds up \uD83D\uDC4D")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ManifestItemRow(title: String, value: String) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(title + ":", fontWeight = FontWeight.Bold)
|
||||
Text("$title:", fontWeight = FontWeight.Bold)
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(value)
|
||||
}
|
||||
@@ -285,6 +332,12 @@ private fun StorageRecordRow(record: SignalStorageRecord) {
|
||||
ManifestItemRow("ID", Hex.toStringCondensed(record.id.raw))
|
||||
}
|
||||
}
|
||||
record.proto.chatFolder != null -> {
|
||||
Column {
|
||||
Text("Chat Folder", fontWeight = FontWeight.Bold)
|
||||
ManifestItemRow("ID", Hex.toStringCondensed(record.id.raw))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Column {
|
||||
Text("Unknown!")
|
||||
@@ -323,6 +376,7 @@ fun ScreenPreview() {
|
||||
forceSsreCapability = true,
|
||||
manifest = SignalStorageManifest.EMPTY,
|
||||
storageRecords = emptyList(),
|
||||
storageInsights = StorageInsights(),
|
||||
oneOffEvent = OneOffEvent.None
|
||||
)
|
||||
}
|
||||
@@ -355,6 +409,7 @@ fun ViewScreenPreview() {
|
||||
storageIds = storageRecords.map { it.id }
|
||||
),
|
||||
storageRecords = storageRecords,
|
||||
storageInsights = StorageInsights(),
|
||||
oneOffEvent = OneOffEvent.None
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.ByteSize
|
||||
import org.signal.core.util.bytes
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
@@ -34,6 +36,10 @@ class InternalStorageServicePlaygroundViewModel : ViewModel() {
|
||||
val storageRecords: State<List<SignalStorageRecord>>
|
||||
get() = _storageItems
|
||||
|
||||
private val _storageInsights: MutableState<StorageInsights> = mutableStateOf(StorageInsights())
|
||||
val storageInsights: State<StorageInsights>
|
||||
get() = _storageInsights
|
||||
|
||||
private val _oneOffEvents: MutableState<OneOffEvent> = mutableStateOf(OneOffEvent.None)
|
||||
val oneOffEvents: State<OneOffEvent>
|
||||
get() = _oneOffEvents
|
||||
@@ -69,11 +75,44 @@ class InternalStorageServicePlaygroundViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
_storageItems.value = records
|
||||
|
||||
// TODO get total manifest size -- we need the raw proto, which we don't have
|
||||
val insights = StorageInsights(
|
||||
totalManifestSize = manifest.protoByteSize,
|
||||
totalRecordSize = records.sumOf { it.sizeInBytes() }.bytes,
|
||||
totalContactSize = records.filter { it.proto.contact != null }.sumOf { it.sizeInBytes() }.bytes,
|
||||
totalGroupV1Size = records.filter { it.proto.groupV1 != null }.sumOf { it.sizeInBytes() }.bytes,
|
||||
totalGroupV2Size = records.filter { it.proto.groupV2 != null }.sumOf { it.sizeInBytes() }.bytes,
|
||||
totalAccountRecordSize = records.filter { it.proto.account != null }.sumOf { it.sizeInBytes() }.bytes,
|
||||
totalCallLinkSize = records.filter { it.proto.callLink != null }.sumOf { it.sizeInBytes() }.bytes,
|
||||
totalDistributionListSize = records.filter { it.proto.storyDistributionList != null }.sumOf { it.sizeInBytes() }.bytes,
|
||||
totalChatFolderSize = records.filter { it.proto.chatFolder != null }.sumOf { it.sizeInBytes() }.bytes,
|
||||
totalUnknownSize = records.filter { it.isUnknown }.sumOf { it.sizeInBytes() }.bytes
|
||||
)
|
||||
|
||||
_storageInsights.value = insights
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun SignalStorageRecord.sizeInBytes(): Int {
|
||||
return this.proto.encode().size
|
||||
}
|
||||
|
||||
enum class OneOffEvent {
|
||||
None, ManifestDecryptionError, StorageRecordDecryptionError, ManifestNotFoundError
|
||||
}
|
||||
|
||||
data class StorageInsights(
|
||||
val totalManifestSize: ByteSize = 0.bytes,
|
||||
val totalRecordSize: ByteSize = 0.bytes,
|
||||
val totalContactSize: ByteSize = 0.bytes,
|
||||
val totalGroupV1Size: ByteSize = 0.bytes,
|
||||
val totalGroupV2Size: ByteSize = 0.bytes,
|
||||
val totalAccountRecordSize: ByteSize = 0.bytes,
|
||||
val totalCallLinkSize: ByteSize = 0.bytes,
|
||||
val totalDistributionListSize: ByteSize = 0.bytes,
|
||||
val totalChatFolderSize: ByteSize = 0.bytes,
|
||||
val totalUnknownSize: ByteSize = 0.bytes
|
||||
)
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ private fun BadgeImage(
|
||||
},
|
||||
update = {
|
||||
it.setBadge(badge)
|
||||
it.isClickable = false
|
||||
},
|
||||
modifier = modifier
|
||||
)
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -23,6 +24,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import org.signal.core.ui.compose.BottomSheets
|
||||
@@ -36,6 +38,7 @@ import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
/**
|
||||
* Displayed after the user completes the donation flow for a bank transfer.
|
||||
@@ -43,13 +46,19 @@ import org.thoughtcrime.securesms.util.SpanUtil
|
||||
class DonationPendingBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
|
||||
private val args: DonationPendingBottomSheetArgs by navArgs()
|
||||
private val viewModel: DonationPendingBottomSheetViewModel by viewModel {
|
||||
DonationPendingBottomSheetViewModel(args.inAppPaymentId)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun SheetContent() {
|
||||
DonationPendingBottomSheetContent(
|
||||
badge = Badges.fromDatabaseBadge(args.inAppPayment.data.badge!!),
|
||||
onDoneClick = this::onDoneClick
|
||||
)
|
||||
val inAppPayment by viewModel.inAppPayment.collectAsStateWithLifecycle()
|
||||
|
||||
if (inAppPayment != null)
|
||||
DonationPendingBottomSheetContent(
|
||||
badge = Badges.fromDatabaseBadge(inAppPayment!!.data.badge!!),
|
||||
onDoneClick = this::onDoneClick
|
||||
)
|
||||
}
|
||||
|
||||
private fun onDoneClick() {
|
||||
@@ -59,7 +68,8 @@ class DonationPendingBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
|
||||
if (!args.inAppPayment.type.recurring) {
|
||||
val iap = viewModel.inAppPayment.value
|
||||
if (iap != null && !iap.type.recurring) {
|
||||
findNavController().popBackStack()
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.concurrent.SignalDispatchers
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
|
||||
class DonationPendingBottomSheetViewModel(
|
||||
inAppPaymentId: InAppPaymentTable.InAppPaymentId
|
||||
) : ViewModel() {
|
||||
|
||||
private val internalInAppPayment = MutableStateFlow<InAppPaymentTable.InAppPayment?>(null)
|
||||
val inAppPayment: StateFlow<InAppPaymentTable.InAppPayment?> = internalInAppPayment
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
val inAppPayment = withContext(SignalDispatchers.IO) {
|
||||
SignalDatabase.inAppPayments.getById(inAppPaymentId)!!
|
||||
}
|
||||
|
||||
internalInAppPayment.update { inAppPayment }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,12 +112,15 @@ object InAppPaymentsRepository {
|
||||
* Common logic for handling errors coming from the Rx chains that handle payments. These errors
|
||||
* are analyzed and then either written to the database or dispatched to the temporary error processor.
|
||||
*/
|
||||
@WorkerThread
|
||||
fun handlePipelineError(
|
||||
inAppPaymentId: InAppPaymentTable.InAppPaymentId,
|
||||
donationErrorSource: DonationErrorSource,
|
||||
paymentSourceType: PaymentSourceType,
|
||||
error: Throwable
|
||||
) {
|
||||
val inAppPayment = SignalDatabase.inAppPayments.getById(inAppPaymentId)!!
|
||||
val donationErrorSource = inAppPayment.type.toErrorSource()
|
||||
val paymentSourceType = inAppPayment.data.paymentMethodType.toPaymentSourceType()
|
||||
|
||||
if (error is InAppPaymentError) {
|
||||
setErrorIfNotPresent(inAppPaymentId, error.inAppPaymentDataError)
|
||||
return
|
||||
@@ -132,7 +135,7 @@ object InAppPaymentsRepository {
|
||||
val inAppPaymentError = InAppPaymentError.fromDonationError(donationError)?.inAppPaymentDataError
|
||||
if (inAppPaymentError != null) {
|
||||
Log.w(TAG, "Detected a terminal error.")
|
||||
setErrorIfNotPresent(inAppPaymentId, inAppPaymentError).subscribe()
|
||||
setErrorIfNotPresent(inAppPaymentId, inAppPaymentError)
|
||||
} else {
|
||||
Log.w(TAG, "Detected a temporary error.")
|
||||
temporaryErrorProcessor.onNext(inAppPaymentId to donationError)
|
||||
@@ -150,20 +153,19 @@ object InAppPaymentsRepository {
|
||||
/**
|
||||
* Writes the given error to the database, if and only if there is not already an error set.
|
||||
*/
|
||||
private fun setErrorIfNotPresent(inAppPaymentId: InAppPaymentTable.InAppPaymentId, error: InAppPaymentData.Error?): Completable {
|
||||
return Completable.fromAction {
|
||||
val inAppPayment = SignalDatabase.inAppPayments.getById(inAppPaymentId)!!
|
||||
if (inAppPayment.data.error == null) {
|
||||
Log.d(TAG, "Setting error on InAppPayment[$inAppPaymentId]")
|
||||
SignalDatabase.inAppPayments.update(
|
||||
inAppPayment.copy(
|
||||
notified = false,
|
||||
state = InAppPaymentTable.State.END,
|
||||
data = inAppPayment.data.copy(error = error)
|
||||
)
|
||||
@WorkerThread
|
||||
private fun setErrorIfNotPresent(inAppPaymentId: InAppPaymentTable.InAppPaymentId, error: InAppPaymentData.Error?) {
|
||||
val inAppPayment = SignalDatabase.inAppPayments.getById(inAppPaymentId)!!
|
||||
if (inAppPayment.data.error == null) {
|
||||
Log.d(TAG, "Setting error on InAppPayment[$inAppPaymentId]")
|
||||
SignalDatabase.inAppPayments.update(
|
||||
inAppPayment.copy(
|
||||
notified = false,
|
||||
state = InAppPaymentTable.State.END,
|
||||
data = inAppPayment.data.copy(error = error)
|
||||
)
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -522,6 +524,7 @@ object InAppPaymentsRepository {
|
||||
nonVerifiedMonthlyDonation = inAppPayment.toNonVerifiedMonthlyDonation()
|
||||
)
|
||||
}
|
||||
|
||||
InAppPaymentTable.State.PENDING, InAppPaymentTable.State.TRANSACTING, InAppPaymentTable.State.REQUIRED_ACTION_COMPLETED -> {
|
||||
if (inAppPayment.data.redemption?.keepAlive == true) {
|
||||
DonationRedemptionJobStatus.PendingKeepAlive
|
||||
@@ -531,6 +534,7 @@ object InAppPaymentsRepository {
|
||||
DonationRedemptionJobStatus.PendingReceiptRequest
|
||||
}
|
||||
}
|
||||
|
||||
InAppPaymentTable.State.END -> {
|
||||
if (type.recurring && inAppPayment.data.error != null) {
|
||||
DonationRedemptionJobStatus.FailedSubscription
|
||||
|
||||
@@ -79,6 +79,7 @@ object RecurringInAppPaymentRepository {
|
||||
@WorkerThread
|
||||
fun getActiveSubscriptionSync(type: InAppPaymentSubscriberRecord.Type): Result<ActiveSubscription> {
|
||||
if (type == InAppPaymentSubscriberRecord.Type.BACKUP && SignalStore.backup.backupTierInternalOverride == MessageBackupTier.PAID) {
|
||||
Log.d(TAG, "Returning mock paid subscription.")
|
||||
return Result.success(MOCK_PAID_SUBSCRIPTION)
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ class InAppPaymentsBottomSheetDelegate(
|
||||
private fun handleLegacyVerifiedMonthlyDonationSheets() {
|
||||
SignalStore.inAppPayments.consumeVerifiedSubscription3DSData()?.also {
|
||||
DonationPendingBottomSheet().apply {
|
||||
arguments = DonationPendingBottomSheetArgs.Builder(it.inAppPayment).build().toBundle()
|
||||
arguments = DonationPendingBottomSheetArgs.Builder(it.inAppPayment.id).build().toBundle()
|
||||
}.show(fragmentManager, null)
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,7 @@ class InAppPaymentsBottomSheetDelegate(
|
||||
.show(fragmentManager, null)
|
||||
} else if (payment.data.error != null && payment.state == InAppPaymentTable.State.PENDING) {
|
||||
DonationPendingBottomSheet().apply {
|
||||
arguments = DonationPendingBottomSheetArgs.Builder(payment).build().toBundle()
|
||||
arguments = DonationPendingBottomSheetArgs.Builder(payment.id).build().toBundle()
|
||||
}.show(fragmentManager, null)
|
||||
} else if (isUnexpectedCancellation(payment.state, payment.data) && SignalStore.inAppPayments.showMonthlyDonationCanceledDialog) {
|
||||
MonthlyDonationCanceledBottomSheetDialogFragment.show(fragmentManager)
|
||||
|
||||
@@ -165,7 +165,7 @@ class DonateToSignalFragment :
|
||||
|
||||
is DonateToSignalAction.DisplayGatewaySelectorDialog -> {
|
||||
Log.d(TAG, "Presenting gateway selector for ${action.inAppPayment.id}")
|
||||
val navAction = DonateToSignalFragmentDirections.actionDonateToSignalFragmentToGatewaySelectorBottomSheetDialog(action.inAppPayment)
|
||||
val navAction = DonateToSignalFragmentDirections.actionDonateToSignalFragmentToGatewaySelectorBottomSheetDialog(action.inAppPayment.id)
|
||||
|
||||
findNavController().safeNavigate(navAction)
|
||||
}
|
||||
@@ -173,8 +173,7 @@ class DonateToSignalFragment :
|
||||
is DonateToSignalAction.CancelSubscription -> {
|
||||
val navAction = DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(
|
||||
InAppPaymentProcessorAction.CANCEL_SUBSCRIPTION,
|
||||
null,
|
||||
InAppPaymentType.RECURRING_DONATION
|
||||
null
|
||||
)
|
||||
|
||||
findNavController().safeNavigate(navAction)
|
||||
@@ -184,16 +183,14 @@ class DonateToSignalFragment :
|
||||
if (action.inAppPayment.data.paymentMethodType == InAppPaymentData.PaymentMethodType.PAYPAL) {
|
||||
val navAction = DonateToSignalFragmentDirections.actionDonateToSignalFragmentToPaypalPaymentInProgressFragment(
|
||||
InAppPaymentProcessorAction.UPDATE_SUBSCRIPTION,
|
||||
action.inAppPayment,
|
||||
action.inAppPayment.type
|
||||
action.inAppPayment.id
|
||||
)
|
||||
|
||||
findNavController().safeNavigate(navAction)
|
||||
} else {
|
||||
val navAction = DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(
|
||||
InAppPaymentProcessorAction.UPDATE_SUBSCRIPTION,
|
||||
action.inAppPayment,
|
||||
action.inAppPayment.type
|
||||
action.inAppPayment.id
|
||||
)
|
||||
|
||||
findNavController().safeNavigate(navAction)
|
||||
@@ -339,6 +336,8 @@ class DonateToSignalFragment :
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
space(24.dp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,8 +476,7 @@ class DonateToSignalFragment :
|
||||
findNavController().safeNavigate(
|
||||
DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(
|
||||
InAppPaymentProcessorAction.PROCESS_NEW_IN_APP_PAYMENT,
|
||||
inAppPayment,
|
||||
inAppPayment.type
|
||||
inAppPayment.id
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -487,22 +485,21 @@ class DonateToSignalFragment :
|
||||
findNavController().safeNavigate(
|
||||
DonateToSignalFragmentDirections.actionDonateToSignalFragmentToPaypalPaymentInProgressFragment(
|
||||
InAppPaymentProcessorAction.PROCESS_NEW_IN_APP_PAYMENT,
|
||||
inAppPayment,
|
||||
inAppPayment.type
|
||||
inAppPayment.id
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun navigateToCreditCardForm(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToCreditCardFragment(inAppPayment))
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToCreditCardFragment(inAppPayment.id))
|
||||
}
|
||||
|
||||
override fun navigateToIdealDetailsFragment(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToIdealTransferDetailsFragment(inAppPayment))
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToIdealTransferDetailsFragment(inAppPayment.id))
|
||||
}
|
||||
|
||||
override fun navigateToBankTransferMandate(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToBankTransferMandateFragment(inAppPayment))
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToBankTransferMandateFragment(inAppPayment.id))
|
||||
}
|
||||
|
||||
override fun onPaymentComplete(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
@@ -523,7 +520,7 @@ class DonateToSignalFragment :
|
||||
}
|
||||
|
||||
override fun navigateToDonationPending(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToDonationPendingBottomSheet(inAppPayment))
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToDonationPendingBottomSheet(inAppPayment.id))
|
||||
}
|
||||
|
||||
override fun exitCheckoutFlow() {
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.navGraphViewModels
|
||||
import com.google.android.gms.wallet.PaymentData
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
@@ -17,7 +18,10 @@ import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.concurrent.SignalDispatchers
|
||||
import org.signal.core.util.getParcelableCompat
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.GooglePayApi
|
||||
@@ -126,12 +130,17 @@ class InAppPaymentCheckoutDelegate(
|
||||
}
|
||||
|
||||
private fun handleSuccessfulDonationProcessorActionResult(result: InAppPaymentProcessorActionResult) {
|
||||
setActivityResult(result.action, result.inAppPaymentType)
|
||||
|
||||
if (result.action == InAppPaymentProcessorAction.CANCEL_SUBSCRIPTION) {
|
||||
callback.onSubscriptionCancelled(result.inAppPaymentType)
|
||||
setActivityResult(result.action, InAppPaymentType.RECURRING_DONATION)
|
||||
callback.onSubscriptionCancelled(InAppPaymentType.RECURRING_DONATION)
|
||||
} else {
|
||||
callback.onPaymentComplete(result.inAppPayment!!)
|
||||
fragment.lifecycleScope.launch {
|
||||
val inAppPayment = withContext(SignalDispatchers.IO) {
|
||||
SignalDatabase.inAppPayments.getById(result.inAppPaymentId!!)!!
|
||||
}
|
||||
|
||||
callback.onPaymentComplete(inAppPayment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,12 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
|
||||
@Parcelize
|
||||
class InAppPaymentProcessorActionResult(
|
||||
val action: InAppPaymentProcessorAction,
|
||||
val inAppPayment: InAppPaymentTable.InAppPayment?,
|
||||
val inAppPaymentType: InAppPaymentType,
|
||||
val inAppPaymentId: InAppPaymentTable.InAppPaymentId?,
|
||||
val status: Status
|
||||
) : Parcelable {
|
||||
enum class Status {
|
||||
|
||||
@@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.errors.Do
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError.BadgeRedemptionError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.InAppPaymentPayPalOneTimeSetupJob
|
||||
@@ -47,17 +48,17 @@ object SharedInAppPaymentPipeline {
|
||||
* This method will enqueue the proper setup job based off the type of [InAppPaymentTable.InAppPayment] and then
|
||||
* await for either [InAppPaymentTable.State.PENDING], [InAppPaymentTable.State.REQUIRES_ACTION] or [InAppPaymentTable.State.END]
|
||||
* before moving further, handling each state appropriately.
|
||||
*
|
||||
* @param requiredActionHandler Dispatch method for handling PayPal input, 3DS, iDEAL, etc.
|
||||
*/
|
||||
@CheckResult
|
||||
fun awaitTransaction(
|
||||
inAppPayment: InAppPaymentTable.InAppPayment,
|
||||
inAppPaymentId: InAppPaymentTable.InAppPaymentId,
|
||||
paymentSource: PaymentSource,
|
||||
requiredActionHandler: RequiredActionHandler
|
||||
): Completable {
|
||||
return InAppPaymentsRepository.observeUpdates(inAppPayment.id)
|
||||
oneTimeRequiredActionHandler: RequiredActionHandler,
|
||||
monthlyRequiredActionHandler: RequiredActionHandler
|
||||
): Single<InAppPaymentTable.InAppPayment> {
|
||||
return InAppPaymentsRepository.observeUpdates(inAppPaymentId)
|
||||
.doOnSubscribe {
|
||||
val inAppPayment = SignalDatabase.inAppPayments.getById(inAppPaymentId)!!
|
||||
val job = if (inAppPayment.type.recurring) {
|
||||
if (inAppPayment.data.paymentMethodType == InAppPaymentData.PaymentMethodType.PAYPAL) {
|
||||
InAppPaymentPayPalRecurringSetupJob.create(inAppPayment, paymentSource)
|
||||
@@ -76,25 +77,27 @@ object SharedInAppPaymentPipeline {
|
||||
}
|
||||
.skipWhile { it.state != InAppPaymentTable.State.PENDING && it.state != InAppPaymentTable.State.REQUIRES_ACTION && it.state != InAppPaymentTable.State.END }
|
||||
.firstOrError()
|
||||
.flatMapCompletable { iap ->
|
||||
.flatMap { iap ->
|
||||
when (iap.state) {
|
||||
InAppPaymentTable.State.PENDING -> {
|
||||
Log.w(TAG, "Payment of type ${inAppPayment.type} is pending. Awaiting completion.")
|
||||
Log.w(TAG, "Payment of type ${iap.type} is pending. Awaiting completion.")
|
||||
awaitRedemption(iap, paymentSource.type)
|
||||
}
|
||||
|
||||
InAppPaymentTable.State.REQUIRES_ACTION -> {
|
||||
Log.d(TAG, "Payment of type ${inAppPayment.type} requires user action to set up.", true)
|
||||
requiredActionHandler(iap.id).andThen(awaitTransaction(iap, paymentSource, requiredActionHandler))
|
||||
Log.d(TAG, "Payment of type ${iap.type} requires user action to set up.", true)
|
||||
|
||||
val requiredActionHandler = if (iap.type.recurring) monthlyRequiredActionHandler else oneTimeRequiredActionHandler
|
||||
requiredActionHandler(iap.id).andThen(awaitTransaction(iap.id, paymentSource, oneTimeRequiredActionHandler, monthlyRequiredActionHandler))
|
||||
}
|
||||
|
||||
InAppPaymentTable.State.END -> {
|
||||
if (iap.data.error != null) {
|
||||
Log.d(TAG, "IAP error detected.", true)
|
||||
Completable.error(InAppPaymentError(iap.data.error))
|
||||
Single.error(InAppPaymentError(iap.data.error))
|
||||
} else {
|
||||
Log.d(TAG, "Unexpected early end state. Possible payment failure.", true)
|
||||
Completable.error(DonationError.genericPaymentFailure(inAppPayment.type.toErrorSource()))
|
||||
Single.error(DonationError.genericPaymentFailure(iap.type.toErrorSource()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +110,7 @@ object SharedInAppPaymentPipeline {
|
||||
* Waits 10 seconds for the redemption to complete, and fails with a temporary error afterwards.
|
||||
*/
|
||||
@CheckResult
|
||||
fun awaitRedemption(inAppPayment: InAppPaymentTable.InAppPayment, paymentSourceType: PaymentSourceType): Completable {
|
||||
fun awaitRedemption(inAppPayment: InAppPaymentTable.InAppPayment, paymentSourceType: PaymentSourceType): Single<InAppPaymentTable.InAppPayment> {
|
||||
val isLongRunning = paymentSourceType.isBankTransfer
|
||||
val errorSource = when (inAppPayment.type) {
|
||||
InAppPaymentType.UNKNOWN -> error("Unsupported type UNKNOWN.")
|
||||
@@ -131,19 +134,6 @@ object SharedInAppPaymentPipeline {
|
||||
throw InAppPaymentError(it.data.error)
|
||||
}
|
||||
it
|
||||
}.firstOrError().timeout(10, TimeUnit.SECONDS, Single.error(timeoutError)).ignoreElement()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic error handling for donations.
|
||||
*/
|
||||
fun handleError(
|
||||
throwable: Throwable,
|
||||
inAppPaymentId: InAppPaymentTable.InAppPaymentId,
|
||||
paymentSourceType: PaymentSourceType,
|
||||
donationErrorSource: DonationErrorSource
|
||||
) {
|
||||
Log.w(TAG, "Failure in $donationErrorSource payment pipeline...", throwable, true)
|
||||
InAppPaymentsRepository.handlePipelineError(inAppPaymentId, donationErrorSource, paymentSourceType, throwable)
|
||||
}.firstOrError().timeout(10, TimeUnit.SECONDS, Single.error(timeoutError))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,14 @@ import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.navigation.navGraphViewModels
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.getParcelableCompat
|
||||
import org.signal.donations.InAppPaymentType
|
||||
@@ -30,12 +34,16 @@ import org.thoughtcrime.securesms.databinding.CreditCardFragmentBinding
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
|
||||
|
||||
private val binding by ViewBinderDelegate(CreditCardFragmentBinding::bind)
|
||||
private val args: CreditCardFragmentArgs by navArgs()
|
||||
private val viewModel: CreditCardViewModel by viewModels()
|
||||
private val viewModel: CreditCardViewModel by viewModel {
|
||||
CreditCardViewModel(args.inAppPaymentId)
|
||||
}
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
private val stripePaymentViewModel: StripePaymentInProgressViewModel by navGraphViewModels(
|
||||
R.id.checkout_flow
|
||||
@@ -43,7 +51,7 @@ class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
TemporaryScreenshotSecurity.bindToViewLifecycleOwner(this)
|
||||
InAppPaymentCheckoutDelegate.ErrorHandler().attach(this, null, args.inAppPayment.id)
|
||||
InAppPaymentCheckoutDelegate.ErrorHandler().attach(this, null, args.inAppPaymentId)
|
||||
|
||||
setFragmentResultListener(StripePaymentInProgressFragment.REQUEST_KEY) { _, bundle ->
|
||||
val result: InAppPaymentProcessorActionResult = bundle.getParcelableCompat(StripePaymentInProgressFragment.REQUEST_KEY, InAppPaymentProcessorActionResult::class.java)!!
|
||||
@@ -53,21 +61,27 @@ class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
|
||||
}
|
||||
}
|
||||
|
||||
binding.continueButton.text = when (args.inAppPayment.type) {
|
||||
InAppPaymentType.RECURRING_DONATION -> {
|
||||
getString(
|
||||
R.string.CreditCardFragment__donate_s_month,
|
||||
FiatMoneyUtil.format(resources, args.inAppPayment.data.amount!!.toFiatMoney(), FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
)
|
||||
}
|
||||
InAppPaymentType.RECURRING_BACKUP -> {
|
||||
getString(
|
||||
R.string.CreditCardFragment__pay_s_month,
|
||||
FiatMoneyUtil.format(resources, args.inAppPayment.data.amount!!.toFiatMoney(), FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
getString(R.string.CreditCardFragment__donate_s, FiatMoneyUtil.format(resources, args.inAppPayment.data.amount!!.toFiatMoney()))
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
viewModel.inAppPayment.collectLatest { inAppPayment ->
|
||||
binding.continueButton.text = when (inAppPayment.type) {
|
||||
InAppPaymentType.RECURRING_DONATION -> {
|
||||
getString(
|
||||
R.string.CreditCardFragment__donate_s_month,
|
||||
FiatMoneyUtil.format(resources, inAppPayment.data.amount!!.toFiatMoney(), FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
)
|
||||
}
|
||||
InAppPaymentType.RECURRING_BACKUP -> {
|
||||
getString(
|
||||
R.string.CreditCardFragment__pay_s_month,
|
||||
FiatMoneyUtil.format(resources, inAppPayment.data.amount!!.toFiatMoney(), FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
getString(R.string.CreditCardFragment__donate_s, FiatMoneyUtil.format(resources, inAppPayment.data.amount!!.toFiatMoney()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,8 +133,7 @@ class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
|
||||
findNavController().safeNavigate(
|
||||
CreditCardFragmentDirections.actionCreditCardFragmentToStripePaymentInProgressFragment(
|
||||
InAppPaymentProcessorAction.PROCESS_NEW_IN_APP_PAYMENT,
|
||||
args.inAppPayment,
|
||||
args.inAppPayment.type
|
||||
args.inAppPaymentId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,27 +1,50 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.card
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
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 io.reactivex.rxjava3.processors.BehaviorProcessor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.donations.StripeApi
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import java.util.Calendar
|
||||
|
||||
class CreditCardViewModel : ViewModel() {
|
||||
class CreditCardViewModel(
|
||||
inAppPaymentId: InAppPaymentTable.InAppPaymentId
|
||||
) : ViewModel() {
|
||||
|
||||
private val formStore = RxStore(CreditCardFormState())
|
||||
private val validationProcessor: BehaviorProcessor<CreditCardValidationState> = BehaviorProcessor.create()
|
||||
private val currentYear: Int
|
||||
private val currentMonth: Int
|
||||
|
||||
private val internalInAppPayment = MutableStateFlow<InAppPaymentTable.InAppPayment?>(null)
|
||||
val inAppPayment: Flow<InAppPaymentTable.InAppPayment> = internalInAppPayment.filterNotNull()
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
init {
|
||||
val calendar = Calendar.getInstance()
|
||||
|
||||
viewModelScope.launch {
|
||||
val inAppPayment = withContext(Dispatchers.IO) {
|
||||
SignalDatabase.inAppPayments.getById(inAppPaymentId)!!
|
||||
}
|
||||
|
||||
internalInAppPayment.update { inAppPayment }
|
||||
}
|
||||
|
||||
currentYear = calendar.get(Calendar.YEAR)
|
||||
currentMonth = calendar.get(Calendar.MONTH) + 1
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.content.Context
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
@@ -31,6 +30,7 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.payments.currency.CurrencyUtil
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
/**
|
||||
* Entry point to capturing the necessary payment token to pay for a donation
|
||||
@@ -41,9 +41,9 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
|
||||
private val args: GatewaySelectorBottomSheetArgs by navArgs()
|
||||
|
||||
private val viewModel: GatewaySelectorViewModel by viewModels(factoryProducer = {
|
||||
GatewaySelectorViewModel.Factory(args, requireListener<GooglePayComponent>().googlePayRepository)
|
||||
})
|
||||
private val viewModel: GatewaySelectorViewModel by viewModel {
|
||||
GatewaySelectorViewModel(args, requireListener<GooglePayComponent>().googlePayRepository)
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
BadgeDisplay112.register(adapter)
|
||||
@@ -59,44 +59,48 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: GatewaySelectorState): DSLConfiguration {
|
||||
return configure {
|
||||
customPref(
|
||||
BadgeDisplay112.Model(
|
||||
badge = state.inAppPayment.data.badge!!.let { Badges.fromDatabaseBadge(it) },
|
||||
withDisplayText = false
|
||||
)
|
||||
)
|
||||
|
||||
space(12.dp)
|
||||
|
||||
presentTitleAndSubtitle(requireContext(), state.inAppPayment)
|
||||
|
||||
space(16.dp)
|
||||
|
||||
if (state.loading) {
|
||||
space(16.dp)
|
||||
customPref(IndeterminateLoadingCircle)
|
||||
space(16.dp)
|
||||
return@configure
|
||||
}
|
||||
|
||||
state.gatewayOrderStrategy.orderedGateways.forEach { gateway ->
|
||||
when (gateway) {
|
||||
InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING -> error("Unsupported payment method.")
|
||||
InAppPaymentData.PaymentMethodType.GOOGLE_PAY -> renderGooglePayButton(state)
|
||||
InAppPaymentData.PaymentMethodType.PAYPAL -> renderPayPalButton(state)
|
||||
InAppPaymentData.PaymentMethodType.CARD -> renderCreditCardButton(state)
|
||||
InAppPaymentData.PaymentMethodType.SEPA_DEBIT -> renderSEPADebitButton(state)
|
||||
InAppPaymentData.PaymentMethodType.IDEAL -> renderIDEALButton(state)
|
||||
InAppPaymentData.PaymentMethodType.UNKNOWN -> error("Unsupported payment method.")
|
||||
return when (state) {
|
||||
GatewaySelectorState.Loading -> {
|
||||
configure {
|
||||
space(16.dp)
|
||||
customPref(IndeterminateLoadingCircle)
|
||||
space(16.dp)
|
||||
}
|
||||
}
|
||||
is GatewaySelectorState.Ready -> {
|
||||
configure {
|
||||
customPref(
|
||||
BadgeDisplay112.Model(
|
||||
badge = state.inAppPayment.data.badge!!.let { Badges.fromDatabaseBadge(it) },
|
||||
withDisplayText = false
|
||||
)
|
||||
)
|
||||
|
||||
space(16.dp)
|
||||
space(12.dp)
|
||||
|
||||
presentTitleAndSubtitle(requireContext(), state.inAppPayment)
|
||||
|
||||
space(16.dp)
|
||||
|
||||
state.gatewayOrderStrategy.orderedGateways.forEach { gateway ->
|
||||
when (gateway) {
|
||||
InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING -> error("Unsupported payment method.")
|
||||
InAppPaymentData.PaymentMethodType.GOOGLE_PAY -> renderGooglePayButton(state)
|
||||
InAppPaymentData.PaymentMethodType.PAYPAL -> renderPayPalButton(state)
|
||||
InAppPaymentData.PaymentMethodType.CARD -> renderCreditCardButton(state)
|
||||
InAppPaymentData.PaymentMethodType.SEPA_DEBIT -> renderSEPADebitButton(state)
|
||||
InAppPaymentData.PaymentMethodType.IDEAL -> renderIDEALButton(state)
|
||||
InAppPaymentData.PaymentMethodType.UNKNOWN -> error("Unsupported payment method.")
|
||||
}
|
||||
}
|
||||
|
||||
space(16.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.renderGooglePayButton(state: GatewaySelectorState) {
|
||||
private fun DSLConfiguration.renderGooglePayButton(state: GatewaySelectorState.Ready) {
|
||||
if (state.isGooglePayAvailable) {
|
||||
space(16.dp)
|
||||
|
||||
@@ -115,7 +119,7 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.renderPayPalButton(state: GatewaySelectorState) {
|
||||
private fun DSLConfiguration.renderPayPalButton(state: GatewaySelectorState.Ready) {
|
||||
if (state.isPayPalAvailable) {
|
||||
space(16.dp)
|
||||
|
||||
@@ -134,7 +138,7 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.renderCreditCardButton(state: GatewaySelectorState) {
|
||||
private fun DSLConfiguration.renderCreditCardButton(state: GatewaySelectorState.Ready) {
|
||||
if (state.isCreditCardAvailable) {
|
||||
space(16.dp)
|
||||
|
||||
@@ -153,7 +157,7 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.renderSEPADebitButton(state: GatewaySelectorState) {
|
||||
private fun DSLConfiguration.renderSEPADebitButton(state: GatewaySelectorState.Ready) {
|
||||
if (state.isSEPADebitAvailable) {
|
||||
space(16.dp)
|
||||
|
||||
@@ -162,7 +166,7 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
icon = DSLSettingsIcon.from(R.drawable.bank_transfer),
|
||||
disableOnClick = true,
|
||||
onClick = {
|
||||
val price = args.inAppPayment.data.amount!!.toFiatMoney()
|
||||
val price = state.inAppPayment.data.amount!!.toFiatMoney()
|
||||
if (state.sepaEuroMaximum != null &&
|
||||
price.currency == CurrencyUtil.EURO &&
|
||||
price.amount > state.sepaEuroMaximum.amount
|
||||
@@ -181,7 +185,7 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.renderIDEALButton(state: GatewaySelectorState) {
|
||||
private fun DSLConfiguration.renderIDEALButton(state: GatewaySelectorState.Ready) {
|
||||
if (state.isIDEALAvailable) {
|
||||
space(16.dp)
|
||||
|
||||
|
||||
@@ -7,17 +7,15 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.getAvaila
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.payments.currency.CurrencyUtil
|
||||
import org.whispersystems.signalservice.api.services.DonationsService
|
||||
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
|
||||
import java.util.Locale
|
||||
|
||||
class GatewaySelectorRepository(
|
||||
private val donationsService: DonationsService
|
||||
) {
|
||||
object GatewaySelectorRepository {
|
||||
fun getAvailableGatewayConfiguration(currencyCode: String): Single<GatewayConfiguration> {
|
||||
return Single.fromCallable {
|
||||
donationsService.getDonationsConfiguration(Locale.getDefault())
|
||||
AppDependencies.donationsService.getDonationsConfiguration(Locale.getDefault())
|
||||
}.flatMap { it.flattenResult() }
|
||||
.map { configuration ->
|
||||
val available = configuration.getAvailablePaymentMethods(currencyCode).map {
|
||||
|
||||
@@ -3,14 +3,17 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate.g
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
|
||||
data class GatewaySelectorState(
|
||||
val gatewayOrderStrategy: GatewayOrderStrategy,
|
||||
val inAppPayment: InAppPaymentTable.InAppPayment,
|
||||
val loading: Boolean = true,
|
||||
val isGooglePayAvailable: Boolean = false,
|
||||
val isPayPalAvailable: Boolean = false,
|
||||
val isCreditCardAvailable: Boolean = false,
|
||||
val isSEPADebitAvailable: Boolean = false,
|
||||
val isIDEALAvailable: Boolean = false,
|
||||
val sepaEuroMaximum: FiatMoney? = null
|
||||
)
|
||||
sealed interface GatewaySelectorState {
|
||||
data object Loading : GatewaySelectorState
|
||||
|
||||
data class Ready(
|
||||
val gatewayOrderStrategy: GatewayOrderStrategy,
|
||||
val inAppPayment: InAppPaymentTable.InAppPayment,
|
||||
val isGooglePayAvailable: Boolean = false,
|
||||
val isPayPalAvailable: Boolean = false,
|
||||
val isCreditCardAvailable: Boolean = false,
|
||||
val isSEPADebitAvailable: Boolean = false,
|
||||
val isIDEALAvailable: Boolean = false,
|
||||
val sepaEuroMaximum: FiatMoney? = null
|
||||
) : GatewaySelectorState
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
@@ -10,47 +9,38 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.GooglePayRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppDonations
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
|
||||
class GatewaySelectorViewModel(
|
||||
args: GatewaySelectorBottomSheetArgs,
|
||||
repository: GooglePayRepository,
|
||||
private val gatewaySelectorRepository: GatewaySelectorRepository
|
||||
repository: GooglePayRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val store = RxStore(
|
||||
GatewaySelectorState(
|
||||
gatewayOrderStrategy = GatewayOrderStrategy.getStrategy(),
|
||||
inAppPayment = args.inAppPayment,
|
||||
isCreditCardAvailable = InAppDonations.isDonationsPaymentSourceAvailable(PaymentSourceType.Stripe.CreditCard, args.inAppPayment.type),
|
||||
isGooglePayAvailable = InAppDonations.isDonationsPaymentSourceAvailable(PaymentSourceType.Stripe.GooglePay, args.inAppPayment.type),
|
||||
isPayPalAvailable = InAppDonations.isDonationsPaymentSourceAvailable(PaymentSourceType.PayPal, args.inAppPayment.type),
|
||||
isSEPADebitAvailable = InAppDonations.isDonationsPaymentSourceAvailable(PaymentSourceType.Stripe.SEPADebit, args.inAppPayment.type),
|
||||
isIDEALAvailable = InAppDonations.isDonationsPaymentSourceAvailable(PaymentSourceType.Stripe.IDEAL, args.inAppPayment.type)
|
||||
)
|
||||
)
|
||||
private val store = RxStore<GatewaySelectorState>(GatewaySelectorState.Loading)
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
val state = store.stateFlowable
|
||||
|
||||
init {
|
||||
val inAppPayment = InAppPaymentsRepository.requireInAppPayment(args.inAppPaymentId)
|
||||
val isGooglePayAvailable = repository.isGooglePayAvailable().toSingleDefault(true).onErrorReturnItem(false)
|
||||
val gatewayConfiguration = gatewaySelectorRepository.getAvailableGatewayConfiguration(currencyCode = args.inAppPayment.data.amount!!.currencyCode)
|
||||
val gatewayConfiguration = inAppPayment.flatMap { GatewaySelectorRepository.getAvailableGatewayConfiguration(currencyCode = it.data.amount!!.currencyCode) }
|
||||
|
||||
disposables += Single.zip(isGooglePayAvailable, gatewayConfiguration, ::Pair).subscribeBy { (googlePayAvailable, gatewayConfiguration) ->
|
||||
disposables += Single.zip(inAppPayment, isGooglePayAvailable, gatewayConfiguration, ::Triple).subscribeBy { (inAppPayment, googlePayAvailable, gatewayConfiguration) ->
|
||||
SignalStore.inAppPayments.isGooglePayReady = googlePayAvailable
|
||||
store.update {
|
||||
it.copy(
|
||||
loading = false,
|
||||
isCreditCardAvailable = it.isCreditCardAvailable && gatewayConfiguration.availableGateways.contains(InAppPaymentData.PaymentMethodType.CARD),
|
||||
isGooglePayAvailable = it.isGooglePayAvailable && googlePayAvailable && gatewayConfiguration.availableGateways.contains(InAppPaymentData.PaymentMethodType.GOOGLE_PAY),
|
||||
isPayPalAvailable = it.isPayPalAvailable && gatewayConfiguration.availableGateways.contains(InAppPaymentData.PaymentMethodType.PAYPAL),
|
||||
isSEPADebitAvailable = it.isSEPADebitAvailable && gatewayConfiguration.availableGateways.contains(InAppPaymentData.PaymentMethodType.SEPA_DEBIT),
|
||||
isIDEALAvailable = it.isIDEALAvailable && gatewayConfiguration.availableGateways.contains(InAppPaymentData.PaymentMethodType.IDEAL),
|
||||
GatewaySelectorState.Ready(
|
||||
gatewayOrderStrategy = GatewayOrderStrategy.getStrategy(),
|
||||
inAppPayment = inAppPayment,
|
||||
isCreditCardAvailable = InAppDonations.isDonationsPaymentSourceAvailable(PaymentSourceType.Stripe.CreditCard, inAppPayment.type) && gatewayConfiguration.availableGateways.contains(InAppPaymentData.PaymentMethodType.CARD),
|
||||
isGooglePayAvailable = InAppDonations.isDonationsPaymentSourceAvailable(PaymentSourceType.Stripe.GooglePay, inAppPayment.type) && googlePayAvailable && gatewayConfiguration.availableGateways.contains(InAppPaymentData.PaymentMethodType.GOOGLE_PAY),
|
||||
isPayPalAvailable = InAppDonations.isDonationsPaymentSourceAvailable(PaymentSourceType.PayPal, inAppPayment.type) && gatewayConfiguration.availableGateways.contains(InAppPaymentData.PaymentMethodType.PAYPAL),
|
||||
isSEPADebitAvailable = InAppDonations.isDonationsPaymentSourceAvailable(PaymentSourceType.Stripe.SEPADebit, inAppPayment.type) && gatewayConfiguration.availableGateways.contains(InAppPaymentData.PaymentMethodType.SEPA_DEBIT),
|
||||
isIDEALAvailable = InAppDonations.isDonationsPaymentSourceAvailable(PaymentSourceType.Stripe.IDEAL, inAppPayment.type) && gatewayConfiguration.availableGateways.contains(InAppPaymentData.PaymentMethodType.IDEAL),
|
||||
sepaEuroMaximum = gatewayConfiguration.sepaEuroMaximum
|
||||
)
|
||||
}
|
||||
@@ -63,16 +53,8 @@ class GatewaySelectorViewModel(
|
||||
}
|
||||
|
||||
fun updateInAppPaymentMethod(inAppPaymentMethodType: InAppPaymentData.PaymentMethodType): Single<InAppPaymentTable.InAppPayment> {
|
||||
return gatewaySelectorRepository.setInAppPaymentMethodType(store.state.inAppPayment, inAppPaymentMethodType).observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
val state = store.state as GatewaySelectorState.Ready
|
||||
|
||||
class Factory(
|
||||
private val args: GatewaySelectorBottomSheetArgs,
|
||||
private val repository: GooglePayRepository,
|
||||
private val gatewaySelectorRepository: GatewaySelectorRepository = GatewaySelectorRepository(AppDependencies.donationsService)
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(GatewaySelectorViewModel(args, repository, gatewaySelectorRepository)) as T
|
||||
}
|
||||
return GatewaySelectorRepository.setInAppPaymentMethodType(state.inAppPayment, inAppPaymentMethodType).observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,12 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate.p
|
||||
import android.content.DialogInterface
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.concurrent.SignalDispatchers
|
||||
import org.signal.core.util.dp
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
@@ -15,6 +19,8 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFrag
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewaySelectorBottomSheet.Companion.presentTitleAndSubtitle
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
|
||||
/**
|
||||
* Bottom sheet for final order confirmation from PayPal
|
||||
@@ -32,7 +38,13 @@ class PayPalCompleteOrderBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
BadgeDisplay112.register(adapter)
|
||||
PayPalCompleteOrderPaymentItem.register(adapter)
|
||||
|
||||
adapter.submitList(getConfiguration().toMappingModelList())
|
||||
lifecycleScope.launch {
|
||||
val inAppPayment = withContext(SignalDispatchers.IO) {
|
||||
SignalDatabase.inAppPayments.getById(args.inAppPaymentId)!!
|
||||
}
|
||||
|
||||
adapter.submitList(getConfiguration(inAppPayment).toMappingModelList())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
@@ -40,18 +52,18 @@ class PayPalCompleteOrderBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to didConfirmOrder))
|
||||
}
|
||||
|
||||
private fun getConfiguration(): DSLConfiguration {
|
||||
private fun getConfiguration(inAppPayment: InAppPaymentTable.InAppPayment): DSLConfiguration {
|
||||
return configure {
|
||||
customPref(
|
||||
BadgeDisplay112.Model(
|
||||
badge = Badges.fromDatabaseBadge(args.inAppPayment.data.badge!!),
|
||||
badge = Badges.fromDatabaseBadge(inAppPayment.data.badge!!),
|
||||
withDisplayText = false
|
||||
)
|
||||
)
|
||||
|
||||
space(12.dp)
|
||||
|
||||
presentTitleAndSubtitle(requireContext(), args.inAppPayment)
|
||||
presentTitleAndSubtitle(requireContext(), inAppPayment)
|
||||
|
||||
space(24.dp)
|
||||
|
||||
|
||||
@@ -21,10 +21,8 @@ import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.getParcelableCompat
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toErrorSource
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentProcessorAction
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentProcessorActionResult
|
||||
@@ -32,6 +30,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.donate.In
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.databinding.DonationInProgressFragmentBinding
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
@@ -50,9 +49,7 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
private val binding by ViewBinderDelegate(DonationInProgressFragmentBinding::bind)
|
||||
private val args: PayPalPaymentInProgressFragmentArgs by navArgs()
|
||||
|
||||
private val viewModel: PayPalPaymentInProgressViewModel by navGraphViewModels(R.id.checkout_flow, factoryProducer = {
|
||||
PayPalPaymentInProgressViewModel.Factory()
|
||||
})
|
||||
private val viewModel: PayPalPaymentInProgressViewModel by navGraphViewModels(R.id.checkout_flow)
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
isCancelable = false
|
||||
@@ -67,21 +64,18 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
when (args.action) {
|
||||
InAppPaymentProcessorAction.PROCESS_NEW_IN_APP_PAYMENT -> {
|
||||
viewModel.processNewDonation(
|
||||
args.inAppPayment!!,
|
||||
if (args.inAppPaymentType.recurring) {
|
||||
this::monthlyConfirmationPipeline
|
||||
} else {
|
||||
this::oneTimeConfirmationPipeline
|
||||
}
|
||||
args.inAppPaymentId!!,
|
||||
this::oneTimeConfirmationPipeline,
|
||||
this::monthlyConfirmationPipeline
|
||||
)
|
||||
}
|
||||
|
||||
InAppPaymentProcessorAction.UPDATE_SUBSCRIPTION -> {
|
||||
viewModel.updateSubscription(args.inAppPayment!!)
|
||||
viewModel.updateSubscription(args.inAppPaymentId!!)
|
||||
}
|
||||
|
||||
InAppPaymentProcessorAction.CANCEL_SUBSCRIPTION -> {
|
||||
viewModel.cancelSubscription(args.inAppPaymentType.requireSubscriberType())
|
||||
viewModel.cancelSubscription(InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,8 +98,7 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
bundleOf(
|
||||
REQUEST_KEY to InAppPaymentProcessorActionResult(
|
||||
action = args.action,
|
||||
inAppPayment = args.inAppPayment,
|
||||
inAppPaymentType = args.inAppPaymentType,
|
||||
inAppPaymentId = args.inAppPaymentId,
|
||||
status = InAppPaymentProcessorActionResult.Status.FAILURE
|
||||
)
|
||||
)
|
||||
@@ -120,8 +113,7 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
bundleOf(
|
||||
REQUEST_KEY to InAppPaymentProcessorActionResult(
|
||||
action = args.action,
|
||||
inAppPayment = args.inAppPayment,
|
||||
inAppPaymentType = args.inAppPaymentType,
|
||||
inAppPaymentId = args.inAppPaymentId,
|
||||
status = InAppPaymentProcessorActionResult.Status.SUCCESS
|
||||
)
|
||||
)
|
||||
@@ -133,11 +125,7 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
}
|
||||
|
||||
private fun getProcessingStatus(): String {
|
||||
return if (args.inAppPaymentType == InAppPaymentType.RECURRING_BACKUP) {
|
||||
getString(R.string.InAppPaymentInProgressFragment__processing_payment)
|
||||
} else {
|
||||
getString(R.string.InAppPaymentInProgressFragment__processing_donation)
|
||||
}
|
||||
return getString(R.string.InAppPaymentInProgressFragment__processing_donation)
|
||||
}
|
||||
|
||||
private fun oneTimeConfirmationPipeline(inAppPaymentId: InAppPaymentTable.InAppPaymentId): Completable {
|
||||
@@ -209,7 +197,9 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
if (result != null) {
|
||||
emitter.onSuccess(result.copy(paymentId = createPaymentIntentResponse.paymentId))
|
||||
} else {
|
||||
emitter.onError(DonationError.UserCancelledPaymentError(args.inAppPaymentType.toErrorSource()))
|
||||
disposables += viewModel.getInAppPaymentType(args.inAppPaymentId!!).subscribeBy {
|
||||
emitter.onError(DonationError.UserCancelledPaymentError(it.toErrorSource()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,7 +227,9 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
if (result) {
|
||||
emitter.onSuccess(PayPalPaymentMethodId(createPaymentIntentResponse.token))
|
||||
} else {
|
||||
emitter.onError(DonationError.UserCancelledPaymentError(args.inAppPaymentType.toErrorSource()))
|
||||
disposables += viewModel.getInAppPaymentType(args.inAppPaymentId!!).subscribeBy {
|
||||
emitter.onError(DonationError.UserCancelledPaymentError(it.toErrorSource()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,39 +1,34 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.paypal
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.core.SingleSource
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.signal.donations.PayPalPaymentSource
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toErrorSource
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toPaymentSourceType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.PayPalRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentProcessorStage
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.RequiredActionHandler
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.SharedInAppPaymentPipeline
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceSubscriptionSyncRequestJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import org.whispersystems.signalservice.api.util.Preconditions
|
||||
|
||||
class PayPalPaymentInProgressViewModel(
|
||||
private val payPalRepository: PayPalRepository
|
||||
) : ViewModel() {
|
||||
class PayPalPaymentInProgressViewModel : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(PayPalPaymentInProgressViewModel::class.java)
|
||||
@@ -63,26 +58,36 @@ class PayPalPaymentInProgressViewModel(
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
fun updateSubscription(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
fun getInAppPaymentType(inAppPaymentId: InAppPaymentTable.InAppPaymentId): Single<InAppPaymentType> {
|
||||
return InAppPaymentsRepository.requireInAppPayment(inAppPaymentId).map { it.type }.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun updateSubscription(inAppPaymentId: InAppPaymentTable.InAppPaymentId) {
|
||||
Log.d(TAG, "Beginning subscription update...", true)
|
||||
|
||||
store.update { InAppPaymentProcessorStage.PAYMENT_PIPELINE }
|
||||
disposables += RecurringInAppPaymentRepository.cancelActiveSubscriptionIfNecessary(inAppPayment.type.requireSubscriberType()).andThen(
|
||||
SingleSource<InAppPaymentTable.InAppPayment> {
|
||||
val freshPayment = SignalDatabase.inAppPayments.moveToTransacting(inAppPayment.id)!!
|
||||
RecurringInAppPaymentRepository.setSubscriptionLevelSync(freshPayment)
|
||||
val iap = InAppPaymentsRepository.requireInAppPayment(inAppPaymentId)
|
||||
|
||||
disposables += iap.flatMap { inAppPayment ->
|
||||
RecurringInAppPaymentRepository.cancelActiveSubscriptionIfNecessary(inAppPayment.type.requireSubscriberType()).andThen(
|
||||
SingleSource<InAppPaymentTable.InAppPayment> {
|
||||
val freshPayment = SignalDatabase.inAppPayments.moveToTransacting(inAppPayment.id)!!
|
||||
RecurringInAppPaymentRepository.setSubscriptionLevelSync(freshPayment)
|
||||
}
|
||||
).flatMap {
|
||||
SharedInAppPaymentPipeline.awaitRedemption(it, PaymentSourceType.PayPal)
|
||||
}
|
||||
).flatMapCompletable {
|
||||
SharedInAppPaymentPipeline.awaitRedemption(it, PaymentSourceType.PayPal)
|
||||
}.subscribeBy(
|
||||
onComplete = {
|
||||
onSuccess = {
|
||||
Log.w(TAG, "Completed subscription update", true)
|
||||
store.update { InAppPaymentProcessorStage.COMPLETE }
|
||||
},
|
||||
onError = { throwable ->
|
||||
Log.w(TAG, "Failed to update subscription", throwable, true)
|
||||
store.update { InAppPaymentProcessorStage.FAILED }
|
||||
InAppPaymentsRepository.handlePipelineError(inAppPayment.id, DonationErrorSource.MONTHLY, PaymentSourceType.PayPal, throwable)
|
||||
SignalExecutors.BOUNDED_IO.execute {
|
||||
InAppPaymentsRepository.handlePipelineError(inAppPaymentId, throwable)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -106,34 +111,29 @@ class PayPalPaymentInProgressViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
fun processNewDonation(inAppPayment: InAppPaymentTable.InAppPayment, requiredActionHandler: RequiredActionHandler) {
|
||||
Log.d(TAG, "Proceeding with InAppPayment::${inAppPayment.id} of type ${inAppPayment.type}...", true)
|
||||
|
||||
check(inAppPayment.data.paymentMethodType.toPaymentSourceType() == PaymentSourceType.PayPal)
|
||||
|
||||
fun processNewDonation(
|
||||
inAppPaymentId: InAppPaymentTable.InAppPaymentId,
|
||||
oneTimeActionHandler: RequiredActionHandler,
|
||||
monthlyActionHandler: RequiredActionHandler
|
||||
) {
|
||||
store.update { InAppPaymentProcessorStage.PAYMENT_PIPELINE }
|
||||
|
||||
disposables += SharedInAppPaymentPipeline.awaitTransaction(
|
||||
inAppPayment,
|
||||
inAppPaymentId,
|
||||
PayPalPaymentSource(),
|
||||
requiredActionHandler
|
||||
oneTimeActionHandler,
|
||||
monthlyActionHandler
|
||||
).subscribeOn(Schedulers.io()).subscribeBy(
|
||||
onComplete = {
|
||||
Log.d(TAG, "Finished ${inAppPayment.type} payment pipeline...", true)
|
||||
onSuccess = {
|
||||
Log.d(TAG, "Finished ${it.type} payment pipeline...", true)
|
||||
store.update { InAppPaymentProcessorStage.COMPLETE }
|
||||
},
|
||||
onError = {
|
||||
store.update { InAppPaymentProcessorStage.FAILED }
|
||||
SharedInAppPaymentPipeline.handleError(it, inAppPayment.id, PaymentSourceType.PayPal, inAppPayment.type.toErrorSource())
|
||||
SignalExecutors.BOUNDED_IO.execute {
|
||||
InAppPaymentsRepository.handlePipelineError(inAppPaymentId, it)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val payPalRepository: PayPalRepository = PayPalRepository(AppDependencies.donationsService)
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(PayPalPaymentInProgressViewModel(payPalRepository)) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ class Stripe3DSDialogFragment : DialogFragment(R.layout.donation_webview_fragmen
|
||||
)
|
||||
)
|
||||
|
||||
if (RemoteConfig.internalUser && args.inAppPayment.data.paymentMethodType == InAppPaymentData.PaymentMethodType.IDEAL) {
|
||||
if (RemoteConfig.internalUser && args.waitingForAuthPayment.data.paymentMethodType == InAppPaymentData.PaymentMethodType.IDEAL) {
|
||||
val openApp = MaterialButton(requireContext()).apply {
|
||||
text = "Open App"
|
||||
layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
|
||||
@@ -119,7 +119,7 @@ class Stripe3DSDialogFragment : DialogFragment(R.layout.donation_webview_fragmen
|
||||
progress.show(parentFragmentManager, null)
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
SignalDatabase.inAppPayments.update(args.inAppPayment)
|
||||
SignalDatabase.inAppPayments.update(args.waitingForAuthPayment)
|
||||
}
|
||||
|
||||
progress.dismissAllowingStateLoss()
|
||||
|
||||
@@ -21,7 +21,6 @@ import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.getParcelableCompat
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.signal.donations.StripeApi
|
||||
import org.signal.donations.StripeIntentAccessor
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -35,6 +34,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.donate.In
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.databinding.DonationInProgressFragmentBinding
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
@@ -67,15 +67,15 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
viewModel.onBeginNewAction()
|
||||
when (args.action) {
|
||||
InAppPaymentProcessorAction.PROCESS_NEW_IN_APP_PAYMENT -> {
|
||||
viewModel.processNewDonation(args.inAppPayment!!, this::handleRequiredAction)
|
||||
viewModel.processNewDonation(args.inAppPaymentId!!, this::handleRequiredAction, this::handleRequiredAction)
|
||||
}
|
||||
|
||||
InAppPaymentProcessorAction.UPDATE_SUBSCRIPTION -> {
|
||||
viewModel.updateSubscription(args.inAppPayment!!)
|
||||
viewModel.updateSubscription(args.inAppPaymentId!!)
|
||||
}
|
||||
|
||||
InAppPaymentProcessorAction.CANCEL_SUBSCRIPTION -> {
|
||||
viewModel.cancelSubscription(args.inAppPaymentType.requireSubscriberType())
|
||||
viewModel.cancelSubscription(InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,8 +98,7 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
bundleOf(
|
||||
REQUEST_KEY to InAppPaymentProcessorActionResult(
|
||||
action = args.action,
|
||||
inAppPayment = args.inAppPayment,
|
||||
inAppPaymentType = args.inAppPaymentType,
|
||||
inAppPaymentId = args.inAppPaymentId,
|
||||
status = InAppPaymentProcessorActionResult.Status.FAILURE
|
||||
)
|
||||
)
|
||||
@@ -114,8 +113,7 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
bundleOf(
|
||||
REQUEST_KEY to InAppPaymentProcessorActionResult(
|
||||
action = args.action,
|
||||
inAppPayment = args.inAppPayment,
|
||||
inAppPaymentType = args.inAppPaymentType,
|
||||
inAppPaymentId = args.inAppPaymentId,
|
||||
status = InAppPaymentProcessorActionResult.Status.SUCCESS
|
||||
)
|
||||
)
|
||||
@@ -127,11 +125,7 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
}
|
||||
|
||||
private fun getProcessingStatus(): String {
|
||||
return if (args.inAppPaymentType == InAppPaymentType.RECURRING_BACKUP) {
|
||||
getString(R.string.InAppPaymentInProgressFragment__processing_payment)
|
||||
} else {
|
||||
getString(R.string.InAppPaymentInProgressFragment__processing_donation)
|
||||
}
|
||||
return getString(R.string.InAppPaymentInProgressFragment__processing_donation)
|
||||
}
|
||||
|
||||
private fun handleRequiredAction(inAppPaymentId: InAppPaymentTable.InAppPaymentId): Completable {
|
||||
@@ -183,11 +177,13 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
if (result != null) {
|
||||
emitter.onSuccess(result)
|
||||
} else {
|
||||
val didLaunchExternal = bundle.getBoolean(Stripe3DSDialogFragment.LAUNCHED_EXTERNAL, false)
|
||||
if (didLaunchExternal) {
|
||||
emitter.onError(DonationError.UserLaunchedExternalApplication(args.inAppPaymentType.toErrorSource()))
|
||||
} else {
|
||||
emitter.onError(DonationError.UserCancelledPaymentError(args.inAppPaymentType.toErrorSource()))
|
||||
disposables += viewModel.getInAppPaymentType(args.inAppPaymentId!!).subscribeBy { inAppPaymentType ->
|
||||
val didLaunchExternal = bundle.getBoolean(Stripe3DSDialogFragment.LAUNCHED_EXTERNAL, false)
|
||||
if (didLaunchExternal) {
|
||||
emitter.onError(DonationError.UserLaunchedExternalApplication(inAppPaymentType.toErrorSource()))
|
||||
} else {
|
||||
emitter.onError(DonationError.UserCancelledPaymentError(inAppPaymentType.toErrorSource()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,13 +12,13 @@ import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.GooglePayPaymentSource
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.signal.donations.PaymentSource
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.signal.donations.StripeApi
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toErrorSource
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toPaymentSourceType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentProcessorStage
|
||||
@@ -64,29 +64,29 @@ class StripePaymentInProgressViewModel : ViewModel() {
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
fun processNewDonation(inAppPayment: InAppPaymentTable.InAppPayment, requiredActionHandler: RequiredActionHandler) {
|
||||
Log.d(TAG, "Proceeding with InAppPayment::${inAppPayment.id} of type ${inAppPayment.type}...", true)
|
||||
|
||||
val paymentSourceProvider: PaymentSourceProvider = resolvePaymentSourceProvider(inAppPayment.type.toErrorSource())
|
||||
|
||||
check(inAppPayment.data.paymentMethodType.toPaymentSourceType() == paymentSourceProvider.paymentSourceType)
|
||||
|
||||
fun processNewDonation(inAppPaymentId: InAppPaymentTable.InAppPaymentId, oneTimeRequiredActionHandler: RequiredActionHandler, monthlyRequiredActionHandler: RequiredActionHandler) {
|
||||
store.update { InAppPaymentProcessorStage.PAYMENT_PIPELINE }
|
||||
val iap = InAppPaymentsRepository.requireInAppPayment(inAppPaymentId)
|
||||
|
||||
disposables += paymentSourceProvider.paymentSource.flatMapCompletable { paymentSource ->
|
||||
SharedInAppPaymentPipeline.awaitTransaction(
|
||||
inAppPayment,
|
||||
paymentSource,
|
||||
requiredActionHandler
|
||||
)
|
||||
disposables += iap.flatMap { inAppPayment ->
|
||||
resolvePaymentSourceProvider(inAppPayment.type.toErrorSource()).paymentSource.flatMap { paymentSource ->
|
||||
SharedInAppPaymentPipeline.awaitTransaction(
|
||||
inAppPaymentId,
|
||||
paymentSource,
|
||||
oneTimeRequiredActionHandler,
|
||||
monthlyRequiredActionHandler
|
||||
)
|
||||
}
|
||||
}.subscribeOn(Schedulers.io()).subscribeBy(
|
||||
onComplete = {
|
||||
Log.d(TAG, "Finished ${inAppPayment.type} payment pipeline...", true)
|
||||
onSuccess = {
|
||||
Log.d(TAG, "Finished ${it.type} payment pipeline...", true)
|
||||
store.update { InAppPaymentProcessorStage.COMPLETE }
|
||||
},
|
||||
onError = {
|
||||
store.update { InAppPaymentProcessorStage.FAILED }
|
||||
SharedInAppPaymentPipeline.handleError(it, inAppPayment.id, paymentSourceProvider.paymentSourceType, inAppPayment.type.toErrorSource())
|
||||
SignalExecutors.BOUNDED_IO.execute {
|
||||
InAppPaymentsRepository.handlePipelineError(inAppPaymentId, it)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -137,6 +137,10 @@ class StripePaymentInProgressViewModel : ViewModel() {
|
||||
this.stripePaymentData = StripePaymentData.IDEAL(bankData)
|
||||
}
|
||||
|
||||
fun getInAppPaymentType(inAppPaymentId: InAppPaymentTable.InAppPaymentId): Single<InAppPaymentType> {
|
||||
return InAppPaymentsRepository.requireInAppPayment(inAppPaymentId).map { it.type }.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
private fun requireNoPaymentInformation() {
|
||||
require(stripePaymentData == null)
|
||||
}
|
||||
@@ -162,22 +166,25 @@ class StripePaymentInProgressViewModel : ViewModel() {
|
||||
)
|
||||
}
|
||||
|
||||
fun updateSubscription(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
fun updateSubscription(inAppPaymentId: InAppPaymentTable.InAppPaymentId) {
|
||||
Log.d(TAG, "Beginning subscription update...", true)
|
||||
store.update { InAppPaymentProcessorStage.PAYMENT_PIPELINE }
|
||||
disposables += RecurringInAppPaymentRepository
|
||||
.cancelActiveSubscriptionIfNecessary(inAppPayment.type.requireSubscriberType())
|
||||
.andThen(RecurringInAppPaymentRepository.getPaymentSourceTypeOfLatestSubscription(inAppPayment.type.requireSubscriberType()))
|
||||
.flatMapCompletable { paymentSourceType ->
|
||||
val freshPayment = SignalDatabase.inAppPayments.moveToTransacting(inAppPayment.id)!!
|
||||
val iap = InAppPaymentsRepository.requireInAppPayment(inAppPaymentId)
|
||||
disposables += iap.flatMap { inAppPayment ->
|
||||
RecurringInAppPaymentRepository
|
||||
.cancelActiveSubscriptionIfNecessary(inAppPayment.type.requireSubscriberType())
|
||||
.andThen(RecurringInAppPaymentRepository.getPaymentSourceTypeOfLatestSubscription(inAppPayment.type.requireSubscriberType()))
|
||||
.flatMap { paymentSourceType ->
|
||||
val freshPayment = SignalDatabase.inAppPayments.moveToTransacting(inAppPayment.id)!!
|
||||
|
||||
Single.fromCallable {
|
||||
RecurringInAppPaymentRepository.setSubscriptionLevelSync(freshPayment)
|
||||
}.flatMapCompletable { SharedInAppPaymentPipeline.awaitRedemption(it, paymentSourceType) }
|
||||
}
|
||||
Single.fromCallable {
|
||||
RecurringInAppPaymentRepository.setSubscriptionLevelSync(freshPayment)
|
||||
}.flatMap { SharedInAppPaymentPipeline.awaitRedemption(it, paymentSourceType) }
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribeBy(
|
||||
onComplete = {
|
||||
onSuccess = {
|
||||
Log.w(TAG, "Completed subscription update", true)
|
||||
store.update { InAppPaymentProcessorStage.COMPLETE }
|
||||
},
|
||||
@@ -185,8 +192,7 @@ class StripePaymentInProgressViewModel : ViewModel() {
|
||||
Log.w(TAG, "Failed to update subscription", throwable, true)
|
||||
store.update { InAppPaymentProcessorStage.FAILED }
|
||||
SignalExecutors.BOUNDED_IO.execute {
|
||||
val paymentSourceType = InAppPaymentsRepository.getLatestPaymentMethodType(inAppPayment.type.requireSubscriberType()).toPaymentSourceType()
|
||||
InAppPaymentsRepository.handlePipelineError(inAppPayment.id, DonationErrorSource.MONTHLY, paymentSourceType, throwable)
|
||||
InAppPaymentsRepository.handlePipelineError(inAppPaymentId, throwable)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -42,9 +42,9 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.navigation.navGraphViewModels
|
||||
@@ -68,6 +68,7 @@ import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
/**
|
||||
* Collects SEPA Debit bank transfer details from the user to proceed with donation.
|
||||
@@ -75,7 +76,10 @@ import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
class BankTransferDetailsFragment : ComposeFragment(), InAppPaymentCheckoutDelegate.ErrorHandlerCallback {
|
||||
|
||||
private val args: BankTransferDetailsFragmentArgs by navArgs()
|
||||
private val viewModel: BankTransferDetailsViewModel by viewModels()
|
||||
|
||||
private val viewModel: BankTransferDetailsViewModel by viewModel {
|
||||
BankTransferDetailsViewModel(args.inAppPaymentId)
|
||||
}
|
||||
|
||||
private val stripePaymentViewModel: StripePaymentInProgressViewModel by navGraphViewModels(
|
||||
R.id.checkout_flow
|
||||
@@ -84,7 +88,7 @@ class BankTransferDetailsFragment : ComposeFragment(), InAppPaymentCheckoutDeleg
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
TemporaryScreenshotSecurity.bindToViewLifecycleOwner(this)
|
||||
|
||||
InAppPaymentCheckoutDelegate.ErrorHandler().attach(this, this, args.inAppPayment.id)
|
||||
InAppPaymentCheckoutDelegate.ErrorHandler().attach(this, this, args.inAppPaymentId)
|
||||
|
||||
setFragmentResultListener(StripePaymentInProgressFragment.REQUEST_KEY) { _, bundle ->
|
||||
val result: InAppPaymentProcessorActionResult = bundle.getParcelableCompat(StripePaymentInProgressFragment.REQUEST_KEY, InAppPaymentProcessorActionResult::class.java)!!
|
||||
@@ -98,17 +102,33 @@ class BankTransferDetailsFragment : ComposeFragment(), InAppPaymentCheckoutDeleg
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state: BankTransferDetailsState by viewModel.state
|
||||
val inAppPayment by viewModel.inAppPayment.collectAsStateWithLifecycle(null)
|
||||
|
||||
val donateLabel = remember(args.inAppPayment) {
|
||||
if (args.inAppPayment.type.recurring) { // TODO [message-requests] backups copy
|
||||
if (inAppPayment != null) {
|
||||
ReadyContent(
|
||||
state,
|
||||
viewModel,
|
||||
inAppPayment!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReadyContent(
|
||||
state: BankTransferDetailsState,
|
||||
viewModel: BankTransferDetailsViewModel,
|
||||
inAppPayment: InAppPaymentTable.InAppPayment
|
||||
) {
|
||||
val donateLabel = remember(inAppPayment) {
|
||||
if (inAppPayment.type.recurring) { // TODO [message-requests] backups copy
|
||||
getString(
|
||||
R.string.BankTransferDetailsFragment__donate_s_month,
|
||||
FiatMoneyUtil.format(resources, args.inAppPayment.data.amount!!.toFiatMoney(), FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
FiatMoneyUtil.format(resources, inAppPayment.data.amount!!.toFiatMoney(), FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
)
|
||||
} else {
|
||||
getString(
|
||||
R.string.BankTransferDetailsFragment__donate_s,
|
||||
FiatMoneyUtil.format(resources, args.inAppPayment.data.amount!!.toFiatMoney())
|
||||
FiatMoneyUtil.format(resources, inAppPayment.data.amount!!.toFiatMoney())
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -142,8 +162,7 @@ class BankTransferDetailsFragment : ComposeFragment(), InAppPaymentCheckoutDeleg
|
||||
findNavController().safeNavigate(
|
||||
BankTransferDetailsFragmentDirections.actionBankTransferDetailsFragmentToStripePaymentInProgressFragment(
|
||||
InAppPaymentProcessorAction.PROCESS_NEW_IN_APP_PAYMENT,
|
||||
args.inAppPayment,
|
||||
args.inAppPayment.type
|
||||
args.inAppPaymentId
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -202,7 +221,7 @@ private fun BankTransferDetailsContent(
|
||||
Scaffolds.Settings(
|
||||
title = "Bank transfer",
|
||||
onNavigationClick = onNavigationClick,
|
||||
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_left_24)
|
||||
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_start_24)
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = CenterHorizontally,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user