mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-14 05:53:19 +01:00
Compare commits
302 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3644466263 | ||
|
|
e9b43e7c25 | ||
|
|
e9e53d6d84 | ||
|
|
03f2feb335 | ||
|
|
3b657ea7bd | ||
|
|
2635ffcbc9 | ||
|
|
927c079cc4 | ||
|
|
a2f7afcb68 | ||
|
|
b6c033b075 | ||
|
|
31d0b81624 | ||
|
|
96ece3f424 | ||
|
|
2bc9926d97 | ||
|
|
f1537cb8a9 | ||
|
|
af8dee9c38 | ||
|
|
39f78273c0 | ||
|
|
d66a0f618d | ||
|
|
497a8188fd | ||
|
|
f3a475d0c8 | ||
|
|
8b828677de | ||
|
|
a050b37f3a | ||
|
|
c865ed0cdc | ||
|
|
918b792d83 | ||
|
|
28ecb37103 | ||
|
|
7c43462771 | ||
|
|
7e00739240 | ||
|
|
feae417af5 | ||
|
|
e5d55418ac | ||
|
|
5d8e0e370d | ||
|
|
e2bffd0fd3 | ||
|
|
8d2979d8ce | ||
|
|
288eda5bb1 | ||
|
|
fb111619d7 | ||
|
|
fb68f3fed1 | ||
|
|
791f1677fa | ||
|
|
632b76081a | ||
|
|
a474666ea7 | ||
|
|
b3c9ec9691 | ||
|
|
778db277c8 | ||
|
|
1800507604 | ||
|
|
b0aee1db05 | ||
|
|
919cbbd7ca | ||
|
|
93403a0d2c | ||
|
|
9867fa3f50 | ||
|
|
b79ec79644 | ||
|
|
961e9fd4b9 | ||
|
|
6d04c8ba42 | ||
|
|
bc94a92f68 | ||
|
|
9b9888565b | ||
|
|
a2a3dd28ee | ||
|
|
844dec06b1 | ||
|
|
5306a9dd7a | ||
|
|
cdd595432b | ||
|
|
fabec719ab | ||
|
|
04c14a82be | ||
|
|
51851fa5fe | ||
|
|
3c77a3d7aa | ||
|
|
7c9bab421a | ||
|
|
9d1960f065 | ||
|
|
ae4c0d1242 | ||
|
|
df3396633b | ||
|
|
9aea264305 | ||
|
|
866c232045 | ||
|
|
524ffd9d79 | ||
|
|
46ca979e59 | ||
|
|
c8bfc88bed | ||
|
|
030678b029 | ||
|
|
e4b99e5cef | ||
|
|
367c0d0a8d | ||
|
|
6dfe3b9c33 | ||
|
|
3aa4e75ef3 | ||
|
|
570a475229 | ||
|
|
2421bbdabb | ||
|
|
39756fd0d4 | ||
|
|
7a69c96746 | ||
|
|
f0acc39829 | ||
|
|
a27daddb70 | ||
|
|
fd47d28026 | ||
|
|
fe853f7b65 | ||
|
|
c89fbbe49f | ||
|
|
5453f101ff | ||
|
|
87cbe305f0 | ||
|
|
b298cb6f89 | ||
|
|
65e1ffaed4 | ||
|
|
43b5cb0641 | ||
|
|
f73d929feb | ||
|
|
85647f1258 | ||
|
|
9164668b8b | ||
|
|
76aaf22429 | ||
|
|
3d7162cdd3 | ||
|
|
ed9a945f05 | ||
|
|
f8d7c27583 | ||
|
|
4e1072b8da | ||
|
|
057715226f | ||
|
|
0f8fdda884 | ||
|
|
393b88fb1f | ||
|
|
639c3ef883 | ||
|
|
ad4142db1a | ||
|
|
5182987735 | ||
|
|
7f5bfc210b | ||
|
|
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,9 +21,9 @@ plugins {
|
||||
|
||||
apply(from = "static-ips.gradle.kts")
|
||||
|
||||
val canonicalVersionCode = 1535
|
||||
val canonicalVersionName = "7.39.5"
|
||||
val currentHotfixVersion = 1
|
||||
val canonicalVersionCode = 1545
|
||||
val canonicalVersionName = "7.42.2"
|
||||
val currentHotfixVersion = 0
|
||||
val maxHotfixVersions = 100
|
||||
|
||||
val keystores: Map<String, Properties?> = mapOf("debug" to loadKeystoreProperties("keystore.debug.properties"))
|
||||
@@ -210,7 +210,8 @@ android {
|
||||
buildConfigField("String[]", "SIGNAL_CDSI_IPS", rootProject.extra["cdsi_ips"] as String)
|
||||
buildConfigField("String[]", "SIGNAL_SVR2_IPS", rootProject.extra["svr2_ips"] as String)
|
||||
buildConfigField("String", "SIGNAL_AGENT", "\"OWA\"")
|
||||
buildConfigField("String", "SVR2_MRENCLAVE", "\"9314436a9a144992bb3680770ea5fd7934a7ffd29257844a33763a238903d570\"")
|
||||
buildConfigField("String", "SVR2_MRENCLAVE_LEGACY", "\"9314436a9a144992bb3680770ea5fd7934a7ffd29257844a33763a238903d570\"")
|
||||
buildConfigField("String", "SVR2_MRENCLAVE", "\"093be9ea32405e85ae28dbb48eb668aebeb7dbe29517b9b86ad4bec4dfe0e6a6\"")
|
||||
buildConfigField("String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\"")
|
||||
buildConfigField("String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0LUlT9vALgh/f2DPVOOmR0RW6bgRvc7DSF20V/omg+YBw==\"")
|
||||
buildConfigField("String", "GENERIC_SERVER_PUBLIC_PARAMS", "\"AByD873dTilmOSG0TjKrvpeaKEsUmIO8Vx9BeMmftwUs9v7ikPwM8P3OHyT0+X3EUMZrSe9VUp26Wai51Q9I8mdk0hX/yo7CeFGJyzoOqn8e/i4Ygbn5HoAyXJx5eXfIbqpc0bIxzju4H/HOQeOpt6h742qii5u/cbwOhFZCsMIbElZTaeU+BWMBQiZHIGHT5IE0qCordQKZ5iPZom0HeFa8Yq0ShuEyAl0WINBiY6xE3H/9WnvzXBbMuuk//eRxXgzO8ieCeK8FwQNxbfXqZm6Ro1cMhCOF3u7xoX83QhpN\"")
|
||||
@@ -370,6 +371,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") {
|
||||
@@ -393,7 +395,8 @@ android {
|
||||
buildConfigField("String", "SIGNAL_CDN3_URL", "\"https://cdn3-staging.signal.org\"")
|
||||
buildConfigField("String", "SIGNAL_CDSI_URL", "\"https://cdsi.staging.signal.org\"")
|
||||
buildConfigField("String", "SIGNAL_SVR2_URL", "\"https://svr2.staging.signal.org\"")
|
||||
buildConfigField("String", "SVR2_MRENCLAVE", "\"38e01eff4fe357dc0b0e8ef7a44b4abc5489fbccba3a78780f3872c277f62bf3\"")
|
||||
buildConfigField("String", "SVR2_MRENCLAVE_LEGACY", "\"38e01eff4fe357dc0b0e8ef7a44b4abc5489fbccba3a78780f3872c277f62bf3\"")
|
||||
buildConfigField("String", "SVR2_MRENCLAVE", "\"2e8cefe6e3f389d8426adb24e9b7fb7adf10902c96f06f7bbcee36277711ed91\"")
|
||||
buildConfigField("String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\"")
|
||||
buildConfigField("String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCNWI+wwkyFCqNDXz/qxl1gAntuCJtSfq9OC3NkdhQlgYQ==\"")
|
||||
buildConfigField("String", "GENERIC_SERVER_PUBLIC_PARAMS", "\"AHILOIrFPXX9laLbalbA9+L1CXpSbM/bTJXZGZiuyK1JaI6dK5FHHWL6tWxmHKYAZTSYmElmJ5z2A5YcirjO/yfoemE03FItyaf8W1fE4p14hzb5qnrmfXUSiAIVrhaXVwIwSzH6RL/+EO8jFIjJ/YfExfJ8aBl48CKHgu1+A6kWynhttonvWWx6h7924mIzW0Czj2ROuh4LwQyZypex4GuOPW8sgIT21KNZaafgg+KbV7XM1x1tF3XA17B4uGUaDbDw2O+nR1+U5p6qHPzmJ7ggFjSN6Utu+35dS1sS0P9N\"")
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,6 +238,23 @@ class AttachmentTableTest {
|
||||
assertArrayEquals(digest, newDigest)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resetArchiveTransferStateByDigest_singleMatch() {
|
||||
// Given an attachment with some digest
|
||||
val blob = BlobProvider.getInstance().forData(byteArrayOf(1, 2, 3, 4, 5)).createForSingleSessionInMemory()
|
||||
val attachment = createAttachment(1, blob, AttachmentTable.TransformProperties.empty())
|
||||
val attachmentId = SignalDatabase.attachments.insertAttachmentsForMessage(-1L, listOf(attachment), emptyList()).values.first()
|
||||
SignalDatabase.attachments.finalizeAttachmentAfterUpload(attachmentId, AttachmentTableTestUtil.createUploadResult(attachmentId))
|
||||
SignalDatabase.attachments.setArchiveTransferState(attachmentId, AttachmentTable.ArchiveTransferState.FINISHED)
|
||||
|
||||
// Reset the transfer state by digest
|
||||
val digest = SignalDatabase.attachments.getAttachment(attachmentId)!!.remoteDigest!!
|
||||
SignalDatabase.attachments.resetArchiveTransferStateByDigest(digest)
|
||||
|
||||
// Verify it's been reset
|
||||
assertThat(SignalDatabase.attachments.getAttachment(attachmentId)!!.archiveTransferState).isEqualTo(AttachmentTable.ArchiveTransferState.NONE)
|
||||
}
|
||||
|
||||
private fun createAttachmentPointer(key: ByteArray, digest: ByteArray, size: Int): Attachment {
|
||||
return PointerAttachment.forPointer(
|
||||
pointer = Optional.of(
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import org.signal.core.util.Base64
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
import org.thoughtcrime.securesms.attachments.Cdn
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.attachment.AttachmentUploadResult
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
|
||||
import kotlin.random.Random
|
||||
|
||||
object AttachmentTableTestUtil {
|
||||
|
||||
fun createUploadResult(attachmentId: AttachmentId, uploadTimestamp: Long = System.currentTimeMillis()): AttachmentUploadResult {
|
||||
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
|
||||
|
||||
return AttachmentUploadResult(
|
||||
remoteId = SignalServiceAttachmentRemoteId.V4("somewhere-${Random.nextLong()}"),
|
||||
cdnNumber = Cdn.CDN_3.cdnNumber,
|
||||
key = databaseAttachment.remoteKey?.let { Base64.decode(it) } ?: Util.getSecretBytes(64),
|
||||
iv = databaseAttachment.remoteIv ?: Util.getSecretBytes(16),
|
||||
digest = Random.nextBytes(32),
|
||||
incrementalDigest = Random.nextBytes(16),
|
||||
incrementalDigestChunkSize = 5,
|
||||
uploadTimestamp = uploadTimestamp,
|
||||
dataSize = databaseAttachment.size,
|
||||
blurHash = databaseAttachment.blurHash?.hash
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,6 @@ import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.attachment.AttachmentUploadResult
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream
|
||||
import java.io.File
|
||||
@@ -729,7 +727,7 @@ class AttachmentTableTest_deduping {
|
||||
|
||||
fun upload(attachmentId: AttachmentId, uploadTimestamp: Long = System.currentTimeMillis()) {
|
||||
SignalDatabase.attachments.createKeyIvIfNecessary(attachmentId)
|
||||
SignalDatabase.attachments.finalizeAttachmentAfterUpload(attachmentId, createUploadResult(attachmentId, uploadTimestamp))
|
||||
SignalDatabase.attachments.finalizeAttachmentAfterUpload(attachmentId, AttachmentTableTestUtil.createUploadResult(attachmentId, uploadTimestamp))
|
||||
|
||||
val attachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
|
||||
SignalDatabase.attachments.setArchiveCdn(
|
||||
@@ -875,23 +873,6 @@ class AttachmentTableTest_deduping {
|
||||
private fun ByteArray.asMediaStream(): MediaStream {
|
||||
return MediaStream(this.inputStream(), MediaUtil.IMAGE_JPEG, 2, 2)
|
||||
}
|
||||
|
||||
private fun createUploadResult(attachmentId: AttachmentId, uploadTimestamp: Long = System.currentTimeMillis()): AttachmentUploadResult {
|
||||
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
|
||||
|
||||
return AttachmentUploadResult(
|
||||
remoteId = SignalServiceAttachmentRemoteId.V4("somewhere-${Random.nextLong()}"),
|
||||
cdnNumber = Cdn.CDN_3.cdnNumber,
|
||||
key = databaseAttachment.remoteKey?.let { Base64.decode(it) } ?: Util.getSecretBytes(64),
|
||||
iv = databaseAttachment.remoteIv ?: Util.getSecretBytes(16),
|
||||
digest = Random.nextBytes(32),
|
||||
incrementalDigest = Random.nextBytes(16),
|
||||
incrementalDigestChunkSize = 5,
|
||||
uploadTimestamp = uploadTimestamp,
|
||||
dataSize = databaseAttachment.size,
|
||||
blurHash = databaseAttachment.blurHash?.hash
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun test(content: TestContext.() -> Unit) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -77,7 +77,7 @@ class SyncMessageProcessorTest_readSyncs {
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
assertThat(threadRecord.unreadCount).isEqualTo(2)
|
||||
assertThat(threadRecord.unreadCount).isEqualTo(1)
|
||||
|
||||
messageHelper.syncReadMessage(messageHelper.alice to message2Timestamp, messageHelper.alice to editMessage1Timestamp1, messageHelper.alice to editMessage1Timestamp2)
|
||||
|
||||
@@ -98,7 +98,7 @@ class SyncMessageProcessorTest_readSyncs {
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.group.recipientId)!!
|
||||
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
assertThat(threadRecord.unreadCount).isEqualTo(2)
|
||||
assertThat(threadRecord.unreadCount).isEqualTo(1)
|
||||
|
||||
messageHelper.syncReadMessage(messageHelper.bob to message2Timestamp, messageHelper.alice to editMessage1Timestamp1, messageHelper.alice to editMessage1Timestamp2)
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.signal.libsignal.protocol.SignalProtocolAddress
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair
|
||||
import org.signal.libsignal.protocol.groups.state.SenderKeyRecord
|
||||
import org.signal.libsignal.protocol.state.IdentityKeyStore
|
||||
import org.signal.libsignal.protocol.state.IdentityKeyStore.IdentityChange
|
||||
import org.signal.libsignal.protocol.state.KyberPreKeyRecord
|
||||
import org.signal.libsignal.protocol.state.PreKeyBundle
|
||||
import org.signal.libsignal.protocol.state.PreKeyRecord
|
||||
@@ -137,7 +138,7 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
|
||||
override fun getLocalRegistrationId(): Int = registrationId
|
||||
override fun isTrustedIdentity(address: SignalProtocolAddress?, identityKey: IdentityKey?, direction: IdentityKeyStore.Direction?): Boolean = true
|
||||
override fun loadSession(address: SignalProtocolAddress?): SessionRecord = aliceSessionRecord ?: SessionRecord()
|
||||
override fun saveIdentity(address: SignalProtocolAddress?, identityKey: IdentityKey?): Boolean = false
|
||||
override fun saveIdentity(address: SignalProtocolAddress?, identityKey: IdentityKey?): IdentityKeyStore.IdentityChange = IdentityChange.NEW_OR_UNCHANGED
|
||||
override fun storeSession(address: SignalProtocolAddress?, record: SessionRecord?) {
|
||||
aliceSessionRecord = record
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -145,17 +146,6 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false" />
|
||||
|
||||
<activity android:name=".InviteActivity"
|
||||
android:theme="@style/Signal.Light.NoActionBar.Invite"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".MainActivity" />
|
||||
</activity>
|
||||
|
||||
<activity android:name=".DeviceProvisioningActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="true">
|
||||
@@ -888,11 +878,8 @@
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".stickers.StickerManagementActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/TextSecure.LightTheme"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false"/>
|
||||
android:exported="false"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity android:name=".logsubmit.SubmitDebugLogActivity"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
@@ -1069,9 +1056,12 @@
|
||||
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:launchMode="singleTask"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".pin.PinRestoreActivity"
|
||||
@@ -1148,6 +1138,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"
|
||||
@@ -1343,6 +1337,12 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".service.MessageBackupListener" android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".service.AnalyzeDatabaseAlarmListener" android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
|
||||
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;
|
||||
@@ -363,7 +364,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;
|
||||
}
|
||||
|
||||
@@ -383,7 +384,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
|
||||
}
|
||||
|
||||
public void initializeMessageRetrieval() {
|
||||
AppDependencies.startNetwork();
|
||||
SignalExecutors.UNBOUNDED.execute(AppDependencies::startNetwork);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
||||
ContactSelectionActivity activity = this.activity.get();
|
||||
|
||||
if (activity != null && !activity.isFinishing()) {
|
||||
activity.contactsFragment.resetQueryFilter();
|
||||
activity.contactsFragment.onDataRefreshed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,9 +556,11 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
|
||||
public void resetQueryFilter() {
|
||||
setQueryFilter(null);
|
||||
onDataRefreshed();
|
||||
}
|
||||
|
||||
public void onDataRefreshed() {
|
||||
this.resetPositionOnCommit = true;
|
||||
|
||||
swipeRefresh.setRefreshing(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.AnimRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.signal.core.util.concurrent.ListenableFuture.Listener;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView.OnFilterChangedListener;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||
import org.thoughtcrime.securesms.contacts.paged.ChatType;
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarInviteTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class InviteActivity extends PassphraseRequiredActivity implements ContactSelectionListFragment.OnContactSelectedListener {
|
||||
|
||||
private ContactSelectionListFragment contactsFragment;
|
||||
private EditText inviteText;
|
||||
private ViewGroup smsSendFrame;
|
||||
private Button smsSendButton;
|
||||
private Animation slideInAnimation;
|
||||
private Animation slideOutAnimation;
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarInviteTheme();
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
super.onPreCreate();
|
||||
dynamicTheme.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, ContactSelectionDisplayMode.FLAG_SMS);
|
||||
getIntent().putExtra(ContactSelectionListFragment.SELECTION_LIMITS, SelectionLimits.NO_LIMITS);
|
||||
getIntent().putExtra(ContactSelectionListFragment.HIDE_COUNT, true);
|
||||
getIntent().putExtra(ContactSelectionListFragment.REFRESHABLE, false);
|
||||
|
||||
setContentView(R.layout.invite_activity);
|
||||
|
||||
initializeAppBar();
|
||||
initializeResources();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
private void initializeAppBar() {
|
||||
final Toolbar primaryToolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(primaryToolbar);
|
||||
|
||||
assert getSupportActionBar() != null;
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setTitle(R.string.AndroidManifest__invite_friends);
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
slideInAnimation = loadAnimation(R.anim.slide_from_bottom);
|
||||
slideOutAnimation = loadAnimation(R.anim.slide_to_bottom);
|
||||
|
||||
View shareButton = findViewById(R.id.share_button);
|
||||
TextView shareText = findViewById(R.id.share_text);
|
||||
View smsButton = findViewById(R.id.sms_button);
|
||||
Button smsCancelButton = findViewById(R.id.cancel_sms_button);
|
||||
ContactFilterView contactFilter = findViewById(R.id.contact_filter_edit_text);
|
||||
|
||||
inviteText = findViewById(R.id.invite_text);
|
||||
smsSendFrame = findViewById(R.id.sms_send_frame);
|
||||
smsSendButton = findViewById(R.id.send_sms_button);
|
||||
contactsFragment = (ContactSelectionListFragment)getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
|
||||
|
||||
inviteText.setText(getString(R.string.InviteActivity_lets_switch_to_signal, getString(R.string.install_url)));
|
||||
inviteText.addTextChangedListener(new AfterTextChanged(editable -> {
|
||||
boolean isEnabled = editable.length() > 0;
|
||||
smsButton.setEnabled(isEnabled);
|
||||
shareButton.setEnabled(isEnabled);
|
||||
smsButton.animate().alpha(isEnabled ? 1f : 0.5f);
|
||||
shareButton.animate().alpha(isEnabled ? 1f : 0.5f);
|
||||
}));
|
||||
|
||||
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
|
||||
|
||||
smsCancelButton.setOnClickListener(new SmsCancelClickListener());
|
||||
smsSendButton.setOnClickListener(new SmsSendClickListener());
|
||||
contactFilter.setOnFilterChangedListener(new ContactFilterChangedListener());
|
||||
|
||||
smsButton.setVisibility(View.GONE);
|
||||
shareText.setText(R.string.InviteActivity_share);
|
||||
shareButton.setOnClickListener(new ShareClickListener());
|
||||
}
|
||||
|
||||
private Animation loadAnimation(@AnimRes int animResId) {
|
||||
final Animation animation = AnimationUtils.loadAnimation(this, animResId);
|
||||
animation.setInterpolator(new FastOutSlowInInterpolator());
|
||||
return animation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBeforeContactSelected(boolean isFromUnknownSearchKey, @NonNull Optional<RecipientId> recipientId, String number, @NonNull Optional<ChatType> chatType, @NonNull Consumer<Boolean> callback) {
|
||||
updateSmsButtonText(contactsFragment.getSelectedContacts().size() + 1);
|
||||
callback.accept(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactDeselected(@NonNull Optional<RecipientId> recipientId, String number, @NonNull Optional<ChatType> chatType) {
|
||||
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectionChanged() {
|
||||
}
|
||||
|
||||
private void sendSmsInvites() {
|
||||
new SendSmsInvitesAsyncTask(this, inviteText.getText().toString())
|
||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
|
||||
contactsFragment.getSelectedContacts()
|
||||
.toArray(new SelectedContact[0]));
|
||||
}
|
||||
|
||||
private void updateSmsButtonText(int count) {
|
||||
smsSendButton.setText(getResources().getString(R.string.InviteActivity_send_sms, count));
|
||||
smsSendButton.setEnabled(count > 0);
|
||||
}
|
||||
|
||||
@Override public void onBackPressed() {
|
||||
if (smsSendFrame.getVisibility() == View.VISIBLE) {
|
||||
cancelSmsSelection();
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
@Override public boolean onSupportNavigateUp() {
|
||||
if (smsSendFrame.getVisibility() == View.VISIBLE) {
|
||||
cancelSmsSelection();
|
||||
return false;
|
||||
} else {
|
||||
return super.onSupportNavigateUp();
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelSmsSelection() {
|
||||
contactsFragment.reset();
|
||||
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
|
||||
ViewUtil.animateOut(smsSendFrame, slideOutAnimation, View.GONE);
|
||||
}
|
||||
|
||||
private class ShareClickListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent sendIntent = new Intent();
|
||||
sendIntent.setAction(Intent.ACTION_SEND);
|
||||
sendIntent.putExtra(Intent.EXTRA_TEXT, inviteText.getText().toString());
|
||||
sendIntent.setType("text/plain");
|
||||
if (sendIntent.resolveActivity(getPackageManager()) != null) {
|
||||
startActivity(Intent.createChooser(sendIntent, getString(R.string.InviteActivity_invite_to_signal)));
|
||||
} else {
|
||||
Toast.makeText(InviteActivity.this, R.string.InviteActivity_no_app_to_share_to, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SmsCancelClickListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
cancelSmsSelection();
|
||||
}
|
||||
}
|
||||
|
||||
private class SmsSendClickListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
new MaterialAlertDialogBuilder(InviteActivity.this)
|
||||
.setTitle(getResources().getQuantityString(R.plurals.InviteActivity_send_sms_invites,
|
||||
contactsFragment.getSelectedContacts().size(),
|
||||
contactsFragment.getSelectedContacts().size()))
|
||||
.setMessage(inviteText.getText().toString())
|
||||
.setPositiveButton(R.string.yes, (dialog, which) -> sendSmsInvites())
|
||||
.setNegativeButton(R.string.no, (dialog, which) -> dialog.dismiss())
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
private class ContactFilterChangedListener implements OnFilterChangedListener {
|
||||
@Override
|
||||
public void onFilterChanged(String filter) {
|
||||
contactsFragment.setQueryFilter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private class SendSmsInvitesAsyncTask extends ProgressDialogAsyncTask<SelectedContact,Void,Void> {
|
||||
private final String message;
|
||||
|
||||
SendSmsInvitesAsyncTask(Context context, String message) {
|
||||
super(context, R.string.InviteActivity_sending, R.string.InviteActivity_sending);
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(SelectedContact... contacts) {
|
||||
final Context context = getContext();
|
||||
if (context == null) return null;
|
||||
|
||||
for (SelectedContact contact : contacts) {
|
||||
RecipientId recipientId = contact.getOrCreateRecipientId();
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
|
||||
MessageSender.send(context, OutgoingMessage.sms(recipient, message), -1L, MessageSender.SendType.SMS, null, null);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
super.onPostExecute(aVoid);
|
||||
final Context context = getContext();
|
||||
if (context == null) return;
|
||||
|
||||
ViewUtil.animateOut(smsSendFrame, slideOutAnimation, View.GONE).addListener(new Listener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
contactsFragment.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(ExecutionException e) {}
|
||||
});
|
||||
Toast.makeText(context, R.string.InviteActivity_invitations_sent, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
123
app/src/main/java/org/thoughtcrime/securesms/InviteFragment.kt
Normal file
123
app/src/main/java/org/thoughtcrime/securesms/InviteFragment.kt
Normal file
@@ -0,0 +1,123 @@
|
||||
package org.thoughtcrime.securesms
|
||||
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
|
||||
/**
|
||||
* Fragment when inviting someone to use Signal
|
||||
*/
|
||||
class InviteFragment : ComposeFragment() {
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
Scaffolds.Settings(
|
||||
title = stringResource(id = R.string.AndroidManifest__invite_friends),
|
||||
onNavigationClick = { requireActivity().onNavigateUp() },
|
||||
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_start_24),
|
||||
navigationContentDescription = stringResource(id = R.string.Material3SearchToolbar__close)
|
||||
) { contentPadding: PaddingValues ->
|
||||
InviteScreen(
|
||||
onShare = { inviteText -> onShare(inviteText) },
|
||||
modifier = Modifier.padding(contentPadding)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onShare(inviteText: String) {
|
||||
val sendIntent = Intent()
|
||||
.setAction(Intent.ACTION_SEND)
|
||||
.putExtra(Intent.EXTRA_TEXT, inviteText)
|
||||
.setType("text/plain")
|
||||
if (sendIntent.resolveActivity(requireContext().packageManager) != null) {
|
||||
startActivity(Intent.createChooser(sendIntent, getString(R.string.InviteActivity_invite_to_signal)))
|
||||
} else {
|
||||
Toast.makeText(requireContext(), R.string.InviteActivity_no_app_to_share_to, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InviteScreen(
|
||||
onShare: (String) -> Unit = {},
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val default = stringResource(R.string.InviteActivity_lets_switch_to_signal, stringResource(R.string.install_url))
|
||||
var inviteText by remember { mutableStateOf(TextFieldValue(default, TextRange(default.length))) }
|
||||
|
||||
Column(
|
||||
modifier = modifier.padding(16.dp).fillMaxHeight()
|
||||
) {
|
||||
TextField(
|
||||
value = inviteText,
|
||||
onValueChange = { inviteText = it },
|
||||
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(onClick = { onShare(inviteText.text) })
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.symbol_share_android_24),
|
||||
contentDescription = stringResource(R.string.InviteActivity_share)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.InviteActivity_share),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun InviteScreenPreview() {
|
||||
Previews.Preview {
|
||||
InviteScreen()
|
||||
}
|
||||
}
|
||||
@@ -5,78 +5,148 @@
|
||||
|
||||
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.ViewGroup
|
||||
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.calculatePaneScaffoldDirective
|
||||
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
|
||||
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.runtime.rememberCoroutineScope
|
||||
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.compose.ui.unit.Dp
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.compose.AndroidFragment
|
||||
import androidx.fragment.compose.rememberFragmentState
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.getSerializableCompat
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.StripeApi
|
||||
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar.show
|
||||
import org.thoughtcrime.securesms.calls.log.CallLogFilter
|
||||
import org.thoughtcrime.securesms.components.ConnectivityWarningBottomSheet
|
||||
import org.thoughtcrime.securesms.calls.log.CallLogFragment
|
||||
import org.thoughtcrime.securesms.calls.new.NewCallActivity
|
||||
import org.thoughtcrime.securesms.components.DebugLogsPromptDialogFragment
|
||||
import org.thoughtcrime.securesms.components.DeviceSpecificNotificationBottomSheet
|
||||
import org.thoughtcrime.securesms.components.PromptBatterySaverDialogFragment
|
||||
import org.thoughtcrime.securesms.components.compose.ConnectivityWarningBottomSheet
|
||||
import org.thoughtcrime.securesms.components.compose.DeviceSpecificNotificationBottomSheet
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity.Companion.manageSubscriptions
|
||||
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.ConversationListArchiveFragment
|
||||
import org.thoughtcrime.securesms.conversationlist.ConversationListFragment
|
||||
import org.thoughtcrime.securesms.conversationlist.RelinkDevicesReminderBottomSheetFragment
|
||||
import org.thoughtcrime.securesms.conversationlist.RestoreCompleteBottomSheetDialog
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
|
||||
import org.thoughtcrime.securesms.conversationlist.model.UnreadPaymentsLiveData
|
||||
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.main.MainActivityListHostFragment
|
||||
import org.thoughtcrime.securesms.main.MainNavigationDestination
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity
|
||||
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.MainToolbarState
|
||||
import org.thoughtcrime.securesms.main.MainToolbarViewModel
|
||||
import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder
|
||||
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.notifications.profiles.NotificationProfile
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles
|
||||
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.landing.StoriesLandingFragment
|
||||
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.BottomSheetUtil
|
||||
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.Material3OnScrollHelper
|
||||
import org.thoughtcrime.securesms.util.SplashScreenUtil
|
||||
import org.thoughtcrime.securesms.util.WindowUtil
|
||||
import org.thoughtcrime.securesms.util.TopToastPopup
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
import org.thoughtcrime.securesms.window.AppScaffold
|
||||
import org.thoughtcrime.securesms.window.WindowSizeClass
|
||||
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState
|
||||
|
||||
class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, MainNavigator.NavigatorProvider {
|
||||
class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, MainNavigator.NavigatorProvider, Material3OnScrollHelperBinder, ConversationListFragment.Callback, CallLogFragment.Callback {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(MainActivity::class)
|
||||
|
||||
private const val KEY_STARTING_TAB = "STARTING_TAB"
|
||||
const val RESULT_CONFIG_CHANGED = Activity.RESULT_FIRST_USER + 901
|
||||
|
||||
@@ -87,23 +157,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,60 +188,226 @@ 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 var previousTopToastPopup: TopToastPopup? = null
|
||||
|
||||
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)
|
||||
UnreadPaymentsLiveData().observe(this) { unread ->
|
||||
toolbarViewModel.setHasUnreadPayments(unread.isPresent)
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
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()
|
||||
|
||||
SignalTheme(isDarkMode = DynamicTheme.isDarkTheme(LocalContext.current)) {
|
||||
MainToolbar(
|
||||
state = state,
|
||||
callback = toolbarCallback
|
||||
)
|
||||
launch {
|
||||
mainNavigationViewModel.getNotificationProfiles().collectLatest { profiles ->
|
||||
withContext(Dispatchers.Main) {
|
||||
updateNotificationProfileStatus(profiles)
|
||||
}
|
||||
|
||||
AndroidFragment(
|
||||
clazz = MainActivityListHostFragment::class.java,
|
||||
fragmentState = listHostState,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shareDataTimestampViewModel.setTimestampFromActivityCreation(savedInstanceState, intent)
|
||||
|
||||
setContent {
|
||||
val snackbar by mainNavigationViewModel.snackbar.collectAsStateWithLifecycle()
|
||||
val mainToolbarState by toolbarViewModel.state.collectAsStateWithLifecycle()
|
||||
val megaphone by mainNavigationViewModel.megaphone.collectAsStateWithLifecycle()
|
||||
val mainNavigationState by mainNavigationViewModel.mainNavigationState.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(mainNavigationState.selectedDestination) {
|
||||
when (mainNavigationState.selectedDestination) {
|
||||
MainNavigationListLocation.CHATS -> toolbarViewModel.presentToolbarForConversationListFragment()
|
||||
MainNavigationListLocation.ARCHIVE -> toolbarViewModel.presentToolbarForConversationListArchiveFragment()
|
||||
MainNavigationListLocation.CALLS -> toolbarViewModel.presentToolbarForCallLogFragment()
|
||||
MainNavigationListLocation.STORIES -> toolbarViewModel.presentToolbarForStoriesLandingFragment()
|
||||
}
|
||||
}
|
||||
|
||||
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 wrappedNavigator = rememberNavigator(windowSizeClass, contentLayoutData, maxWidth)
|
||||
|
||||
AppScaffold(
|
||||
navigator = wrappedNavigator,
|
||||
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)
|
||||
) {
|
||||
when (val destination = mainNavigationState.selectedDestination) {
|
||||
MainNavigationListLocation.CHATS -> {
|
||||
val state = key(destination) { rememberFragmentState() }
|
||||
AndroidFragment(
|
||||
clazz = ConversationListFragment::class.java,
|
||||
fragmentState = state,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
MainNavigationListLocation.ARCHIVE -> {
|
||||
val state = key(destination) { rememberFragmentState() }
|
||||
AndroidFragment(
|
||||
clazz = ConversationListArchiveFragment::class.java,
|
||||
fragmentState = state,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
MainNavigationListLocation.CALLS -> {
|
||||
val state = key(destination) { rememberFragmentState() }
|
||||
AndroidFragment(
|
||||
clazz = CallLogFragment::class.java,
|
||||
fragmentState = state,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
MainNavigationListLocation.STORIES -> {
|
||||
val state = key(destination) { rememberFragmentState() }
|
||||
AndroidFragment(
|
||||
clazz = StoriesLandingFragment::class.java,
|
||||
fragmentState = state,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MainBottomChrome(
|
||||
state = mainBottomChromeState,
|
||||
callback = mainBottomChromeCallback,
|
||||
megaphoneActionController = megaphoneActionController,
|
||||
modifier = Modifier.align(Alignment.BottomCenter)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
detailContent = {
|
||||
when (val destination = wrappedNavigator.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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val content: View = findViewById(android.R.id.content)
|
||||
content.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
|
||||
override fun onPreDraw(): Boolean {
|
||||
@@ -191,11 +427,72 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
|
||||
handleDeepLinkIntent(intent)
|
||||
CachedInflater.from(this).clear()
|
||||
updateNavigationBarColor()
|
||||
|
||||
lifecycleDisposable += vitalsViewModel.vitalsState.subscribe(this::presentVitalsState)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and wraps a scaffold navigator such that we can use it to operate with both
|
||||
* our split pane and legacy activities.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
@Composable
|
||||
private fun rememberNavigator(
|
||||
windowSizeClass: WindowSizeClass,
|
||||
contentLayoutData: MainContentLayoutData,
|
||||
maxWidth: Dp
|
||||
): ThreePaneScaffoldNavigator<Any> {
|
||||
val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator<Any>(
|
||||
scaffoldDirective = calculatePaneScaffoldDirective(
|
||||
currentWindowAdaptiveInfo()
|
||||
).copy(
|
||||
maxHorizontalPartitions = if (windowSizeClass.isSplitPane()) 2 else 1,
|
||||
horizontalPartitionSpacerSize = contentLayoutData.partitionWidth,
|
||||
defaultPanePreferredWidth = contentLayoutData.rememberDefaultPanePreferredWidth(maxWidth)
|
||||
)
|
||||
)
|
||||
|
||||
val coroutine = rememberCoroutineScope()
|
||||
|
||||
return remember(scaffoldNavigator, coroutine) {
|
||||
mainNavigationViewModel.wrapNavigator(coroutine, scaffoldNavigator) { detailLocation ->
|
||||
when (detailLocation) {
|
||||
is MainNavigationDetailLocation.Conversation -> {
|
||||
startActivity(detailLocation.intent)
|
||||
}
|
||||
MainNavigationDetailLocation.Empty -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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 +502,15 @@ 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.ARCHIVE -> mainNavigationViewModel.onArchiveSelected()
|
||||
MainNavigationListLocation.CALLS -> mainNavigationViewModel.onCallsSelected()
|
||||
MainNavigationListLocation.STORIES -> {
|
||||
if (Stories.isFeatureEnabled()) {
|
||||
conversationListTabsViewModel.onStoriesSelected()
|
||||
mainNavigationViewModel.onStoriesSelected()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +527,8 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
super.onResume()
|
||||
dynamicTheme.onResume(this)
|
||||
|
||||
toolbarViewModel.refresh()
|
||||
|
||||
if (SignalStore.misc.shouldShowLinkedDevicesReminder) {
|
||||
SignalStore.misc.shouldShowLinkedDevicesReminder = false
|
||||
RelinkDevicesReminderBottomSheetFragment.show(supportFragmentManager)
|
||||
@@ -250,9 +550,8 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
.show()
|
||||
}
|
||||
|
||||
updateNavigationBarColor()
|
||||
|
||||
vitalsViewModel.checkSlowNotificationHeuristics()
|
||||
mainNavigationViewModel.refreshNavigationBarState()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
@@ -260,10 +559,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 +568,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() {
|
||||
@@ -281,7 +592,58 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
return navigator
|
||||
}
|
||||
|
||||
override fun bindScrollHelper(recyclerView: RecyclerView, lifecycleOwner: LifecycleOwner) {
|
||||
Material3OnScrollHelper(
|
||||
activity = this,
|
||||
views = listOf(),
|
||||
viewStubs = listOf(),
|
||||
onSetToolbarColor = {
|
||||
toolbarViewModel.setToolbarColor(it)
|
||||
},
|
||||
setStatusBarColor = {},
|
||||
lifecycleOwner = lifecycleOwner
|
||||
).attach(recyclerView)
|
||||
}
|
||||
|
||||
override fun bindScrollHelper(recyclerView: RecyclerView, lifecycleOwner: LifecycleOwner, chatFolders: RecyclerView, setChatFolder: (Int) -> Unit) {
|
||||
Material3OnScrollHelper(
|
||||
activity = this,
|
||||
views = listOf(chatFolders),
|
||||
viewStubs = listOf(),
|
||||
setStatusBarColor = {},
|
||||
onSetToolbarColor = {
|
||||
toolbarViewModel.setToolbarColor(it)
|
||||
},
|
||||
lifecycleOwner = lifecycleOwner,
|
||||
setChatFolderColor = setChatFolder
|
||||
).attach(recyclerView)
|
||||
}
|
||||
|
||||
override fun updateProxyStatus(state: WebSocketConnectionState) {
|
||||
if (SignalStore.proxy.isProxyEnabled) {
|
||||
val proxyState: MainToolbarState.ProxyState = when (state) {
|
||||
WebSocketConnectionState.CONNECTING, WebSocketConnectionState.DISCONNECTING, WebSocketConnectionState.DISCONNECTED -> MainToolbarState.ProxyState.CONNECTING
|
||||
WebSocketConnectionState.CONNECTED -> MainToolbarState.ProxyState.CONNECTED
|
||||
WebSocketConnectionState.AUTHENTICATION_FAILED, WebSocketConnectionState.FAILED, WebSocketConnectionState.REMOTE_DEPRECATED -> MainToolbarState.ProxyState.FAILED
|
||||
else -> MainToolbarState.ProxyState.NONE
|
||||
}
|
||||
|
||||
toolbarViewModel.setProxyState(proxyState = proxyState)
|
||||
} else {
|
||||
toolbarViewModel.setProxyState(proxyState = MainToolbarState.ProxyState.NONE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMultiSelectStarted() {
|
||||
toolbarViewModel.presentToolbarForMultiselect()
|
||||
}
|
||||
|
||||
override fun onMultiSelectFinished() {
|
||||
toolbarViewModel.presentToolbarForCurrentDestination()
|
||||
}
|
||||
|
||||
private fun handleDeepLinkIntent(intent: Intent) {
|
||||
handleConversationIntent(intent)
|
||||
handleGroupLinkInIntent(intent)
|
||||
handleProxyInIntent(intent)
|
||||
handleSignalMeIntent(intent)
|
||||
@@ -289,10 +651,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 +664,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())
|
||||
@@ -340,6 +704,44 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateNotificationProfileStatus(notificationProfiles: List<NotificationProfile>) {
|
||||
val activeProfile = NotificationProfiles.getActiveProfile(notificationProfiles)
|
||||
if (activeProfile != null) {
|
||||
if (activeProfile.id != SignalStore.notificationProfile.lastProfilePopup) {
|
||||
val view = findViewById<ViewGroup>(android.R.id.content)
|
||||
|
||||
view.postDelayed({
|
||||
try {
|
||||
var fragmentView = view ?: return@postDelayed
|
||||
|
||||
SignalStore.notificationProfile.lastProfilePopup = activeProfile.id
|
||||
SignalStore.notificationProfile.lastProfilePopupTime = System.currentTimeMillis()
|
||||
|
||||
if (previousTopToastPopup?.isShowing == true) {
|
||||
previousTopToastPopup?.dismiss()
|
||||
}
|
||||
|
||||
val fragment = supportFragmentManager.findFragmentByTag(BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
if (fragment != null && fragment.isAdded && fragment.view != null) {
|
||||
fragmentView = fragment.requireView() as ViewGroup
|
||||
}
|
||||
|
||||
previousTopToastPopup = TopToastPopup.show(fragmentView, R.drawable.ic_moon_16, getString(R.string.ConversationListFragment__s_on, activeProfile.name))
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Unable to show toast popup", e)
|
||||
}
|
||||
}, 500L)
|
||||
}
|
||||
toolbarViewModel.setNotificationProfileEnabled(true)
|
||||
} else {
|
||||
toolbarViewModel.setNotificationProfileEnabled(false)
|
||||
}
|
||||
|
||||
if (!SignalStore.notificationProfile.hasSeenTooltip && Util.hasItems(notificationProfiles)) {
|
||||
toolbarViewModel.setShowNotificationProfilesTooltip(true)
|
||||
}
|
||||
}
|
||||
|
||||
inner class ToolbarCallback : MainToolbarCallback {
|
||||
|
||||
override fun onNewGroupClick() {
|
||||
@@ -357,8 +759,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
}
|
||||
|
||||
override fun onInviteFriendsClick() {
|
||||
val intent = Intent(this@MainActivity, InviteActivity::class.java)
|
||||
startActivity(intent)
|
||||
openSettings.launch(AppSettingsActivity.invite(this@MainActivity))
|
||||
}
|
||||
|
||||
override fun onFilterUnreadChatsClick() {
|
||||
@@ -418,4 +819,97 @@ 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()
|
||||
MainNavigationListLocation.ARCHIVE -> mainNavigationViewModel.onArchiveSelected()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -86,11 +59,6 @@ public class MainNavigator {
|
||||
activity.startActivity(CreateGroupActivity.newIntent(activity));
|
||||
}
|
||||
|
||||
public void goToInvite() {
|
||||
Intent intent = new Intent(activity, InviteActivity.class);
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
|
||||
private @NonNull FragmentManager getFragmentManager() {
|
||||
return activity.getSupportFragmentManager();
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar;
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem;
|
||||
import org.thoughtcrime.securesms.components.menu.SignalContextMenu;
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
|
||||
import org.thoughtcrime.securesms.contacts.management.ContactsManagementRepository;
|
||||
import org.thoughtcrime.securesms.contacts.management.ContactsManagementViewModel;
|
||||
import org.thoughtcrime.securesms.contacts.paged.ChatType;
|
||||
@@ -215,7 +216,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
}
|
||||
|
||||
private void handleInvite() {
|
||||
startActivity(new Intent(this, InviteActivity.class));
|
||||
startActivity(AppSettingsActivity.invite(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -361,6 +362,8 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
handleManualRefresh();
|
||||
displaySnackbar(R.string.NewConversationActivity__s_has_been_blocked, recipient.getDisplayName(this));
|
||||
contactsFragment.reset();
|
||||
}, (throwable) -> {
|
||||
displaySnackbar(R.string.NewConversationActivity__block_failed);
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
@@ -195,7 +195,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
|
||||
private boolean userMustCreateSignalPin() {
|
||||
return !SignalStore.registration().isRegistrationComplete() &&
|
||||
!SignalStore.svr().hasOptedInWithAccess() &&
|
||||
!SignalStore.svr().hasPin() &&
|
||||
!SignalStore.svr().lastPinCreateFailed() &&
|
||||
!SignalStore.svr().hasOptedOut();
|
||||
}
|
||||
|
||||
@@ -8,4 +8,5 @@ interface AudioRecordingHandler {
|
||||
fun onRecordSaved()
|
||||
fun onRecordMoved(offsetX: Float, absoluteX: Float)
|
||||
fun onRecordPermissionRequired()
|
||||
fun onRecorderAlreadyInUse()
|
||||
}
|
||||
|
||||
@@ -9,18 +9,30 @@ 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.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
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 kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.rx3.asFlow
|
||||
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(
|
||||
recipient: Recipient,
|
||||
modifier: Modifier = Modifier,
|
||||
useProfile: Boolean = true
|
||||
useProfile: Boolean = true,
|
||||
contentDescription: String? = null
|
||||
) {
|
||||
if (LocalInspectionMode.current) {
|
||||
Spacer(
|
||||
@@ -28,15 +40,38 @@ fun AvatarImage(
|
||||
.background(color = Color.Red, shape = CircleShape)
|
||||
)
|
||||
} else {
|
||||
val context = LocalContext.current
|
||||
var state: AvatarImageState by remember {
|
||||
mutableStateOf(AvatarImageState(null, recipient, ProfileAvatarFileDetails.NO_DETAILS))
|
||||
}
|
||||
|
||||
LaunchedEffect(recipient.id) {
|
||||
Recipient.observable(recipient.id).asFlow()
|
||||
.collectLatest {
|
||||
state = AvatarImageState(NameUtil.getAbbreviation(it.getDisplayName(context)), it, AvatarHelper.getAvatarFileDetails(context, it.id))
|
||||
}
|
||||
}
|
||||
|
||||
AndroidView(
|
||||
factory = ::AvatarImageView,
|
||||
factory = {
|
||||
AvatarImageView(context).apply {
|
||||
initialize(context, null)
|
||||
this.contentDescription = contentDescription
|
||||
}
|
||||
},
|
||||
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"
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupWriter
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupReader
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupWriter
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider
|
||||
@@ -68,8 +69,10 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobs.AvatarGroupsV2DownloadJob
|
||||
import org.thoughtcrime.securesms.jobs.CheckRestoreMediaLeftJob
|
||||
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
|
||||
import org.thoughtcrime.securesms.jobs.RestoreAttachmentJob
|
||||
import org.thoughtcrime.securesms.jobs.RestoreOptimizedMediaJob
|
||||
import org.thoughtcrime.securesms.keyvalue.BackupValues.ArchiveServiceCredentials
|
||||
import org.thoughtcrime.securesms.keyvalue.KeyValueStore
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
@@ -195,6 +198,12 @@ object BackupRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun resumeMediaRestore() {
|
||||
SignalStore.backup.userManuallySkippedMediaRestore = false
|
||||
RestoreOptimizedMediaJob.enqueue()
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels any relevant jobs for media restore
|
||||
*/
|
||||
@@ -205,6 +214,10 @@ object BackupRepository {
|
||||
AppDependencies.jobManager.cancelAllInQueue(RestoreAttachmentJob.constructQueueString(RestoreAttachmentJob.RestoreOperation.RESTORE_OFFLOADED))
|
||||
AppDependencies.jobManager.cancelAllInQueue(RestoreAttachmentJob.constructQueueString(RestoreAttachmentJob.RestoreOperation.INITIAL_RESTORE))
|
||||
AppDependencies.jobManager.cancelAllInQueue(RestoreAttachmentJob.constructQueueString(RestoreAttachmentJob.RestoreOperation.MANUAL))
|
||||
|
||||
AppDependencies.jobManager.add(CheckRestoreMediaLeftJob(RestoreAttachmentJob.constructQueueString(RestoreAttachmentJob.RestoreOperation.RESTORE_OFFLOADED)))
|
||||
AppDependencies.jobManager.add(CheckRestoreMediaLeftJob(RestoreAttachmentJob.constructQueueString(RestoreAttachmentJob.RestoreOperation.INITIAL_RESTORE)))
|
||||
AppDependencies.jobManager.add(CheckRestoreMediaLeftJob(RestoreAttachmentJob.constructQueueString(RestoreAttachmentJob.RestoreOperation.MANUAL)))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -350,12 +363,18 @@ object BackupRepository {
|
||||
fun turnOffAndDisableBackups(): Boolean {
|
||||
return try {
|
||||
Log.d(TAG, "Attempting to disable backups.")
|
||||
if (SignalStore.backup.backupTier == MessageBackupTier.PAID) {
|
||||
|
||||
val backupsSubscriber = InAppPaymentsRepository.getSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP)
|
||||
if (SignalStore.backup.backupTier == MessageBackupTier.PAID && backupsSubscriber != null) {
|
||||
Log.d(TAG, "User is currently on a paid tier. Canceling.")
|
||||
RecurringInAppPaymentRepository.cancelActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP)
|
||||
Log.d(TAG, "Successfully canceled paid tier.")
|
||||
}
|
||||
|
||||
if (backupsSubscriber == null) {
|
||||
Log.w(TAG, "No backup subscriber in the database. Proceeding with disabling backups anyway.")
|
||||
}
|
||||
|
||||
Log.d(TAG, "Disabling backups.")
|
||||
SignalStore.backup.disableBackups()
|
||||
SignalDatabase.attachments.clearAllArchiveData()
|
||||
|
||||
@@ -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,18 @@ 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.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||
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 +71,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 +87,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 +108,7 @@ class BackupAlertBottomSheet : UpgradeToPaidTierBottomSheet() {
|
||||
}
|
||||
|
||||
BackupAlert.FailedToRenew -> launchManageBackupsSubscription()
|
||||
is BackupAlert.MediaBackupsAreOff -> {
|
||||
onSubscribeClick()
|
||||
}
|
||||
is BackupAlert.MediaBackupsAreOff -> error("Use MediaBackupsAreOffBottomSheet instead.")
|
||||
|
||||
BackupAlert.MediaWillBeDeletedToday -> {
|
||||
performFullMediaDownload()
|
||||
@@ -152,7 +129,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()
|
||||
}
|
||||
@@ -206,17 +183,13 @@ class BackupAlertBottomSheet : UpgradeToPaidTierBottomSheet() {
|
||||
}
|
||||
|
||||
private fun performFullMediaDownload() {
|
||||
// TODO [backups] -- We need to force this to download everything
|
||||
AppDependencies.jobManager.add(BackupRestoreMediaJob())
|
||||
BackupRepository.resumeMediaRestore()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BackupAlertSheetContent(
|
||||
fun BackupAlertSheetContent(
|
||||
backupAlert: BackupAlert,
|
||||
pricePerMonth: String = "",
|
||||
isSubscribeEnabled: Boolean = true,
|
||||
mediaTtl: Duration,
|
||||
onPrimaryActionClick: () -> Unit = {},
|
||||
onSecondaryActionClick: () -> Unit = {}
|
||||
) {
|
||||
@@ -231,7 +204,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 +250,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 +353,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(
|
||||
@@ -463,7 +413,7 @@ private fun titleString(backupAlert: BackupAlert): String {
|
||||
return when (backupAlert) {
|
||||
is BackupAlert.CouldNotCompleteBackup -> stringResource(R.string.BackupAlertBottomSheet__couldnt_complete_backup)
|
||||
BackupAlert.FailedToRenew -> stringResource(R.string.BackupAlertBottomSheet__your_backups_subscription_failed_to_renew)
|
||||
is BackupAlert.MediaBackupsAreOff -> stringResource(R.string.BackupAlertBottomSheet__your_backups_subscription_expired)
|
||||
is BackupAlert.MediaBackupsAreOff -> error("Use MediaBackupsAreOffBottomSheet instead.")
|
||||
BackupAlert.MediaWillBeDeletedToday -> stringResource(R.string.BackupAlertBottomSheet__your_media_will_be_deleted_today)
|
||||
is BackupAlert.DiskFull -> stringResource(R.string.BackupAlertBottomSheet__free_up_s_on_this_device, backupAlert.requiredSpace)
|
||||
BackupAlert.BackupFailed -> stringResource(R.string.BackupAlertBottomSheet__backup_failed)
|
||||
@@ -473,13 +423,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 +442,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 +456,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 +466,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 +476,7 @@ private fun BackupAlertSheetContentPreviewMedia() {
|
||||
private fun BackupAlertSheetContentPreviewDelete() {
|
||||
Previews.BottomSheetPreview {
|
||||
BackupAlertSheetContent(
|
||||
backupAlert = BackupAlert.MediaWillBeDeletedToday,
|
||||
mediaTtl = 60.days
|
||||
backupAlert = BackupAlert.MediaWillBeDeletedToday
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -552,8 +486,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 +496,7 @@ private fun BackupAlertSheetContentPreviewDiskFull() {
|
||||
private fun BackupAlertSheetContentPreviewBackupFailed() {
|
||||
Previews.BottomSheetPreview {
|
||||
BackupAlertSheetContent(
|
||||
backupAlert = BackupAlert.BackupFailed,
|
||||
mediaTtl = 60.days
|
||||
backupAlert = BackupAlert.BackupFailed
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -574,8 +506,7 @@ private fun BackupAlertSheetContentPreviewBackupFailed() {
|
||||
private fun BackupAlertSheetContentPreviewCouldNotRedeemBackup() {
|
||||
Previews.BottomSheetPreview {
|
||||
BackupAlertSheetContent(
|
||||
backupAlert = BackupAlert.CouldNotRedeemBackup,
|
||||
mediaTtl = 60.days
|
||||
backupAlert = BackupAlert.CouldNotRedeemBackup
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* 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 = stringResource(org.thoughtcrime.securesms.R.string.BackupAlertBottomSheet__your_backups_subscription_expired),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.padding(top = 16.dp, bottom = 12.dp)
|
||||
)
|
||||
|
||||
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 = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -283,8 +283,18 @@ sealed interface BackupStatusData {
|
||||
val restoreStatus: RestoreStatus = RestoreStatus.NORMAL
|
||||
) : BackupStatusData {
|
||||
override val iconRes: Int = if (restoreStatus == RestoreStatus.FINISHED) R.drawable.symbol_check_circle_24 else R.drawable.symbol_backup_light
|
||||
override val iconColors: BackupsIconColors = if (restoreStatus == RestoreStatus.FINISHED) BackupsIconColors.Success else BackupsIconColors.Normal
|
||||
override val iconColors: BackupsIconColors = when (restoreStatus) {
|
||||
RestoreStatus.FINISHED -> BackupsIconColors.Success
|
||||
RestoreStatus.NORMAL -> BackupsIconColors.Normal
|
||||
RestoreStatus.LOW_BATTERY,
|
||||
RestoreStatus.WAITING_FOR_INTERNET,
|
||||
RestoreStatus.WAITING_FOR_WIFI -> BackupsIconColors.Warning
|
||||
}
|
||||
override val showDismissAction: Boolean = restoreStatus == RestoreStatus.FINISHED
|
||||
override val actionRes: Int = when (restoreStatus) {
|
||||
RestoreStatus.WAITING_FOR_WIFI -> R.string.BackupStatus__resume
|
||||
else -> NONE
|
||||
}
|
||||
|
||||
override val title: String
|
||||
@Composable get() = stringResource(
|
||||
@@ -311,7 +321,7 @@ sealed interface BackupStatusData {
|
||||
RestoreStatus.FINISHED -> bytesTotal.toUnitString()
|
||||
}
|
||||
|
||||
override val progress: Float = if (bytesTotal.bytes > 0 && restoreStatus != RestoreStatus.FINISHED) {
|
||||
override val progress: Float = if (bytesTotal.bytes > 0 && restoreStatus == RestoreStatus.NORMAL) {
|
||||
min(1f, max(0f, bytesDownloaded.bytes.toFloat() / bytesTotal.bytes.toFloat()))
|
||||
} else {
|
||||
NONE.toFloat()
|
||||
|
||||
@@ -184,9 +184,10 @@ private fun getRestoringMediaString(backupStatusData: BackupStatusData.Restoring
|
||||
|
||||
@Composable
|
||||
private fun progressColor(backupStatusData: BackupStatusData): Color {
|
||||
return when (backupStatusData) {
|
||||
is BackupStatusData.RestoringMedia -> MaterialTheme.colorScheme.primary
|
||||
else -> backupStatusData.iconColors.foreground
|
||||
return if (backupStatusData is BackupStatusData.RestoringMedia && backupStatusData.restoreStatus == BackupStatusData.RestoreStatus.NORMAL) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
backupStatusData.iconColors.foreground
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Scaffolds
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.compose.BetaHeader
|
||||
import org.thoughtcrime.securesms.components.compose.TextWithBetaLabel
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
/**
|
||||
@@ -62,6 +64,10 @@ fun MessageBackupsEducationScreen(
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
) {
|
||||
item {
|
||||
BetaHeader()
|
||||
}
|
||||
|
||||
item {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.image_signal_backups),
|
||||
@@ -73,9 +79,9 @@ fun MessageBackupsEducationScreen(
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
TextWithBetaLabel(
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__signal_backups),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
textStyle = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(top = 15.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
||||
|
||||
@VisibleForTesting
|
||||
const val TIER = "tier"
|
||||
const val CLIPBOARD_TIMEOUT_SECONDS = 60
|
||||
|
||||
fun create(messageBackupTier: MessageBackupTier?): MessageBackupsFlowFragment {
|
||||
return MessageBackupsFlowFragment().apply {
|
||||
@@ -115,7 +116,7 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
||||
onNavigationClick = viewModel::goToPreviousStage,
|
||||
onNextClick = viewModel::goToNextStage,
|
||||
onCopyToClipboardClick = {
|
||||
Util.copyToClipboard(context, it)
|
||||
Util.copyToClipboard(context, it, CLIPBOARD_TIMEOUT_SECONDS)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -130,7 +130,7 @@ fun ChatStyle.toLocal(importState: ImportState): ChatColors? {
|
||||
if (this.customColorId != null) {
|
||||
return importState.remoteToLocalColorId[this.customColorId]?.let { localId ->
|
||||
val colorId = ChatColors.Id.forLongValue(localId)
|
||||
ChatColorsPalette.Bubbles.default.withId(colorId)
|
||||
return SignalDatabase.chatColors.getById(colorId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -45,17 +45,18 @@ class BannerManager @JvmOverloads constructor(
|
||||
return@setContent
|
||||
}
|
||||
|
||||
val bannerState by banner.dataFlow.collectAsStateWithLifecycle(initialValue = null)
|
||||
key(banner) {
|
||||
val bannerState by banner.dataFlow.collectAsStateWithLifecycle(initialValue = null)
|
||||
|
||||
bannerState?.let { model ->
|
||||
SignalTheme {
|
||||
Box {
|
||||
banner.DisplayBanner(model, PaddingValues(horizontal = 12.dp, vertical = 8.dp))
|
||||
bannerState?.let { model ->
|
||||
SignalTheme {
|
||||
Box {
|
||||
banner.DisplayBanner(model, PaddingValues(horizontal = 12.dp, vertical = 8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onNewBannerShownListener()
|
||||
} ?: onNoBannerShownListener()
|
||||
onNewBannerShownListener()
|
||||
} ?: onNoBannerShownListener()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,12 +66,16 @@ class BannerManager @JvmOverloads constructor(
|
||||
*/
|
||||
@Composable
|
||||
fun Banner() {
|
||||
val banner by rememberUpdatedState(banners.firstOrNull { it.enabled } as Banner<Any>?)
|
||||
val banner: Banner<Any>? = banners.firstOrNull { it.enabled } as Banner<Any>?
|
||||
if (banner == null) {
|
||||
return
|
||||
}
|
||||
|
||||
banner?.let { nonNullBanner ->
|
||||
val state by nonNullBanner.dataFlow.collectAsStateWithLifecycle(initialValue = null)
|
||||
state?.let { model ->
|
||||
nonNullBanner.DisplayBanner(model, PaddingValues(horizontal = 12.dp, vertical = 8.dp))
|
||||
key(banner) {
|
||||
val bannerState by banner.dataFlow.collectAsStateWithLifecycle(initialValue = null)
|
||||
|
||||
bannerState?.let { model ->
|
||||
banner.DisplayBanner(model, PaddingValues(horizontal = 12.dp, vertical = 8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ class MediaRestoreProgressBanner(private val listener: RestoreProgressBannerList
|
||||
totalRestoredSize > 0 -> {
|
||||
flowOf(
|
||||
BackupStatusData.RestoringMedia(
|
||||
bytesTotal = totalRestoredSize.bytes.also { totalRestoredSize = 0 },
|
||||
bytesTotal = totalRestoredSize.bytes,
|
||||
restoreStatus = BackupStatusData.RestoreStatus.FINISHED
|
||||
)
|
||||
)
|
||||
@@ -75,7 +75,10 @@ class MediaRestoreProgressBanner(private val listener: RestoreProgressBannerList
|
||||
data = model,
|
||||
onBannerClick = listener::onBannerClick,
|
||||
onActionClick = listener::onActionClick,
|
||||
onDismissClick = listener::onDismissComplete
|
||||
onDismissClick = {
|
||||
totalRestoredSize = 0
|
||||
listener.onDismissComplete()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -34,7 +34,6 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.util.BreakIteratorCompat
|
||||
@@ -45,11 +44,11 @@ class EditCallLinkNameDialogFragment : ComposeDialogFragment() {
|
||||
|
||||
companion object {
|
||||
const val RESULT_KEY = "edit_call_link_name"
|
||||
|
||||
private const val MAX_CHARACTER_COUNT = 32
|
||||
const val ARG_NAME = "name"
|
||||
}
|
||||
|
||||
private val args: EditCallLinkNameDialogFragmentArgs by navArgs()
|
||||
private val argName: String
|
||||
get() = requireArguments().getString(ARG_NAME)!!
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -69,8 +68,8 @@ class EditCallLinkNameDialogFragment : ComposeDialogFragment() {
|
||||
var callName by remember {
|
||||
mutableStateOf(
|
||||
TextFieldValue(
|
||||
text = args.name,
|
||||
selection = TextRange(args.name.length)
|
||||
text = argName,
|
||||
selection = TextRange(argName.length)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -31,9 +33,9 @@ import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.app.ShareCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.ui.compose.BottomSheets
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
@@ -125,9 +127,9 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
|
||||
|
||||
private fun onAddACallNameClicked() {
|
||||
val snapshot = viewModel.callLink.value
|
||||
findNavController().navigate(
|
||||
CreateCallLinkBottomSheetDialogFragmentDirections.actionCreateCallLinkBottomSheetToEditCallLinkNameDialogFragment(snapshot.state.name)
|
||||
)
|
||||
EditCallLinkNameDialogFragment().apply {
|
||||
arguments = bundleOf(EditCallLinkNameDialogFragment.ARG_NAME to snapshot.state.name)
|
||||
}.show(parentFragmentManager, null)
|
||||
}
|
||||
|
||||
private fun onJoinClicked() {
|
||||
@@ -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,14 @@ 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 +25,8 @@ 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.create.CreateCallLinkBottomSheetDialogFragment
|
||||
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,21 @@ 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.BottomSheetUtil
|
||||
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 org.thoughtcrime.securesms.window.WindowSizeClass.Companion.getWindowSizeClass
|
||||
import java.util.Objects
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Call Log tab.
|
||||
@@ -89,12 +84,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 +143,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,12 +170,16 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (!closeSearchIfOpen()) {
|
||||
tabsViewModel.onChatsSelected()
|
||||
mainNavigationViewModel.onChatsSelected()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (resources.getWindowSizeClass().isCompact()) {
|
||||
ViewUtil.setBottomMargin(binding.bottomActionBar, ViewUtil.getNavigationBarHeight(binding.bottomActionBar))
|
||||
}
|
||||
|
||||
signalBottomActionBarController = SignalBottomActionBarController(
|
||||
binding.bottomActionBar,
|
||||
binding.recycler,
|
||||
@@ -204,29 +198,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()
|
||||
})
|
||||
@@ -320,7 +294,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
}
|
||||
|
||||
override fun onCreateACallLinkClicked() {
|
||||
findNavController().navigate(R.id.createCallLinkBottomSheet)
|
||||
CreateCallLinkBottomSheetDialogFragment().show(parentFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
}
|
||||
|
||||
override fun onCallClicked(callLogRow: CallLogRow.Call) {
|
||||
@@ -363,14 +337,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 +443,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 +469,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
|
||||
|
||||
@@ -7,7 +7,8 @@ import org.signal.paging.PagedDataSource
|
||||
class CallLogPagedDataSource(
|
||||
private val query: String?,
|
||||
private val filter: CallLogFilter,
|
||||
private val repository: CallRepository
|
||||
private val repository: CallRepository,
|
||||
private val hasSelection: Boolean
|
||||
) : PagedDataSource<CallLogRow.Id, CallLogRow> {
|
||||
|
||||
companion object {
|
||||
@@ -46,7 +47,7 @@ class CallLogPagedDataSource(
|
||||
val clearFilterStart = callEventStart + callEventsCount
|
||||
|
||||
var remaining = length
|
||||
if (start < callLinkStart) {
|
||||
if (start < callLinkStart && !hasSelection) {
|
||||
callLogRows.add(CallLogRow.CreateCallLink)
|
||||
remaining -= 1
|
||||
}
|
||||
|
||||
@@ -59,14 +59,21 @@ class CallLogViewModel(
|
||||
|
||||
init {
|
||||
disposables.add(callLogStore)
|
||||
disposables += distinctQueryFilterPairs.subscribe { (query, filter) ->
|
||||
pagedData.onNext(
|
||||
PagedData.createForObservable(
|
||||
CallLogPagedDataSource(query, filter, callLogRepository),
|
||||
pagingConfig
|
||||
disposables += distinctQueryFilterPairs
|
||||
.switchMap { (query, filter) ->
|
||||
selected.map {
|
||||
Triple(query, filter, it != CallLogSelectionState.empty())
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.subscribe { (query, filter, hasSelection) ->
|
||||
pagedData.onNext(
|
||||
PagedData.createForObservable(
|
||||
CallLogPagedDataSource(query, filter, callLogRepository, hasSelection),
|
||||
pagingConfig
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
disposables += pagedData.map { it.controller }.subscribe {
|
||||
controller.set(it)
|
||||
|
||||
@@ -13,9 +13,9 @@ import org.signal.core.util.concurrent.SimpleTask
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.ContactSelectionActivity
|
||||
import org.thoughtcrime.securesms.ContactSelectionListFragment
|
||||
import org.thoughtcrime.securesms.InviteActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode
|
||||
import org.thoughtcrime.securesms.contacts.paged.ChatType
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
@@ -111,7 +111,7 @@ class NewCallActivity : ContactSelectionActivity(), ContactSelectionListFragment
|
||||
}
|
||||
|
||||
override fun onInvite() {
|
||||
startActivity(Intent(this, InviteActivity::class.java))
|
||||
startActivity(AppSettingsActivity.invite(this))
|
||||
}
|
||||
|
||||
private fun handleManualRefresh() {
|
||||
@@ -130,7 +130,7 @@ class NewCallActivity : ContactSelectionActivity(), ContactSelectionListFragment
|
||||
when (menuItem.itemId) {
|
||||
android.R.id.home -> ActivityCompat.finishAfterTransition(this@NewCallActivity)
|
||||
R.id.menu_refresh -> handleManualRefresh()
|
||||
R.id.menu_invite -> startActivity(Intent(this@NewCallActivity, InviteActivity::class.java))
|
||||
R.id.menu_invite -> startActivity(AppSettingsActivity.invite(this@NewCallActivity))
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -253,7 +253,7 @@ public class ComposeText extends EmojiEditText {
|
||||
}
|
||||
|
||||
public @NonNull List<Mention> getMentions() {
|
||||
return MentionAnnotation.getMentionsFromAnnotations(getText());
|
||||
return MentionAnnotation.getMentionsFromAnnotations(getTextTrimmed());
|
||||
}
|
||||
|
||||
public boolean hasStyling() {
|
||||
|
||||
@@ -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;
|
||||
@@ -565,6 +568,11 @@ public class InputPanel extends ConstraintLayout
|
||||
if (listener != null) listener.onRecorderPermissionRequired();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecorderAlreadyInUse() {
|
||||
if (listener != null) listener.onRecorderAlreadyInUse();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecordPressed() {
|
||||
if (listener != null) listener.onRecorderStarted();
|
||||
@@ -784,13 +792,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;
|
||||
}
|
||||
@@ -808,11 +820,13 @@ public class InputPanel extends ConstraintLayout
|
||||
void onRecorderFinished();
|
||||
void onRecorderCanceled(boolean byUser);
|
||||
void onRecorderPermissionRequired();
|
||||
void onRecorderAlreadyInUse();
|
||||
void onEmojiToggle();
|
||||
void onLinkPreviewCanceled();
|
||||
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 {
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components;
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.media.AudioManager;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
@@ -19,6 +20,7 @@ import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.audio.AudioRecordingHandler;
|
||||
@@ -40,12 +42,16 @@ public final class MicrophoneRecorderView extends FrameLayout implements View.On
|
||||
private @Nullable AudioRecordingHandler handler;
|
||||
private @NonNull State state = State.NOT_RUNNING;
|
||||
|
||||
private final AudioManager audioManager;
|
||||
|
||||
public MicrophoneRecorderView(Context context) {
|
||||
super(context);
|
||||
this.audioManager = ContextCompat.getSystemService(context, AudioManager.class);
|
||||
}
|
||||
|
||||
public MicrophoneRecorderView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
this.audioManager = ContextCompat.getSystemService(context, AudioManager.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -110,10 +116,16 @@ public final class MicrophoneRecorderView extends FrameLayout implements View.On
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View v, final MotionEvent event) {
|
||||
boolean isMicPossiblyInUse = false;
|
||||
if (audioManager != null) {
|
||||
isMicPossiblyInUse = audioManager.getMode() == AudioManager.MODE_IN_COMMUNICATION || audioManager.getMode() == AudioManager.MODE_IN_CALL;
|
||||
}
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
if (!Permissions.hasAll(getContext(), Manifest.permission.RECORD_AUDIO)) {
|
||||
if (handler != null) handler.onRecordPermissionRequired();
|
||||
} else if (isMicPossiblyInUse) {
|
||||
if (handler != null) handler.onRecorderAlreadyInUse();
|
||||
} else if (state == State.NOT_RUNNING) {
|
||||
state = State.RUNNING_HELD;
|
||||
floatingRecordButton.display(event.getX(), event.getY());
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package org.thoughtcrime.securesms.components.compose
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
* Adds a 'Beta' label next to [text] to indicate a feature is in development
|
||||
*/
|
||||
@Composable
|
||||
fun TextWithBetaLabel(
|
||||
text: String,
|
||||
textStyle: TextStyle = TextStyle.Default,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = textStyle
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.Beta__beta_title).uppercase(),
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 4.dp)
|
||||
.background(color = MaterialTheme.colorScheme.surfaceVariant, shape = RoundedCornerShape(28.dp))
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 'Beta' header to indicate a feature is currently in development
|
||||
*/
|
||||
@Composable
|
||||
fun BetaHeader(modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.symbol_info_24),
|
||||
contentDescription = stringResource(id = R.string.Beta__info),
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.Beta__this_is_beta),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(start = 12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
fun BetaLabelPreview() {
|
||||
Previews.Preview {
|
||||
TextWithBetaLabel("Signal Backups")
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
fun BetaHeaderPreview() {
|
||||
Previews.Preview {
|
||||
BetaHeader()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.compose
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components
|
||||
package org.thoughtcrime.securesms.components.compose
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
@@ -1,4 +1,9 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.compose
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.compose
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.role
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
* A custom circular [Checkbox] that can be toggled between checked an unchecked states.
|
||||
*
|
||||
* @param checked Indicates whether the checkbox is checked or not.
|
||||
* @param onCheckedChange A callback function invoked when this checkbox is clicked.
|
||||
* @param modifier The [Modifier] to be applied to this checkbox.
|
||||
*/
|
||||
@Composable
|
||||
fun RoundCheckbox(
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val contentDescription = if (checked) {
|
||||
stringResource(R.string.SignalCheckbox_accessibility_checked_description)
|
||||
} else {
|
||||
stringResource(R.string.SignalCheckbox_accessibility_unchecked_description)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.padding(12.dp)
|
||||
.size(24.dp)
|
||||
.aspectRatio(1f)
|
||||
.border(
|
||||
width = 1.5.dp,
|
||||
color = if (checked) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.outline
|
||||
},
|
||||
shape = CircleShape
|
||||
)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
onClick = { onCheckedChange(!checked) },
|
||||
onClickLabel = stringResource(R.string.SignalCheckbox_accessibility_on_click_label)
|
||||
)
|
||||
.semantics(mergeDescendants = true) {
|
||||
this.role = Role.Checkbox
|
||||
this.contentDescription = contentDescription
|
||||
}
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = checked,
|
||||
enter = fadeIn(animationSpec = tween(durationMillis = 150)) + scaleIn(initialScale = 1.20f, animationSpec = tween(durationMillis = 500)),
|
||||
exit = fadeOut(animationSpec = tween(durationMillis = 300)) + scaleOut(targetScale = 0.50f, animationSpec = tween(durationMillis = 600))
|
||||
) {
|
||||
Image(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.ic_check_circle_solid_24),
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun RoundCheckboxCheckedPreview() = SignalTheme {
|
||||
RoundCheckbox(checked = true, onCheckedChange = {})
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun RoundCheckboxUncheckedPreview() = SignalTheme {
|
||||
RoundCheckbox(checked = false, onCheckedChange = {})
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -10,6 +10,23 @@ import android.view.animation.AnimationUtils
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
@@ -20,7 +37,7 @@ import org.thoughtcrime.securesms.util.ViewUtil
|
||||
*
|
||||
* Overflow items are rendered in a [SignalContextMenu].
|
||||
*/
|
||||
class SignalBottomActionBar(context: Context, attributeSet: AttributeSet) : LinearLayout(context, attributeSet) {
|
||||
class SignalBottomActionBar(context: Context, attributeSet: AttributeSet?) : LinearLayout(context, attributeSet) {
|
||||
|
||||
val items: MutableList<ActionItem> = mutableListOf()
|
||||
|
||||
@@ -118,3 +135,55 @@ class SignalBottomActionBar(context: Context, attributeSet: AttributeSet) : Line
|
||||
view.setOnClickListener { item.action.run() }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SignalBottomActionBar(
|
||||
visible: Boolean = true,
|
||||
items: List<ActionItem>,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val slideAnimationOffset = with(LocalDensity.current) { 40.dp.roundToPx() }
|
||||
|
||||
val enterAnimation = slideInVertically(
|
||||
animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing),
|
||||
initialOffsetY = { slideAnimationOffset }
|
||||
) + fadeIn(
|
||||
animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing)
|
||||
)
|
||||
|
||||
val exitAnimation = slideOutVertically(
|
||||
animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing),
|
||||
targetOffsetY = { slideAnimationOffset }
|
||||
) + fadeOut(
|
||||
animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing)
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = enterAnimation,
|
||||
exit = exitAnimation,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 16.dp)
|
||||
.wrapContentHeight()
|
||||
) {
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
SignalBottomActionBar(context, null)
|
||||
.apply {
|
||||
elevation = 0f
|
||||
setItems(items)
|
||||
}
|
||||
},
|
||||
update = { view ->
|
||||
view.setItems(items)
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(4.dp) // prevent shadow clipping during visibility animations
|
||||
.shadow(
|
||||
elevation = 4.dp,
|
||||
shape = RoundedCornerShape(18.dp)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,4 +26,12 @@ public enum Orientation {
|
||||
|
||||
return PORTRAIT_BOTTOM_EDGE;
|
||||
}
|
||||
|
||||
public static @NonNull Orientation fromSurfaceRotation(int surfaceRotation) {
|
||||
return switch (surfaceRotation) {
|
||||
case 1 -> LANDSCAPE_LEFT_EDGE;
|
||||
case 3 -> LANDSCAPE_RIGHT_EDGE;
|
||||
default -> PORTRAIT_BOTTOM_EDGE;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ class AppSettingsActivity : DSLSettingsActivity(), GooglePayComponent {
|
||||
)
|
||||
|
||||
StartLocation.BACKUPS_SETTINGS -> AppSettingsFragmentDirections.actionDirectToBackupsSettingsFragment()
|
||||
StartLocation.INVITE -> AppSettingsFragmentDirections.actionDirectToInviteFragment()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,6 +224,9 @@ class AppSettingsActivity : DSLSettingsActivity(), GooglePayComponent {
|
||||
@JvmStatic
|
||||
fun backupsSettings(context: Context): Intent = getIntentForStartLocation(context, StartLocation.BACKUPS_SETTINGS)
|
||||
|
||||
@JvmStatic
|
||||
fun invite(context: Context): Intent = getIntentForStartLocation(context, StartLocation.INVITE)
|
||||
|
||||
private fun getIntentForStartLocation(context: Context, startLocation: StartLocation): Intent {
|
||||
return Intent(context, AppSettingsActivity::class.java)
|
||||
.putExtra(ARG_NAV_GRAPH, R.navigation.app_settings_with_change_number)
|
||||
@@ -250,7 +254,8 @@ class AppSettingsActivity : DSLSettingsActivity(), GooglePayComponent {
|
||||
REMOTE_BACKUPS(16),
|
||||
CHAT_FOLDERS(17),
|
||||
CREATE_CHAT_FOLDER(18),
|
||||
BACKUPS_SETTINGS(19);
|
||||
BACKUPS_SETTINGS(19),
|
||||
INVITE(20);
|
||||
|
||||
companion object {
|
||||
fun fromCode(code: Int?): StartLocation {
|
||||
|
||||
@@ -29,12 +29,14 @@ import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
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.fragment.app.viewModels
|
||||
@@ -64,6 +66,7 @@ import org.thoughtcrime.securesms.banner.banners.UnauthorizedBanner
|
||||
import org.thoughtcrime.securesms.banner.ui.compose.Action
|
||||
import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner
|
||||
import org.thoughtcrime.securesms.banner.ui.compose.Importance
|
||||
import org.thoughtcrime.securesms.components.compose.TextWithBetaLabel
|
||||
import org.thoughtcrime.securesms.components.emoji.Emojifier
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.BadgeImageMedium
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
@@ -181,7 +184,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(
|
||||
@@ -371,8 +374,19 @@ private fun AppSettingsContent(
|
||||
if (state.showBackups) {
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.preferences_chats__backups),
|
||||
icon = painterResource(R.drawable.symbol_backup_24),
|
||||
text = {
|
||||
TextWithBetaLabel(
|
||||
text = stringResource(R.string.preferences_chats__backups),
|
||||
textStyle = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.symbol_backup_24),
|
||||
contentDescription = stringResource(R.string.preferences_chats__backups),
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
callbacks.navigate(R.id.action_appSettingsFragment_to_backupsSettingsFragment)
|
||||
},
|
||||
@@ -468,7 +482,7 @@ private fun AppSettingsContent(
|
||||
text = stringResource(R.string.AppSettingsFragment__invite_your_friends),
|
||||
icon = painterResource(R.drawable.symbol_invite_24),
|
||||
onClick = {
|
||||
callbacks.navigate(R.id.action_appSettingsFragment_to_inviteActivity)
|
||||
callbacks.navigate(R.id.action_appSettingsFragment_to_inviteFragment)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -69,10 +69,10 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(if (state.hasOptedInWithAccess) R.string.preferences_app_protection__change_your_pin else R.string.preferences_app_protection__create_a_pin),
|
||||
isEnabled = state.isDeprecatedOrUnregistered(),
|
||||
title = DSLSettingsText.from(if (state.hasPin || state.hasRestoredAep) R.string.preferences_app_protection__change_your_pin else R.string.preferences_app_protection__create_a_pin),
|
||||
isEnabled = state.isNotDeprecatedOrUnregistered(),
|
||||
onClick = {
|
||||
if (state.hasOptedInWithAccess) {
|
||||
if (state.hasPin) {
|
||||
startActivityForResult(CreateSvrPinActivity.getIntentForPinChangeFromSettings(requireContext()), CreateSvrPinActivity.REQUEST_NEW_PIN)
|
||||
} else {
|
||||
startActivityForResult(CreateSvrPinActivity.getIntentForPinCreate(requireContext()), CreateSvrPinActivity.REQUEST_NEW_PIN)
|
||||
@@ -84,7 +84,7 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
|
||||
title = DSLSettingsText.from(R.string.preferences_app_protection__pin_reminders),
|
||||
summary = DSLSettingsText.from(R.string.AccountSettingsFragment__youll_be_asked_less_frequently),
|
||||
isChecked = state.hasPin && state.pinRemindersEnabled,
|
||||
isEnabled = state.hasPin && state.isDeprecatedOrUnregistered(),
|
||||
isEnabled = state.hasPin && state.isNotDeprecatedOrUnregistered(),
|
||||
onClick = {
|
||||
setPinRemindersEnabled(!state.pinRemindersEnabled)
|
||||
}
|
||||
@@ -94,7 +94,7 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
|
||||
title = DSLSettingsText.from(R.string.preferences_app_protection__registration_lock),
|
||||
summary = DSLSettingsText.from(R.string.AccountSettingsFragment__require_your_signal_pin),
|
||||
isChecked = state.registrationLockEnabled,
|
||||
isEnabled = (state.hasOptedInWithAccess) && state.isDeprecatedOrUnregistered(),
|
||||
isEnabled = state.hasPin && state.isNotDeprecatedOrUnregistered(),
|
||||
onClick = {
|
||||
setRegistrationLockEnabled(!state.registrationLockEnabled)
|
||||
}
|
||||
@@ -102,7 +102,7 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__advanced_pin_settings),
|
||||
isEnabled = state.isDeprecatedOrUnregistered(),
|
||||
isEnabled = state.isNotDeprecatedOrUnregistered(),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_accountSettingsFragment_to_advancedPinSettingsActivity)
|
||||
}
|
||||
@@ -115,7 +115,7 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
|
||||
if (SignalStore.account.isRegistered) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.AccountSettingsFragment__change_phone_number),
|
||||
isEnabled = state.isDeprecatedOrUnregistered(),
|
||||
isEnabled = state.isNotDeprecatedOrUnregistered(),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_accountSettingsFragment_to_changePhoneNumberFragment)
|
||||
}
|
||||
@@ -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.isNotDeprecatedOrUnregistered(),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_accountSettingsFragment_to_oldDeviceTransferActivity)
|
||||
}
|
||||
@@ -132,13 +133,13 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.AccountSettingsFragment__request_account_data),
|
||||
isEnabled = state.isDeprecatedOrUnregistered(),
|
||||
isEnabled = state.isNotDeprecatedOrUnregistered(),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_accountSettingsFragment_to_exportAccountFragment)
|
||||
}
|
||||
)
|
||||
|
||||
if (!state.isDeprecatedOrUnregistered()) {
|
||||
if (!state.isNotDeprecatedOrUnregistered()) {
|
||||
if (state.clientDeprecated) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences_account_update_signal),
|
||||
@@ -173,8 +174,8 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
|
||||
}
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__delete_account, ContextCompat.getColor(requireContext(), if (state.isDeprecatedOrUnregistered()) R.color.signal_alert_primary else R.color.signal_alert_primary_50)),
|
||||
isEnabled = state.isDeprecatedOrUnregistered(),
|
||||
title = DSLSettingsText.from(R.string.preferences__delete_account, ContextCompat.getColor(requireContext(), if (state.isNotDeprecatedOrUnregistered()) R.color.signal_alert_primary else R.color.signal_alert_primary_50)),
|
||||
isEnabled = state.isNotDeprecatedOrUnregistered(),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_accountSettingsFragment_to_deleteAccountFragment)
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ package org.thoughtcrime.securesms.components.settings.app.account
|
||||
|
||||
data class AccountSettingsState(
|
||||
val hasPin: Boolean,
|
||||
val hasOptedInWithAccess: Boolean,
|
||||
val hasRestoredAep: Boolean,
|
||||
val pinRemindersEnabled: Boolean,
|
||||
val registrationLockEnabled: Boolean,
|
||||
val userUnregistered: Boolean,
|
||||
val clientDeprecated: Boolean
|
||||
) {
|
||||
fun isDeprecatedOrUnregistered(): Boolean {
|
||||
fun isNotDeprecatedOrUnregistered(): Boolean {
|
||||
return !(userUnregistered || clientDeprecated)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ class AccountSettingsViewModel : ViewModel() {
|
||||
private fun getCurrentState(): AccountSettingsState {
|
||||
return AccountSettingsState(
|
||||
hasPin = SignalStore.svr.hasPin() && !SignalStore.svr.hasOptedOut(),
|
||||
hasOptedInWithAccess = SignalStore.svr.hasOptedInWithAccess(),
|
||||
hasRestoredAep = SignalStore.account.restoredAccountEntropyPool,
|
||||
pinRemindersEnabled = SignalStore.pin.arePinRemindersEnabled() && SignalStore.svr.hasPin(),
|
||||
registrationLockEnabled = SignalStore.svr.isRegistrationLockEnabled,
|
||||
userUnregistered = TextSecurePreferences.isUnauthorizedReceived(AppDependencies.application),
|
||||
|
||||
@@ -27,10 +27,12 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
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.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -46,6 +48,7 @@ import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import org.thoughtcrime.securesms.components.compose.TextWithBetaLabel
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.MessageBackupsCheckoutLauncher.createBackupsCheckoutLauncher
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
@@ -114,7 +117,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(
|
||||
@@ -228,9 +231,7 @@ private fun NeverEnabledBackupsRow(
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__signal_backups)
|
||||
)
|
||||
TextWithBetaLabel(text = stringResource(R.string.RemoteBackupsSettingsFragment__signal_backups))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.BackupsSettingsFragment_automatic_backups_with_signals),
|
||||
@@ -268,9 +269,23 @@ private fun InactiveBackupsRow(
|
||||
onBackupsRowClick: () -> Unit = {}
|
||||
) {
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__signal_backups),
|
||||
label = stringResource(R.string.preferences_off),
|
||||
icon = painterResource(R.drawable.symbol_backup_24),
|
||||
text = {
|
||||
Column {
|
||||
TextWithBetaLabel(text = stringResource(R.string.RemoteBackupsSettingsFragment__signal_backups))
|
||||
Text(
|
||||
text = stringResource(R.string.preferences_off),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.symbol_backup_24),
|
||||
contentDescription = stringResource(R.string.preferences_chats__backups),
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
onClick = onBackupsRowClick
|
||||
)
|
||||
}
|
||||
@@ -297,9 +312,7 @@ private fun ActiveBackupsRow(
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__signal_backups)
|
||||
)
|
||||
TextWithBetaLabel(text = stringResource(R.string.RemoteBackupsSettingsFragment__signal_backups))
|
||||
|
||||
when (enabledState.type) {
|
||||
is MessageBackupsType.Paid -> {
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.backups
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
@@ -16,6 +18,7 @@ import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.rx3.asFlow
|
||||
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
|
||||
@@ -41,37 +44,54 @@ class BackupsSettingsViewModel : ViewModel() {
|
||||
|
||||
val stateFlow: StateFlow<BackupsSettingsState> = internalStateFlow
|
||||
|
||||
private val loadRequests = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
viewModelScope.launch(SignalDispatchers.Default) {
|
||||
InternetConnectionObserver.observe().asFlow()
|
||||
.distinctUntilChanged()
|
||||
.filter { it }
|
||||
.drop(1)
|
||||
.collect {
|
||||
refreshState()
|
||||
Log.d(TAG, "Triggering refresh from internet reconnect.")
|
||||
loadRequests.tryEmit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch(SignalDispatchers.Default) {
|
||||
loadRequests.collect {
|
||||
Log.d(TAG, "-- Dispatching state load.")
|
||||
loadEnabledState().join()
|
||||
Log.d(TAG, "-- Completed state load.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshState() {
|
||||
Log.d(TAG, "Refreshing state.")
|
||||
loadEnabledState()
|
||||
override fun onCleared() {
|
||||
Log.d(TAG, "ViewModel has been cleared.")
|
||||
}
|
||||
|
||||
private fun loadEnabledState() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
fun refreshState() {
|
||||
Log.d(TAG, "Refreshing state from manual call.")
|
||||
loadRequests.tryEmit(Unit)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun loadEnabledState(): Job {
|
||||
return viewModelScope.launch(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 +102,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 +119,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 +149,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 {
|
||||
|
||||
@@ -16,12 +16,17 @@ import org.thoughtcrime.securesms.util.Util
|
||||
* Fragment which only displays the backup key to the user.
|
||||
*/
|
||||
class BackupKeyDisplayFragment : ComposeFragment() {
|
||||
|
||||
companion object {
|
||||
const val CLIPBOARD_TIMEOUT_SECONDS = 60
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
MessageBackupsKeyRecordScreen(
|
||||
backupKey = SignalStore.account.accountEntropyPool.displayValue,
|
||||
onNavigationClick = { findNavController().popBackStack() },
|
||||
onCopyToClipboardClick = { Util.copyToClipboard(requireContext(), it) },
|
||||
onCopyToClipboardClick = { Util.copyToClipboard(requireContext(), it, CLIPBOARD_TIMEOUT_SECONDS) },
|
||||
onNextClick = { findNavController().popBackStack() }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -40,10 +38,12 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
@@ -54,6 +54,7 @@ import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
@@ -95,11 +96,13 @@ import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusData
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusRow
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import org.thoughtcrime.securesms.billing.launchManageBackupsSubscription
|
||||
import org.thoughtcrime.securesms.components.compose.BetaHeader
|
||||
import org.thoughtcrime.securesms.components.compose.TextWithBetaLabel
|
||||
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
|
||||
@@ -219,7 +222,7 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
|
||||
}
|
||||
|
||||
override fun onCancelMediaRestore() {
|
||||
viewModel.cancelMediaRestore()
|
||||
viewModel.requestDialog(RemoteBackupsSettingsState.Dialog.CANCEL_MEDIA_RESTORE_PROTECTION)
|
||||
}
|
||||
|
||||
override fun onDisplaySkipMediaRestoreProtectionDialog() {
|
||||
@@ -247,8 +250,12 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
|
||||
BackupAlertBottomSheet.create(BackupAlert.BackupFailed).show(parentFragmentManager, null)
|
||||
}
|
||||
|
||||
override fun onRestoreUsingCellularClick(canUseCellular: Boolean) {
|
||||
viewModel.setCanRestoreUsingCellular(canUseCellular)
|
||||
override fun onRestoreUsingCellularConfirm() {
|
||||
viewModel.requestDialog(RemoteBackupsSettingsState.Dialog.RESTORE_OVER_CELLULAR_PROTECTION)
|
||||
}
|
||||
|
||||
override fun onRestoreUsingCellularClick() {
|
||||
viewModel.setCanRestoreUsingCellular()
|
||||
}
|
||||
|
||||
override fun onRedemptionErrorDetailsClick() {
|
||||
@@ -336,10 +343,12 @@ private interface ContentCallbacks {
|
||||
fun onLearnMoreAboutLostSubscription() = Unit
|
||||
fun onContactSupport() = Unit
|
||||
fun onLearnMoreAboutBackupFailure() = Unit
|
||||
fun onRestoreUsingCellularClick(canUseCellular: Boolean) = Unit
|
||||
fun onRestoreUsingCellularConfirm() = Unit
|
||||
fun onRestoreUsingCellularClick() = Unit
|
||||
fun onRedemptionErrorDetailsClick() = Unit
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun RemoteBackupsSettingsContent(
|
||||
backupsEnabled: Boolean,
|
||||
@@ -360,10 +369,22 @@ private fun RemoteBackupsSettingsContent(
|
||||
SnackbarHostState()
|
||||
}
|
||||
|
||||
Scaffolds.Settings(
|
||||
title = stringResource(id = R.string.RemoteBackupsSettingsFragment__signal_backups),
|
||||
onNavigationClick = contentCallbacks::onNavigationClick,
|
||||
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_left_24),
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
Scaffolds.DefaultTopAppBar(
|
||||
title = stringResource(R.string.RemoteBackupsSettingsFragment__signal_backups),
|
||||
titleContent = { _, title ->
|
||||
TextWithBetaLabel(text = title, textStyle = MaterialTheme.typography.titleLarge)
|
||||
},
|
||||
onNavigationClick = contentCallbacks::onNavigationClick,
|
||||
navigationIconPainter = painterResource(R.drawable.symbol_arrow_start_24),
|
||||
navigationContentDescription = stringResource(R.string.DefaultTopAppBar__navigate_up_content_description),
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
snackbarHost = {
|
||||
Snackbars.Host(snackbarHostState = snackbarHostState)
|
||||
}
|
||||
@@ -372,6 +393,10 @@ private fun RemoteBackupsSettingsContent(
|
||||
modifier = Modifier
|
||||
.padding(it)
|
||||
) {
|
||||
item {
|
||||
BetaHeader(modifier = Modifier.padding(horizontal = 16.dp))
|
||||
}
|
||||
|
||||
if (hasRedemptionError) {
|
||||
item {
|
||||
RedemptionErrorAlert(onDetailsClick = contentCallbacks::onRedemptionErrorDetailsClick)
|
||||
@@ -427,18 +452,20 @@ private fun RemoteBackupsSettingsContent(
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.ToggleRow(
|
||||
checked = canRestoreUsingCellular,
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__restore_using_cellular),
|
||||
onCheckChanged = contentCallbacks::onRestoreUsingCellularClick
|
||||
)
|
||||
if (!canRestoreUsingCellular) {
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__resume_download),
|
||||
icon = painterResource(R.drawable.symbol_arrow_circle_down_24),
|
||||
onClick = contentCallbacks::onRestoreUsingCellularConfirm
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (backupRestoreState is BackupRestoreState.Ready && backupState is RemoteBackupsSettingsState.BackupState.Canceled) {
|
||||
} else if (backupRestoreState is BackupRestoreState.Ready) {
|
||||
item {
|
||||
BackupReadyToDownloadRow(
|
||||
ready = backupRestoreState,
|
||||
endOfSubscription = backupState.renewalTime,
|
||||
backupState = backupState,
|
||||
onDownloadClick = contentCallbacks::onStartMediaRestore
|
||||
)
|
||||
}
|
||||
@@ -538,6 +565,20 @@ private fun RemoteBackupsSettingsContent(
|
||||
onSkipClick = contentCallbacks::onSkipMediaRestore
|
||||
)
|
||||
}
|
||||
|
||||
RemoteBackupsSettingsState.Dialog.CANCEL_MEDIA_RESTORE_PROTECTION -> {
|
||||
CancelInitialRestoreDialog(
|
||||
onDismiss = contentCallbacks::onDialogDismissed,
|
||||
onSkipClick = contentCallbacks::onSkipMediaRestore
|
||||
)
|
||||
}
|
||||
|
||||
RemoteBackupsSettingsState.Dialog.RESTORE_OVER_CELLULAR_PROTECTION -> {
|
||||
ResumeRestoreOverCellularDialog(
|
||||
onDismiss = contentCallbacks::onDialogDismissed,
|
||||
onResumeOverCellularClick = contentCallbacks::onRestoreUsingCellularClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val snackbarMessageId = remember(requestedSnackbar) {
|
||||
@@ -687,14 +728,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
|
||||
)
|
||||
@@ -1170,6 +1207,37 @@ private fun SkipDownloadDialog(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CancelInitialRestoreDialog(
|
||||
onSkipClick: () -> Unit = {},
|
||||
onDismiss: () -> Unit = {}
|
||||
) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(R.string.RemoteBackupsSettingsFragment__skip_restore_question),
|
||||
body = stringResource(R.string.RemoteBackupsSettingsFragment__skip_restore_message),
|
||||
confirm = stringResource(R.string.RemoteBackupsSettingsFragment__skip),
|
||||
dismiss = stringResource(android.R.string.cancel),
|
||||
confirmColor = MaterialTheme.colorScheme.error,
|
||||
onConfirm = onSkipClick,
|
||||
onDismiss = onDismiss
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ResumeRestoreOverCellularDialog(
|
||||
onResumeOverCellularClick: () -> Unit = {},
|
||||
onDismiss: () -> Unit = {}
|
||||
) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(R.string.ResumeRestoreCellular_resume_using_cellular_title),
|
||||
body = stringResource(R.string.ResumeRestoreCellular_resume_using_cellular_message),
|
||||
confirm = stringResource(R.string.BackupStatus__resume),
|
||||
dismiss = stringResource(android.R.string.cancel),
|
||||
onConfirm = onResumeOverCellularClick,
|
||||
onDismiss = onDismiss
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun CircularProgressDialog(
|
||||
@@ -1183,8 +1251,8 @@ private fun CircularProgressDialog(
|
||||
)
|
||||
) {
|
||||
Surface(
|
||||
shape = AlertDialogDefaults.shape,
|
||||
color = AlertDialogDefaults.containerColor
|
||||
shape = Dialogs.Defaults.shape,
|
||||
color = Dialogs.Defaults.containerColor
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
@@ -1206,21 +1274,21 @@ 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),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.padding(24.dp)
|
||||
)
|
||||
|
||||
@@ -1260,11 +1328,16 @@ private fun BackupFrequencyDialog(
|
||||
@Composable
|
||||
private fun BackupReadyToDownloadRow(
|
||||
ready: BackupRestoreState.Ready,
|
||||
endOfSubscription: Duration,
|
||||
backupState: RemoteBackupsSettingsState.BackupState,
|
||||
onDownloadClick: () -> Unit = {}
|
||||
) {
|
||||
val days = (endOfSubscription - System.currentTimeMillis().milliseconds).inWholeDays.toInt()
|
||||
val string = pluralStringResource(R.plurals.RemoteBackupsSettingsFragment__you_have_s_of_backup_data, days, ready.bytes, days)
|
||||
val string = if (backupState is RemoteBackupsSettingsState.BackupState.Canceled) {
|
||||
val days = (backupState.renewalTime - System.currentTimeMillis().milliseconds).inWholeDays.toInt()
|
||||
pluralStringResource(R.plurals.RemoteBackupsSettingsFragment__you_have_s_of_backup_data, days, ready.bytes, days)
|
||||
} else {
|
||||
stringResource(R.string.RemoteBackupsSettingsFragment__you_have_s_of_backup_data_not_on_device, ready.bytes)
|
||||
}
|
||||
|
||||
val annotated = buildAnnotatedString {
|
||||
append(string)
|
||||
val startIndex = string.indexOf(ready.bytes)
|
||||
@@ -1442,7 +1515,7 @@ private fun BackupReadyToDownloadPreview() {
|
||||
Previews.Preview {
|
||||
BackupReadyToDownloadRow(
|
||||
ready = BackupRestoreState.Ready("12GB"),
|
||||
endOfSubscription = System.currentTimeMillis().milliseconds + 30.days
|
||||
backupState = RemoteBackupsSettingsState.BackupState.None
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +115,9 @@ data class RemoteBackupsSettingsState(
|
||||
DOWNLOADING_YOUR_BACKUP,
|
||||
TURN_OFF_FAILED,
|
||||
SUBSCRIPTION_NOT_FOUND,
|
||||
SKIP_MEDIA_RESTORE_PROTECTION
|
||||
SKIP_MEDIA_RESTORE_PROTECTION,
|
||||
CANCEL_MEDIA_RESTORE_PROTECTION,
|
||||
RESTORE_OVER_CELLULAR_PROTECTION
|
||||
}
|
||||
|
||||
enum class Snackbar {
|
||||
|
||||
@@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.takeWhile
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -54,7 +55,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||
class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(RemoteBackupsSettingsFragment::class)
|
||||
private val TAG = Log.tag(RemoteBackupsSettingsViewModel::class)
|
||||
}
|
||||
|
||||
private val _state = MutableStateFlow(
|
||||
@@ -83,15 +84,25 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val restoreProgress = MediaRestoreProgressBanner()
|
||||
|
||||
var optimizedRemainingBytes = 0L
|
||||
while (isActive) {
|
||||
if (restoreProgress.enabled) {
|
||||
Log.d(TAG, "Backup is being restored. Collecting updates.")
|
||||
restoreProgress.dataFlow.collectLatest { latest ->
|
||||
_restoreState.update { BackupRestoreState.FromBackupStatusData(latest) }
|
||||
}
|
||||
restoreProgress
|
||||
.dataFlow
|
||||
.takeWhile { it !is BackupStatusData.RestoringMedia || it.restoreStatus != BackupStatusData.RestoreStatus.FINISHED }
|
||||
.collectLatest { latest ->
|
||||
_restoreState.update { BackupRestoreState.FromBackupStatusData(latest) }
|
||||
}
|
||||
} else if (
|
||||
!SignalStore.backup.optimizeStorage &&
|
||||
SignalStore.backup.userManuallySkippedMediaRestore &&
|
||||
SignalDatabase.attachments.getOptimizedMediaAttachmentSize().also { optimizedRemainingBytes = it } > 0
|
||||
) {
|
||||
_restoreState.update { BackupRestoreState.Ready(optimizedRemainingBytes.bytes.toUnitString()) }
|
||||
} else if (SignalStore.backup.totalRestorableAttachmentSize > 0L) {
|
||||
_restoreState.update { BackupRestoreState.Ready(SignalStore.backup.totalRestorableAttachmentSize.bytes.toUnitString()) }
|
||||
} else if (BackupRepository.shouldDisplayBackupFailedSettingsRow()) {
|
||||
@@ -126,9 +137,9 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
_state.update { it.copy(canBackUpUsingCellular = canBackUpUsingCellular) }
|
||||
}
|
||||
|
||||
fun setCanRestoreUsingCellular(canRestoreUsingCellular: Boolean) {
|
||||
SignalStore.backup.restoreWithCellular = canRestoreUsingCellular
|
||||
_state.update { it.copy(canRestoreUsingCellular = canRestoreUsingCellular) }
|
||||
fun setCanRestoreUsingCellular() {
|
||||
SignalStore.backup.restoreWithCellular = true
|
||||
_state.update { it.copy(canRestoreUsingCellular = true) }
|
||||
}
|
||||
|
||||
fun setBackupsFrequency(backupsFrequency: BackupFrequency) {
|
||||
@@ -139,17 +150,13 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
fun beginMediaRestore() {
|
||||
// TODO - [backups] Begin media restore.
|
||||
BackupRepository.resumeMediaRestore()
|
||||
}
|
||||
|
||||
fun skipMediaRestore() {
|
||||
BackupRepository.skipMediaRestore()
|
||||
}
|
||||
|
||||
fun cancelMediaRestore() {
|
||||
// TODO - [backups] Cancel in-progress media restoration
|
||||
}
|
||||
|
||||
fun requestDialog(dialog: RemoteBackupsSettingsState.Dialog) {
|
||||
_state.update { it.copy(dialog = dialog) }
|
||||
}
|
||||
@@ -207,6 +214,15 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
private suspend fun refreshState(lastPurchase: InAppPaymentTable.InAppPayment?) {
|
||||
try {
|
||||
performStateRefresh(lastPurchase)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "State refresh failed", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun performStateRefresh(lastPurchase: InAppPaymentTable.InAppPayment?) {
|
||||
val tier = SignalStore.backup.latestBackupTier
|
||||
|
||||
_state.update {
|
||||
@@ -262,18 +278,26 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
it.copy(
|
||||
backupState = RemoteBackupsSettingsState.BackupState.SubscriptionMismatchMissingGooglePlay(
|
||||
messageBackupsType = type,
|
||||
renewalTime = activeSubscription!!.activeSubscription.endOfCurrentPeriod.seconds
|
||||
renewalTime = activeSubscription.activeSubscription.endOfCurrentPeriod.seconds
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
hasActiveSignalSubscription && hasActiveGooglePlayBillingSubscription -> {
|
||||
Log.d(TAG, "Found erroneous mismatch. Clearing.")
|
||||
Log.d(TAG, "Found active signal subscription and active google play subscription. Clearing mismatch.")
|
||||
SignalStore.backup.subscriptionStateMismatchDetected = false
|
||||
}
|
||||
|
||||
!hasActiveSignalSubscription && !hasActiveGooglePlayBillingSubscription -> {
|
||||
Log.d(TAG, "Found inactive signal subscription and inactive google play subscription. Clearing mismatch.")
|
||||
SignalStore.backup.subscriptionStateMismatchDetected = false
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.w(TAG, "Hit unexpected subscription mismatch state: signal:false, google:true")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -287,6 +311,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
BackupRepository.getBackupsType(tier) as MessageBackupsType.Paid
|
||||
}
|
||||
|
||||
Log.d(TAG, "Attempting to retrieve current subscription...")
|
||||
val activeSubscription = withContext(Dispatchers.IO) {
|
||||
RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -267,12 +267,14 @@ class ChangeNumberRepository(
|
||||
|
||||
SignalStore.misc.setPendingChangeNumberMetadata(metadata)
|
||||
withContext(Dispatchers.IO) {
|
||||
result = accountManager.registrationApi.changeNumber(request)
|
||||
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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user