Compare commits
554 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82d42c03f7 | ||
|
|
c0f8e5adbf | ||
|
|
c54c73cb48 | ||
|
|
02c8656b92 | ||
|
|
3553a28683 | ||
|
|
acf4e97578 | ||
|
|
5142c8c58f | ||
|
|
55919cba59 | ||
|
|
1a6bd3d3f2 | ||
|
|
100dc54292 | ||
|
|
cffbfcb957 | ||
|
|
f73c5dde6b | ||
|
|
d5a466851a | ||
|
|
38836198a1 | ||
|
|
52429dcd33 | ||
|
|
fae427c09b | ||
|
|
e22ddb8f96 | ||
|
|
19f0722df3 | ||
|
|
921f7a70b3 | ||
|
|
bb8faebc7d | ||
|
|
5ed6a05eb9 | ||
|
|
a4a4665aaa | ||
|
|
5d16d1cd23 | ||
|
|
38b6362b25 | ||
|
|
ef0c6c79cb | ||
|
|
f10d5651f0 | ||
|
|
8a2f89b4f6 | ||
|
|
6563ea970f | ||
|
|
f1cb416bda | ||
|
|
df48e5ce92 | ||
|
|
e710e231ad | ||
|
|
9599d3a0b6 | ||
|
|
1fad4d4f65 | ||
|
|
f57e06677b | ||
|
|
f7b9942f11 | ||
|
|
2f1b05f882 | ||
|
|
6650f41200 | ||
|
|
2d8de03e05 | ||
|
|
9ffa866907 | ||
|
|
3c0c5478b5 | ||
|
|
aae888f5af | ||
|
|
e00a3730b4 | ||
|
|
5c2394aa4f | ||
|
|
c6be273a38 | ||
|
|
33236ea8e6 | ||
|
|
a6f1e0e972 | ||
|
|
fa8f8beb56 | ||
|
|
11db59d8a1 | ||
|
|
39a11ce26c | ||
|
|
8bb1b2d596 | ||
|
|
3f1abe05fc | ||
|
|
08ac99b4c1 | ||
|
|
ebf2ef65e2 | ||
|
|
8cb74fb776 | ||
|
|
eccb796199 | ||
|
|
4635a77fbc | ||
|
|
9505c3d070 | ||
|
|
657a9c7b0a | ||
|
|
e22560a794 | ||
|
|
c081193373 | ||
|
|
d23faf4278 | ||
|
|
da7e4cefd5 | ||
|
|
0d9a5ef9a6 | ||
|
|
e5aea7c49e | ||
|
|
b8c42fa57e | ||
|
|
2a086ad574 | ||
|
|
33346d8033 | ||
|
|
1446af97a2 | ||
|
|
64b5dad783 | ||
|
|
a6e7f9a4c1 | ||
|
|
70b0a120f0 | ||
|
|
4a6569fa1c | ||
|
|
f5173fa6f5 | ||
|
|
5478285362 | ||
|
|
e2292dfa34 | ||
|
|
17111abc72 | ||
|
|
c4bc2162f2 | ||
|
|
bfd966217f | ||
|
|
797c02e893 | ||
|
|
65372e547a | ||
|
|
2454b2e0db | ||
|
|
0505a46603 | ||
|
|
7f77cd6a22 | ||
|
|
efe7b3099f | ||
|
|
26a831b49f | ||
|
|
a3a5bb8177 | ||
|
|
4282f3eb6d | ||
|
|
8a49db650a | ||
|
|
fadd4ac61e | ||
|
|
d0c14895d0 | ||
|
|
32ee18240b | ||
|
|
cd10aa90cc | ||
|
|
33d28c4359 | ||
|
|
530403ec04 | ||
|
|
f15072bc8d | ||
|
|
8c2db972cf | ||
|
|
ff8f9ca81a | ||
|
|
40991cc8e9 | ||
|
|
2f551ee3f2 | ||
|
|
f1ab0a05f1 | ||
|
|
fb919466de | ||
|
|
4a4cf08cd8 | ||
|
|
ed20c24326 | ||
|
|
e01cbcec62 | ||
|
|
04d6ccc30e | ||
|
|
6860f96973 | ||
|
|
944c8530d8 | ||
|
|
8b1552952c | ||
|
|
dfcadde076 | ||
|
|
55acd0f048 | ||
|
|
820c016aad | ||
|
|
d1d63d83dc | ||
|
|
7da5b2cdef | ||
|
|
442dde5c40 | ||
|
|
f038e81ff3 | ||
|
|
32c4fcb065 | ||
|
|
e2703b459f | ||
|
|
405d99fbe2 | ||
|
|
7b89687206 | ||
|
|
d74f1a386c | ||
|
|
b041ed1510 | ||
|
|
3426556a51 | ||
|
|
e2cb535f3f | ||
|
|
3b17a41415 | ||
|
|
631720f111 | ||
|
|
ab031d3dad | ||
|
|
6101048f07 | ||
|
|
115f7063d5 | ||
|
|
159d67ec59 | ||
|
|
e09ce4c820 | ||
|
|
8cfc013960 | ||
|
|
a436c46cb2 | ||
|
|
893be51810 | ||
|
|
97b5a49e36 | ||
|
|
043f06e188 | ||
|
|
fa13b464f8 | ||
|
|
bfaaf20fd9 | ||
|
|
2f97b80b9c | ||
|
|
eee9c967fa | ||
|
|
515981c044 | ||
|
|
a06528e5e1 | ||
|
|
98194c854a | ||
|
|
2d60a88a75 | ||
|
|
c3e7d6c74c | ||
|
|
8da66bc789 | ||
|
|
9ceb5b2e85 | ||
|
|
17b8e086c9 | ||
|
|
9a097d113d | ||
|
|
46ca1e16bb | ||
|
|
d4d3124a90 | ||
|
|
35a9fddbb2 | ||
|
|
41e417ff0b | ||
|
|
f6614c1174 | ||
|
|
9136bcf5e8 | ||
|
|
7c156d10d6 | ||
|
|
3372d942ec | ||
|
|
7fc9876b1e | ||
|
|
cff62e9528 | ||
|
|
24f59b0a17 | ||
|
|
0a07800eba | ||
|
|
c863e9ed4d | ||
|
|
523537cf05 | ||
|
|
fd4543ffe0 | ||
|
|
83b0309f23 | ||
|
|
5cabe5ecfa | ||
|
|
fae3004512 | ||
|
|
e143c47c25 | ||
|
|
27c3fca324 | ||
|
|
26d637cafc | ||
|
|
03e8fe9f27 | ||
|
|
23939aeee3 | ||
|
|
d7b793ce4c | ||
|
|
d3096c56cb | ||
|
|
b0e7b49056 | ||
|
|
2f0f26c328 | ||
|
|
7f2f5a182f | ||
|
|
33b88796e8 | ||
|
|
31e4db2186 | ||
|
|
76ad7866ec | ||
|
|
e9804eccbb | ||
|
|
d62d0efb1d | ||
|
|
3ec9cd1244 | ||
|
|
bd5f48f193 | ||
|
|
98fc3e5b0b | ||
|
|
d06c633dc4 | ||
|
|
d401386e2d | ||
|
|
8f0f9e64b9 | ||
|
|
bd5ac85ac0 | ||
|
|
417070e957 | ||
|
|
a92638e897 | ||
|
|
08abe890ff | ||
|
|
66b6420f21 | ||
|
|
b21bd5a01e | ||
|
|
d11e8ec04b | ||
|
|
3e5fe0f1cb | ||
|
|
b65d62e065 | ||
|
|
fc55be0916 | ||
|
|
a87aa0fbe2 | ||
|
|
a44a105cbc | ||
|
|
c4817ac017 | ||
|
|
65835606cc | ||
|
|
ff26922afb | ||
|
|
a894ba7a51 | ||
|
|
cb63fe600c | ||
|
|
2d6146351d | ||
|
|
e5953b25e1 | ||
|
|
6354cb194c | ||
|
|
8d6beb92cb | ||
|
|
bb5edccf34 | ||
|
|
6d86b25acd | ||
|
|
04677d21bb | ||
|
|
20022b88fc | ||
|
|
3088d7f182 | ||
|
|
ce8dafd33d | ||
|
|
6054285ddb | ||
|
|
dabea5169b | ||
|
|
7fb5ceeda4 | ||
|
|
dc6fd8be7f | ||
|
|
c271b9c2de | ||
|
|
6fb6092a6b | ||
|
|
46bb64ad24 | ||
|
|
bcd16ce296 | ||
|
|
07ec14d5c4 | ||
|
|
d716416d1d | ||
|
|
343871ed8b | ||
|
|
aa60247e42 | ||
|
|
283e3e99a5 | ||
|
|
ca79bdb16b | ||
|
|
a75d2cfa34 | ||
|
|
1746f37276 | ||
|
|
73f32868a2 | ||
|
|
dabd131222 | ||
|
|
612c6db6db | ||
|
|
c56ef33833 | ||
|
|
2253e25ae1 | ||
|
|
adb24d480a | ||
|
|
9fb1dcf28f | ||
|
|
bba36a5724 | ||
|
|
fa515be258 | ||
|
|
be241524db | ||
|
|
bb66c3fa68 | ||
|
|
a32d5bef20 | ||
|
|
d409278dd5 | ||
|
|
3328e43a40 | ||
|
|
678e832058 | ||
|
|
2c341f450f | ||
|
|
5854074d4a | ||
|
|
dbe186248d | ||
|
|
0504161b04 | ||
|
|
9748f1cff8 | ||
|
|
6a061ed52c | ||
|
|
102d58502a | ||
|
|
477698f917 | ||
|
|
d865b5d7b5 | ||
|
|
eeaf6df925 | ||
|
|
95abca4e03 | ||
|
|
c5906b6f3a | ||
|
|
91c581b475 | ||
|
|
47760867d5 | ||
|
|
5612a5d9e4 | ||
|
|
4c462bd75a | ||
|
|
092b30f64f | ||
|
|
b34ca8ca2f | ||
|
|
e2c54eef77 | ||
|
|
2a2c27edef | ||
|
|
ec92d5ddb7 | ||
|
|
a9f208153c | ||
|
|
d9ffd67f36 | ||
|
|
19861ef0d1 | ||
|
|
469879c211 | ||
|
|
157198fd17 | ||
|
|
0d61b8db38 | ||
|
|
593334456a | ||
|
|
98b9cc23e4 | ||
|
|
3e42c044b8 | ||
|
|
11c3ea769e | ||
|
|
b8bb2e234b | ||
|
|
87b00bb156 | ||
|
|
3da2fc4d9b | ||
|
|
972ab9b368 | ||
|
|
c359b0134a | ||
|
|
2cd7462573 | ||
|
|
2a7d515932 | ||
|
|
1bb04035ab | ||
|
|
267efb0763 | ||
|
|
0ef215dfc5 | ||
|
|
50bea8140f | ||
|
|
086e3ed4ec | ||
|
|
b02539684a | ||
|
|
b9747607ad | ||
|
|
284a6ae667 | ||
|
|
116e711f1a | ||
|
|
7aeb641036 | ||
|
|
e4f69c0b6f | ||
|
|
48d7228ae7 | ||
|
|
ae28df901f | ||
|
|
f17f45f277 | ||
|
|
2b15fc2966 | ||
|
|
76a9342afa | ||
|
|
14849d6e45 | ||
|
|
77ea2deada | ||
|
|
390b7ff834 | ||
|
|
0e4187b062 | ||
|
|
4098f77e08 | ||
|
|
a60eed35fe | ||
|
|
904215fe38 | ||
|
|
6376642d38 | ||
|
|
6b36a446f0 | ||
|
|
95ee7f5c00 | ||
|
|
b109effc94 | ||
|
|
44efda8318 | ||
|
|
e6e1b6d746 | ||
|
|
e303cbcc22 | ||
|
|
c67aed5b65 | ||
|
|
0278882c30 | ||
|
|
42ccd638bd | ||
|
|
e11577bd23 | ||
|
|
3c0b87bbca | ||
|
|
84a61b01ca | ||
|
|
c149e008fd | ||
|
|
c7a345eb0b | ||
|
|
348b6e9742 | ||
|
|
003b3e02e4 | ||
|
|
5b668d7931 | ||
|
|
87748fa80c | ||
|
|
ad0482fb5b | ||
|
|
12ceb1cb32 | ||
|
|
9ec2c5da52 | ||
|
|
f742d34588 | ||
|
|
4d8e058d33 | ||
|
|
77ba6e0f7b | ||
|
|
76a0e5c851 | ||
|
|
f3096cc24c | ||
|
|
49957e1d95 | ||
|
|
2f5cb5f090 | ||
|
|
ba394e1021 | ||
|
|
7611c64493 | ||
|
|
231248d20a | ||
|
|
a3a79fc58d | ||
|
|
6476e585c4 | ||
|
|
fd2961710d | ||
|
|
7f4ab67f98 | ||
|
|
b9ce38b85b | ||
|
|
50d5658add | ||
|
|
72777bc6cd | ||
|
|
f2046c3c05 | ||
|
|
745dfc3fbb | ||
|
|
cb77165b53 | ||
|
|
bde4700e87 | ||
|
|
e58cea9a26 | ||
|
|
556dc0d1ec | ||
|
|
8c1ddcf1c0 | ||
|
|
2549c1f97d | ||
|
|
5faa497821 | ||
|
|
d7a7e72c3a | ||
|
|
af1701e6fa | ||
|
|
32d1cc7d54 | ||
|
|
783a615c07 | ||
|
|
65bfee6eba | ||
|
|
8d4419705b | ||
|
|
6c3baf229c | ||
|
|
6e9a6283fc | ||
|
|
5b3899237b | ||
|
|
dddf830e47 | ||
|
|
fd930d0b1d | ||
|
|
2b5d65ae04 | ||
|
|
2ebaa04c2f | ||
|
|
1e316ea19f | ||
|
|
ac9257ec1c | ||
|
|
9b83c5e283 | ||
|
|
a7a4972013 | ||
|
|
f6f4e6fde7 | ||
|
|
e9160c2449 | ||
|
|
b0b1029d0f | ||
|
|
72b3a0555d | ||
|
|
135fde68c1 | ||
|
|
954e45ed97 | ||
|
|
2b3f16d3ad | ||
|
|
6820b84921 | ||
|
|
6a5f5f4ffa | ||
|
|
19381342b3 | ||
|
|
c2627dda8d | ||
|
|
db309b7930 | ||
|
|
403958fed3 | ||
|
|
3af53f2089 | ||
|
|
e472760d92 | ||
|
|
b04acd8ae0 | ||
|
|
6890973ce8 | ||
|
|
b3d9a85fa2 | ||
|
|
9f027ed584 | ||
|
|
8fb598e60a | ||
|
|
2edaba39a0 | ||
|
|
b0be7effe8 | ||
|
|
142979ce93 | ||
|
|
093dd7c62c | ||
|
|
4acafc3d77 | ||
|
|
65bf0aad79 | ||
|
|
ef6e846512 | ||
|
|
782464f664 | ||
|
|
c7352f62e5 | ||
|
|
90dd6b7cb3 | ||
|
|
ddfb4bf0a5 | ||
|
|
cdef21d6c0 | ||
|
|
c0f843061e | ||
|
|
5774771ea6 | ||
|
|
ab8d5474e0 | ||
|
|
6497ec8098 | ||
|
|
83c3b16b92 | ||
|
|
3c2bd032ba | ||
|
|
f798866619 | ||
|
|
ffad2c7386 | ||
|
|
7252e54593 | ||
|
|
cfab4dc658 | ||
|
|
7f5a8ce6bb | ||
|
|
d02a597451 | ||
|
|
8d92a1f195 | ||
|
|
74e630aacb | ||
|
|
65a12767f9 | ||
|
|
ab9d813636 | ||
|
|
007975e7da | ||
|
|
86ca1ebda0 | ||
|
|
57fb3e6377 | ||
|
|
7e6fcb80a3 | ||
|
|
5e46e1e3d9 | ||
|
|
21e370de9b | ||
|
|
77ef877c59 | ||
|
|
77caedb3bb | ||
|
|
3e77975c17 | ||
|
|
75e15c81e1 | ||
|
|
782a1ce301 | ||
|
|
be21b9e163 | ||
|
|
284140871e | ||
|
|
e6ac40a07c | ||
|
|
b8e98350c1 | ||
|
|
445ff263c6 | ||
|
|
e10c40d2b8 | ||
|
|
a41a2b3e64 | ||
|
|
e603391c35 | ||
|
|
7e0cd99f48 | ||
|
|
daedb8261d | ||
|
|
2c8744a319 | ||
|
|
a7c441225b | ||
|
|
e3c491860a | ||
|
|
43ad0b2294 | ||
|
|
bf897d10d2 | ||
|
|
0b1a93d3e6 | ||
|
|
7edef20f4f | ||
|
|
945c308cf5 | ||
|
|
f91494f813 | ||
|
|
9d28caac00 | ||
|
|
798f3a7b0e | ||
|
|
768e170ed4 | ||
|
|
a0ebb891de | ||
|
|
570b39f82e | ||
|
|
dc50899fe0 | ||
|
|
0f889e0259 | ||
|
|
cb906edd11 | ||
|
|
604f6709db | ||
|
|
0359f27cd9 | ||
|
|
0ca438ed25 | ||
|
|
6b6e9e92e8 | ||
|
|
b5e0991f5e | ||
|
|
f06f0e7ae0 | ||
|
|
0fcbb5ffda | ||
|
|
b1f7dbefd8 | ||
|
|
8fc2d5be37 | ||
|
|
40020728de | ||
|
|
4abb169568 | ||
|
|
da1ac5358f | ||
|
|
d504bd593a | ||
|
|
63e48efdfe | ||
|
|
8bb27b60fa | ||
|
|
437c1e2f21 | ||
|
|
7f4a12c179 | ||
|
|
19d3bbc70a | ||
|
|
559561bf72 | ||
|
|
c8c0589ac4 | ||
|
|
666218773c | ||
|
|
d3049a3433 | ||
|
|
130d5a8945 | ||
|
|
172751cd42 | ||
|
|
3ad7c96a3c | ||
|
|
6d41d1f6d2 | ||
|
|
cb74833dc2 | ||
|
|
8c7b6293fb | ||
|
|
9d1f46da9f | ||
|
|
216059b659 | ||
|
|
18392ed0a4 | ||
|
|
63a4d20ea9 | ||
|
|
057231b9c3 | ||
|
|
749bbf428d | ||
|
|
b0458f10a3 | ||
|
|
5b91c927b6 | ||
|
|
b45740884b | ||
|
|
87ad4be117 | ||
|
|
78de70881f | ||
|
|
e7a370a549 | ||
|
|
54eb579558 | ||
|
|
732b67d8cb | ||
|
|
eed45b57a1 | ||
|
|
3503c60fd1 | ||
|
|
c17ba30cfc | ||
|
|
5167c7235d | ||
|
|
803f94012a | ||
|
|
9281bcdd7d | ||
|
|
4dca554967 | ||
|
|
7c45fb6c17 | ||
|
|
8aa283488f | ||
|
|
604c65c7fb | ||
|
|
711148423d | ||
|
|
1f82ceecc6 | ||
|
|
1ac8701ada | ||
|
|
d61e33fdf3 | ||
|
|
e552b5160f | ||
|
|
7e063e8ad8 | ||
|
|
88a34936cd | ||
|
|
c1181478dd | ||
|
|
d13d8628b5 | ||
|
|
6048208c8c | ||
|
|
78214fb39b | ||
|
|
ff8d7fa6c2 | ||
|
|
3a2e8b9b19 | ||
|
|
bca4289c96 | ||
|
|
3fbd9baf0c | ||
|
|
e12c96f4b2 | ||
|
|
eec26aa481 | ||
|
|
865aeda6f2 | ||
|
|
2c4ebedda4 | ||
|
|
042bc8d79a | ||
|
|
4c7bd80f72 | ||
|
|
3a8591fdfb | ||
|
|
629aaa2093 | ||
|
|
5b5b118b7a | ||
|
|
c7016aa462 | ||
|
|
cf857e109a | ||
|
|
1c79840684 | ||
|
|
4ba7de9519 | ||
|
|
2eb8df347e | ||
|
|
9056371c41 | ||
|
|
1f57e1f366 | ||
|
|
aeb568bcf4 | ||
|
|
b7afe4411e | ||
|
|
cba784b8ec | ||
|
|
3aba15e88d | ||
|
|
fa384e93dc | ||
|
|
1f3e04da29 | ||
|
|
a484d48377 | ||
|
|
15f51ea26e | ||
|
|
80bfa103ab | ||
|
|
66f93e0d32 | ||
|
|
366780f6cb | ||
|
|
fb4c1fc268 | ||
|
|
e3bb7ccbd3 | ||
|
|
e3fb8a2137 |
23
.github/stale.yml
vendored
@@ -0,0 +1,23 @@
|
||||
# Number of days of inactivity before an Issue or Pull Request becomes stale
|
||||
daysUntilStale: 60
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
|
||||
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
|
||||
daysUntilClose: 7
|
||||
|
||||
issues:
|
||||
exemptLabels:
|
||||
- acknowledged
|
||||
|
||||
# Comment to post when marking as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
|
||||
# Comment to post when closing a stale Issue or Pull Request.
|
||||
closeComment: >
|
||||
This issue has been closed due to inactivity.
|
||||
|
||||
# Limit the number of actions per hour, from 1-30. Default is 30
|
||||
limitPerRun: 1
|
||||
|
||||
2
.github/workflows/android.yml
vendored
@@ -4,7 +4,7 @@ on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'main'
|
||||
- '4.**'
|
||||
- '5.**'
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ try:
|
||||
print("\nTo include this in the distribution, copy it to the project's assets/databases/ directory.")
|
||||
print("If you support API 10 or lower, you must use the gzipped version to avoid corruption.")
|
||||
|
||||
except sqlite3.Error, e:
|
||||
except sqlite3.Error as e:
|
||||
if connection:
|
||||
connection.rollback()
|
||||
print("Error: %s" % e.args[0])
|
||||
|
||||
@@ -63,8 +63,8 @@ ktlint {
|
||||
version = "0.43.2"
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 1019
|
||||
def canonicalVersionName = "5.33.0"
|
||||
def canonicalVersionCode = 1048
|
||||
def canonicalVersionName = "5.37.3"
|
||||
|
||||
def postFixSize = 100
|
||||
def abiPostFix = ['universal' : 0,
|
||||
@@ -455,8 +455,9 @@ dependencies {
|
||||
implementation project(':device-transfer')
|
||||
implementation project(':image-editor')
|
||||
implementation project(':donations')
|
||||
implementation project(':contacts')
|
||||
|
||||
implementation libs.signal.client.android
|
||||
implementation libs.libsignal.android
|
||||
implementation libs.google.protobuf.javalite
|
||||
|
||||
implementation(libs.mobilecoin) {
|
||||
@@ -524,10 +525,7 @@ dependencies {
|
||||
testImplementation testLibs.junit.junit
|
||||
testImplementation testLibs.assertj.core
|
||||
testImplementation testLibs.mockito.core
|
||||
testImplementation testLibs.powermock.api.mockito
|
||||
testImplementation testLibs.powermock.module.junit4.core
|
||||
testImplementation testLibs.powermock.module.junit4.rule
|
||||
testImplementation testLibs.powermock.classloading.xstream
|
||||
testImplementation testLibs.mockito.kotlin
|
||||
|
||||
testImplementation testLibs.androidx.test.core
|
||||
testImplementation (testLibs.robolectric.robolectric) {
|
||||
@@ -554,7 +552,7 @@ dependencies {
|
||||
implementation libs.rxjava3.rxkotlin
|
||||
implementation libs.rxdogtag
|
||||
|
||||
androidTestUtil 'androidx.test:orchestrator:1.4.0'
|
||||
androidTestUtil 'androidx.test:orchestrator:1.4.1'
|
||||
}
|
||||
|
||||
def getLastCommitTimestamp() {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
-dontobfuscate
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
-keep class org.whispersystems.** { *; }
|
||||
-keep class org.signal.libsignal.protocol.** { *; }
|
||||
-keep class org.thoughtcrime.securesms.** { *; }
|
||||
-keepclassmembers class ** {
|
||||
public void onEvent*(**);
|
||||
|
||||
@@ -8,7 +8,6 @@ import org.thoughtcrime.securesms.database.model.DistributionListRecord
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import java.lang.IllegalStateException
|
||||
import java.util.UUID
|
||||
|
||||
class DistributionListDatabaseTest {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
class DonationReceiptDatabaseTest {
|
||||
|
||||
private val records = listOf(
|
||||
DonationReceiptRecord.createForBoost(FiatMoney(BigDecimal.valueOf(100), Currency.getInstance("USD"))),
|
||||
DonationReceiptRecord.createForBoost(FiatMoney(BigDecimal.valueOf(200), Currency.getInstance("USD")))
|
||||
)
|
||||
|
||||
@Test
|
||||
fun givenNoReceipts_whenICheckHasReceipts_thenIExpectFalse() {
|
||||
assertFalse(SignalDatabase.donationReceipts.hasReceipts())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenOneReceipt_whenICheckHasReceipts_thenIExpectTrue() {
|
||||
SignalDatabase.donationReceipts.addReceipt(records.first())
|
||||
assertTrue(SignalDatabase.donationReceipts.hasReceipts())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenMultipleReceipts_whenICheckHasReceipts_thenIExpectTrue() {
|
||||
records.forEach {
|
||||
SignalDatabase.donationReceipts.addReceipt(it)
|
||||
}
|
||||
|
||||
assertTrue(SignalDatabase.donationReceipts.hasReceipts())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MmsDatabaseTest_stories {
|
||||
|
||||
private lateinit var mms: MmsDatabase
|
||||
|
||||
private val localAci = ACI.from(UUID.randomUUID())
|
||||
private val localPni = PNI.from(UUID.randomUUID())
|
||||
|
||||
private lateinit var myStory: Recipient
|
||||
private lateinit var recipients: List<RecipientId>
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
mms = SignalDatabase.mms
|
||||
|
||||
mms.deleteAllThreads()
|
||||
|
||||
SignalStore.account().setAci(localAci)
|
||||
SignalStore.account().setPni(localPni)
|
||||
|
||||
myStory = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(DistributionListId.MY_STORY))
|
||||
recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoStories_whenIGetOrderedStoryRecipientsAndIds_thenIExpectAnEmptyList() {
|
||||
// WHEN
|
||||
val result = mms.orderedStoryRecipientsAndIds
|
||||
|
||||
// THEN
|
||||
assertEquals(0, result.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenOneOutgoingAndOneIncomingStory_whenIGetOrderedStoryRecipientsAndIds_thenIExpectIncomingThenOutgoing() {
|
||||
// GIVEN
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(myStory)
|
||||
val sender = recipients[0]
|
||||
|
||||
MmsHelper.insert(
|
||||
recipient = myStory,
|
||||
sentTimeMillis = 1,
|
||||
storyType = StoryType.STORY_WITH_REPLIES,
|
||||
threadId = threadId
|
||||
)
|
||||
|
||||
MmsHelper.insert(
|
||||
IncomingMediaMessage(
|
||||
from = sender,
|
||||
sentTimeMillis = 2,
|
||||
serverTimeMillis = 2,
|
||||
receivedTimeMillis = 2,
|
||||
storyType = StoryType.STORY_WITH_REPLIES
|
||||
),
|
||||
-1L
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = mms.orderedStoryRecipientsAndIds
|
||||
|
||||
// THEN
|
||||
assertEquals(listOf(sender.toLong(), myStory.id.toLong()), result.map { it.recipientId.toLong() })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAStory_whenISetIncomingStoryMessageViewed_thenIExpectASetReceiptTimestamp() {
|
||||
// GIVEN
|
||||
val sender = recipients[0]
|
||||
val messageId = MmsHelper.insert(
|
||||
IncomingMediaMessage(
|
||||
from = sender,
|
||||
sentTimeMillis = 2,
|
||||
serverTimeMillis = 2,
|
||||
receivedTimeMillis = 2,
|
||||
storyType = StoryType.STORY_WITH_REPLIES
|
||||
),
|
||||
-1L
|
||||
).get().messageId
|
||||
|
||||
val messageBeforeMark = SignalDatabase.mms.getMessageRecord(messageId)
|
||||
assertFalse(messageBeforeMark.incomingStoryViewedAtTimestamp > 0)
|
||||
|
||||
// WHEN
|
||||
SignalDatabase.mms.setIncomingMessageViewed(messageId)
|
||||
|
||||
// THEN
|
||||
val messageAfterMark = SignalDatabase.mms.getMessageRecord(messageId)
|
||||
assertTrue(messageAfterMark.incomingStoryViewedAtTimestamp > 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun given5ViewedStories_whenIGetOrderedStoryRecipientsAndIds_thenIExpectLatestViewedFirst() {
|
||||
// GIVEN
|
||||
val messageIds = recipients.take(5).map {
|
||||
MmsHelper.insert(
|
||||
IncomingMediaMessage(
|
||||
from = it,
|
||||
sentTimeMillis = 2,
|
||||
serverTimeMillis = 2,
|
||||
receivedTimeMillis = 2,
|
||||
storyType = StoryType.STORY_WITH_REPLIES,
|
||||
),
|
||||
-1L
|
||||
).get().messageId
|
||||
}
|
||||
|
||||
val randomizedOrderedIds = messageIds.shuffled()
|
||||
randomizedOrderedIds.forEach {
|
||||
SignalDatabase.mms.setIncomingMessageViewed(it)
|
||||
Thread.sleep(5)
|
||||
}
|
||||
|
||||
// WHEN
|
||||
val result = SignalDatabase.mms.orderedStoryRecipientsAndIds
|
||||
val resultOrderedIds = result.map { it.messageId }
|
||||
|
||||
// THEN
|
||||
assertEquals(randomizedOrderedIds.reversed(), resultOrderedIds)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun given15Stories_whenIGetOrderedStoryRecipientsAndIds_thenIExpectUnviewedThenInterspersedViewedAndSelfSendsAllDescending() {
|
||||
val myStoryThread = SignalDatabase.threads.getOrCreateThreadIdFor(myStory)
|
||||
|
||||
val unviewedIds: List<Long> = (0 until 5).map {
|
||||
Thread.sleep(5)
|
||||
MmsHelper.insert(
|
||||
IncomingMediaMessage(
|
||||
from = recipients[it],
|
||||
sentTimeMillis = System.currentTimeMillis(),
|
||||
serverTimeMillis = 2,
|
||||
receivedTimeMillis = 2,
|
||||
storyType = StoryType.STORY_WITH_REPLIES,
|
||||
),
|
||||
-1L
|
||||
).get().messageId
|
||||
}
|
||||
|
||||
val viewedIds: List<Long> = (0 until 5).map {
|
||||
Thread.sleep(5)
|
||||
MmsHelper.insert(
|
||||
IncomingMediaMessage(
|
||||
from = recipients[it],
|
||||
sentTimeMillis = System.currentTimeMillis(),
|
||||
serverTimeMillis = 2,
|
||||
receivedTimeMillis = 2,
|
||||
storyType = StoryType.STORY_WITH_REPLIES,
|
||||
),
|
||||
-1L
|
||||
).get().messageId
|
||||
}
|
||||
|
||||
val interspersedIds: List<Long> = (0 until 10).map {
|
||||
Thread.sleep(5)
|
||||
if (it % 2 == 0) {
|
||||
SignalDatabase.mms.setIncomingMessageViewed(viewedIds[it / 2])
|
||||
viewedIds[it / 2]
|
||||
} else {
|
||||
MmsHelper.insert(
|
||||
recipient = myStory,
|
||||
sentTimeMillis = System.currentTimeMillis(),
|
||||
storyType = StoryType.STORY_WITH_REPLIES,
|
||||
threadId = myStoryThread
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val result = SignalDatabase.mms.orderedStoryRecipientsAndIds
|
||||
val resultOrderedIds = result.map { it.messageId }
|
||||
|
||||
assertEquals(unviewedIds.reversed() + interspersedIds.reversed(), resultOrderedIds)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import java.util.Optional
|
||||
|
||||
/**
|
||||
* Helper methods for inserting an MMS message into the MMS table.
|
||||
*/
|
||||
object MmsHelper {
|
||||
|
||||
fun insert(
|
||||
recipient: Recipient = Recipient.UNKNOWN,
|
||||
body: String = "body",
|
||||
sentTimeMillis: Long = System.currentTimeMillis(),
|
||||
subscriptionId: Int = -1,
|
||||
expiresIn: Long = 0,
|
||||
viewOnce: Boolean = false,
|
||||
distributionType: Int = ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
threadId: Long = 1,
|
||||
storyType: StoryType = StoryType.NONE
|
||||
): Long {
|
||||
val message = OutgoingMediaMessage(
|
||||
recipient,
|
||||
body,
|
||||
emptyList(),
|
||||
sentTimeMillis,
|
||||
subscriptionId,
|
||||
expiresIn,
|
||||
viewOnce,
|
||||
distributionType,
|
||||
storyType,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
emptySet(),
|
||||
emptySet(),
|
||||
null
|
||||
)
|
||||
|
||||
return insert(
|
||||
message = message,
|
||||
threadId = threadId
|
||||
)
|
||||
}
|
||||
|
||||
fun insert(
|
||||
message: OutgoingMediaMessage,
|
||||
threadId: Long
|
||||
): Long {
|
||||
return SignalDatabase.mms.insertMessageOutbox(message, threadId, false, GroupReceiptDatabase.STATUS_UNKNOWN, null)
|
||||
}
|
||||
|
||||
fun insert(
|
||||
message: IncomingMediaMessage,
|
||||
threadId: Long
|
||||
): Optional<MessageDatabase.InsertResult> {
|
||||
return SignalDatabase.mms.insertSecureDecryptedMessageInbox(message, threadId)
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,9 @@ import org.thoughtcrime.securesms.keyvalue.MockKeyValuePersistentStorage
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@@ -523,7 +522,7 @@ class RecipientDatabaseTest {
|
||||
}
|
||||
|
||||
private fun ensureDbEmpty() {
|
||||
SignalDatabase.rawDatabase.rawQuery("SELECT COUNT(*) FROM ${RecipientDatabase.TABLE_NAME}", null).use { cursor ->
|
||||
SignalDatabase.rawDatabase.rawQuery("SELECT COUNT(*) FROM ${RecipientDatabase.TABLE_NAME} WHERE ${RecipientDatabase.DISTRIBUTION_LIST_ID} IS NULL ", null).use { cursor ->
|
||||
assertTrue(cursor.moveToFirst())
|
||||
assertEquals(0, cursor.getLong(0))
|
||||
}
|
||||
|
||||
@@ -9,13 +9,16 @@ import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.CursorUtil
|
||||
import org.signal.libsignal.protocol.IdentityKey
|
||||
import org.signal.libsignal.protocol.SignalProtocolAddress
|
||||
import org.signal.libsignal.protocol.state.SessionRecord
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember
|
||||
import org.signal.zkgroup.groups.GroupMasterKey
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListRecord
|
||||
@@ -30,14 +33,10 @@ import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage
|
||||
import org.thoughtcrime.securesms.util.CursorUtil
|
||||
import org.whispersystems.libsignal.IdentityKey
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress
|
||||
import org.whispersystems.libsignal.state.SessionRecord
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@@ -76,8 +75,6 @@ class RecipientDatabaseTest_merges {
|
||||
|
||||
SignalStore.account().setAci(localAci)
|
||||
SignalStore.account().setPni(localPni)
|
||||
|
||||
ensureDbEmpty()
|
||||
}
|
||||
|
||||
/** High trust lets you merge two different users into one. You should prefer the ACI user. Not shown: merging threads, dropping e164 sessions, etc. */
|
||||
@@ -217,19 +214,12 @@ class RecipientDatabaseTest_merges {
|
||||
private val context: Application
|
||||
get() = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application
|
||||
|
||||
private fun ensureDbEmpty() {
|
||||
SignalDatabase.rawDatabase.rawQuery("SELECT COUNT(*) FROM ${RecipientDatabase.TABLE_NAME}", null).use { cursor ->
|
||||
assertTrue(cursor.moveToFirst())
|
||||
assertEquals(0, cursor.getLong(0))
|
||||
}
|
||||
}
|
||||
|
||||
private fun smsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.absent()): IncomingTextMessage {
|
||||
private fun smsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.empty()): IncomingTextMessage {
|
||||
return IncomingTextMessage(sender, 1, time, time, time, body, groupId, 0, true, null)
|
||||
}
|
||||
|
||||
private fun mmsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.absent()): IncomingMediaMessage {
|
||||
return IncomingMediaMessage(sender, groupId, body, time, time, time, emptyList(), 0, 0, false, false, true, Optional.absent())
|
||||
private fun mmsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.empty()): IncomingMediaMessage {
|
||||
return IncomingMediaMessage(sender, groupId, body, time, time, time, emptyList(), 0, 0, false, false, true, Optional.empty())
|
||||
}
|
||||
|
||||
private fun identityKey(value: Byte): IdentityKey {
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.Matchers.`is`
|
||||
import org.hamcrest.Matchers.notNullValue
|
||||
import org.hamcrest.Matchers.nullValue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.addMember
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.addRequestingMember
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.deleteRequestingMember
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.groupChange
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.groupContext
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.sms.IncomingGroupUpdateMessage
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName", "TestFunctionName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
|
||||
|
||||
private lateinit var recipients: RecipientDatabase
|
||||
private lateinit var sms: SmsDatabase
|
||||
|
||||
private val localAci = ACI.from(UUID.randomUUID())
|
||||
private val localPni = PNI.from(UUID.randomUUID())
|
||||
|
||||
private var wallClock: Long = 1000
|
||||
|
||||
private lateinit var alice: RecipientId
|
||||
private lateinit var bob: RecipientId
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
recipients = SignalDatabase.recipients
|
||||
sms = SignalDatabase.sms
|
||||
|
||||
SignalStore.account().setAci(localAci)
|
||||
SignalStore.account().setPni(localPni)
|
||||
|
||||
alice = recipients.getOrInsertFromServiceId(aliceServiceId)
|
||||
bob = recipients.getOrInsertFromServiceId(bobServiceId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Do nothing if no previous messages.
|
||||
*/
|
||||
@Test
|
||||
fun noPreviousMessage() {
|
||||
val result = sms.collapseJoinRequestEventsIfPossible(
|
||||
1,
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat("result is null when not collapsing", result.orElse(null), nullValue())
|
||||
}
|
||||
|
||||
/**
|
||||
* Do nothing if previous message is text.
|
||||
*/
|
||||
@Test
|
||||
fun previousTextMesssage() {
|
||||
val threadId = sms.insertMessageInbox(smsMessage(sender = alice, body = "What up")).get().threadId
|
||||
|
||||
val result = sms.collapseJoinRequestEventsIfPossible(
|
||||
threadId,
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat("result is null when not collapsing", result.orElse(null), nullValue())
|
||||
}
|
||||
|
||||
/**
|
||||
* Do nothing if previous is unrelated group change.
|
||||
*/
|
||||
@Test
|
||||
fun previousUnrelatedGroupChange() {
|
||||
val threadId = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
addMember(bobServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
).get().threadId
|
||||
|
||||
val result = sms.collapseJoinRequestEventsIfPossible(
|
||||
threadId,
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat("result is null when not collapsing", result.orElse(null), nullValue())
|
||||
}
|
||||
|
||||
/**
|
||||
* Do nothing if previous join request is from a different recipient.
|
||||
*/
|
||||
@Test
|
||||
fun previousJoinRequestFromADifferentRecipient() {
|
||||
val threadId = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = bob,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = bobServiceId) {
|
||||
deleteRequestingMember(bobServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
).get().threadId
|
||||
|
||||
val result = sms.collapseJoinRequestEventsIfPossible(
|
||||
threadId,
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat("result is null when not collapsing", result.orElse(null), nullValue())
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse if previous is join request from same.
|
||||
*/
|
||||
@Test
|
||||
fun previousJoinRequestCollapse() {
|
||||
val latestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
addRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
).get()
|
||||
|
||||
val result = sms.collapseJoinRequestEventsIfPossible(
|
||||
latestMessage.threadId,
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat("result is not null when collapsing", result.orElse(null), notNullValue())
|
||||
assertThat("result message id should be same as latest message", result.get().messageId, `is`(latestMessage.messageId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse if previous is join request from same, and leave second previous alone if text.
|
||||
*/
|
||||
@Test
|
||||
fun previousJoinThenTextCollapse() {
|
||||
val secondLatestMessage = sms.insertMessageInbox(smsMessage(sender = alice, body = "What up")).get()
|
||||
|
||||
val latestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
addRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
).get()
|
||||
|
||||
assert(secondLatestMessage.threadId == latestMessage.threadId)
|
||||
|
||||
val result = sms.collapseJoinRequestEventsIfPossible(
|
||||
latestMessage.threadId,
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat("result is not null when collapsing", result.orElse(null), notNullValue())
|
||||
assertThat("result message id should be same as latest message", result.get().messageId, `is`(latestMessage.messageId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse "twice" is previous is a join request and second previous is already collapsed join/delete from the same recipient.
|
||||
*/
|
||||
@Test
|
||||
fun previousCollapseAndJoinRequestDoubleCollapse() {
|
||||
val secondLatestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
addRequestingMember(aliceServiceId)
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
).get()
|
||||
|
||||
val latestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
addRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
).get()
|
||||
|
||||
assert(secondLatestMessage.threadId == latestMessage.threadId)
|
||||
|
||||
val result = sms.collapseJoinRequestEventsIfPossible(
|
||||
latestMessage.threadId,
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat("result is not null when collapsing", result.orElse(null), notNullValue())
|
||||
assertThat("result message id should be same as second latest message", result.get().messageId, `is`(secondLatestMessage.messageId))
|
||||
assertThat("latest message should be deleted", sms.getMessageRecordOrNull(latestMessage.messageId), nullValue())
|
||||
}
|
||||
|
||||
private fun smsMessage(sender: RecipientId, body: String? = ""): IncomingTextMessage {
|
||||
wallClock++
|
||||
return IncomingTextMessage(sender, 1, wallClock, wallClock, wallClock, body, Optional.of(groupId), 0, true, null)
|
||||
}
|
||||
|
||||
private fun groupUpdateMessage(sender: RecipientId, groupContext: DecryptedGroupV2Context): IncomingGroupUpdateMessage {
|
||||
return IncomingGroupUpdateMessage(smsMessage(sender, null), groupContext)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val aliceServiceId: ServiceId = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
|
||||
private val bobServiceId: ServiceId = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
|
||||
|
||||
private val masterKey = GroupMasterKey(Hex.fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
|
||||
private val groupId = GroupId.v2(masterKey)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.Matchers.containsInAnyOrder
|
||||
import org.hamcrest.Matchers.hasSize
|
||||
import org.hamcrest.Matchers.`is`
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class StorySendsDatabaseTest {
|
||||
|
||||
private lateinit var recipients1to10: List<RecipientId>
|
||||
private lateinit var recipients11to20: List<RecipientId>
|
||||
private lateinit var recipients6to15: List<RecipientId>
|
||||
private lateinit var recipients6to10: List<RecipientId>
|
||||
|
||||
private var messageId1: Long = 0
|
||||
private var messageId2: Long = 0
|
||||
private var messageId3: Long = 0
|
||||
|
||||
private lateinit var storySends: StorySendsDatabase
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
storySends = SignalDatabase.storySends
|
||||
|
||||
messageId1 = MmsHelper.insert(storyType = StoryType.STORY_WITHOUT_REPLIES)
|
||||
messageId2 = MmsHelper.insert(storyType = StoryType.STORY_WITH_REPLIES)
|
||||
messageId3 = MmsHelper.insert(storyType = StoryType.STORY_WITHOUT_REPLIES)
|
||||
|
||||
recipients1to10 = makeRecipients(10)
|
||||
recipients11to20 = makeRecipients(10)
|
||||
|
||||
recipients6to15 = recipients1to10.takeLast(5) + recipients11to20.take(5)
|
||||
recipients6to10 = recipients1to10.takeLast(5)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRecipientsToSendTo_noOverlap() {
|
||||
storySends.insert(messageId1, recipients1to10, 100, false)
|
||||
storySends.insert(messageId2, recipients11to20, 200, true)
|
||||
storySends.insert(messageId3, recipients1to10, 300, false)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
|
||||
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 200, true)
|
||||
|
||||
assertThat(recipientIdsForMessage1, hasSize(10))
|
||||
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.toTypedArray()))
|
||||
|
||||
assertThat(recipientIdsForMessage2, hasSize(10))
|
||||
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients11to20.toTypedArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRecipientsToSendTo_overlap() {
|
||||
storySends.insert(messageId1, recipients1to10, 100, false)
|
||||
storySends.insert(messageId2, recipients6to15, 100, true)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
|
||||
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, true)
|
||||
|
||||
assertThat(recipientIdsForMessage1, hasSize(5))
|
||||
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.take(5).toTypedArray()))
|
||||
|
||||
assertThat(recipientIdsForMessage2, hasSize(10))
|
||||
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients6to15.toTypedArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRecipientsToSendTo_overlapAll() {
|
||||
val recipient1 = recipients1to10.first()
|
||||
val recipient2 = recipients11to20.first()
|
||||
|
||||
storySends.insert(messageId1, listOf(recipient1, recipient2), 100, false)
|
||||
storySends.insert(messageId2, listOf(recipient1), 100, true)
|
||||
storySends.insert(messageId3, listOf(recipient2), 100, true)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
|
||||
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, true)
|
||||
val recipientIdsForMessage3 = storySends.getRecipientsToSendTo(messageId3, 100, true)
|
||||
|
||||
assertThat(recipientIdsForMessage1, hasSize(0))
|
||||
|
||||
assertThat(recipientIdsForMessage2, hasSize(1))
|
||||
assertThat(recipientIdsForMessage2, containsInAnyOrder(recipient1))
|
||||
|
||||
assertThat(recipientIdsForMessage3, hasSize(1))
|
||||
assertThat(recipientIdsForMessage3, containsInAnyOrder(recipient2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRecipientsToSendTo_overlapWithEarlierMessage() {
|
||||
storySends.insert(messageId1, recipients6to15, 100, true)
|
||||
storySends.insert(messageId2, recipients1to10, 100, false)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, true)
|
||||
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, false)
|
||||
|
||||
assertThat(recipientIdsForMessage1, hasSize(10))
|
||||
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients6to15.toTypedArray()))
|
||||
|
||||
assertThat(recipientIdsForMessage2, hasSize(5))
|
||||
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients1to10.take(5).toTypedArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRemoteDeleteRecipients_noOverlap() {
|
||||
storySends.insert(messageId1, recipients1to10, 100, false)
|
||||
storySends.insert(messageId2, recipients11to20, 200, true)
|
||||
storySends.insert(messageId3, recipients1to10, 300, false)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRemoteDeleteRecipients(messageId1, 100)
|
||||
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
|
||||
|
||||
assertThat(recipientIdsForMessage1, hasSize(10))
|
||||
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.toTypedArray()))
|
||||
|
||||
assertThat(recipientIdsForMessage2, hasSize(10))
|
||||
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients11to20.toTypedArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRemoteDeleteRecipients_overlapNoPreviousDeletes() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false)
|
||||
storySends.insert(messageId2, recipients6to15, 200, true)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRemoteDeleteRecipients(messageId1, 200)
|
||||
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
|
||||
|
||||
assertThat(recipientIdsForMessage1, hasSize(5))
|
||||
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.take(5).toTypedArray()))
|
||||
|
||||
assertThat(recipientIdsForMessage2, hasSize(5))
|
||||
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients6to15.takeLast(5).toTypedArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRemoteDeleteRecipients_overlapWithPreviousDeletes() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false)
|
||||
SignalDatabase.mms.markAsRemoteDelete(messageId1)
|
||||
|
||||
storySends.insert(messageId2, recipients6to15, 200, true)
|
||||
|
||||
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
|
||||
|
||||
assertThat(recipientIdsForMessage2, hasSize(10))
|
||||
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients6to15.toTypedArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun canReply_storyWithReplies() {
|
||||
storySends.insert(messageId2, recipients1to10, 200, true)
|
||||
|
||||
val canReply = storySends.canReply(recipients1to10[0], 200)
|
||||
|
||||
assertThat(canReply, `is`(true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun canReply_storyWithoutReplies() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false)
|
||||
|
||||
val canReply = storySends.canReply(recipients1to10[0], 200)
|
||||
|
||||
assertThat(canReply, `is`(false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun canReply_storyWithAndWithoutRepliesOverlap() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false)
|
||||
storySends.insert(messageId2, recipients6to10, 200, true)
|
||||
|
||||
val message1OnlyRecipientCanReply = storySends.canReply(recipients1to10[0], 200)
|
||||
val message2RecipientCanReply = storySends.canReply(recipients6to10[0], 200)
|
||||
|
||||
assertThat(message1OnlyRecipientCanReply, `is`(false))
|
||||
assertThat(message2RecipientCanReply, `is`(true))
|
||||
}
|
||||
|
||||
private fun makeRecipients(count: Int): List<RecipientId> {
|
||||
return (1..count).map {
|
||||
SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
import org.signal.core.util.Hex;
|
||||
import org.whispersystems.signalservice.api.kbs.HashedPin;
|
||||
import org.whispersystems.signalservice.api.kbs.KbsData;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
|
||||
@@ -179,11 +179,12 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
<activity android:name=".sharing.ShareActivity"
|
||||
<activity android:name=".sharing.v2.ShareActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:excludeFromRecents="true"
|
||||
android:taskAffinity=""
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:exported="true"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
@@ -403,7 +404,7 @@
|
||||
<activity
|
||||
android:name=".stories.viewer.StoryViewerActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/TextSecure.DarkNoActionBar"
|
||||
android:theme="@style/TextSecure.DarkNoActionBar.StoryViewer"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing" />
|
||||
|
||||
<activity android:name=".components.settings.app.changenumber.ChangeNumberLockActivity"
|
||||
@@ -416,6 +417,11 @@
|
||||
android:windowSoftInputMode="stateAlwaysHidden">
|
||||
</activity>
|
||||
|
||||
<activity android:name=".badges.gifts.flow.GiftFlowActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|screenLayout|screenSize"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateAlwaysHidden">
|
||||
</activity>
|
||||
|
||||
<activity android:name=".wallpaper.ChatWallpaperActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
@@ -757,6 +763,8 @@
|
||||
|
||||
<receiver android:name=".service.ExpirationListener" />
|
||||
|
||||
<receiver android:name=".service.ExpiringStoriesManager$ExpireStoriesAlarm" />
|
||||
|
||||
<receiver android:name=".revealable.ViewOnceMessageManager$ViewOnceAlarm" />
|
||||
|
||||
<receiver android:name=".service.PendingRetryReceiptManager$PendingRetryReceiptAlarm" />
|
||||
|
||||
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 233 KiB After Width: | Height: | Size: 238 KiB |
|
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 87 KiB |
BIN
app/src/main/assets/emoji/People_9.webp
Normal file
|
After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
@@ -0,0 +1,14 @@
|
||||
package com.google.android.material.bottomsheet
|
||||
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/**
|
||||
* Manually adjust the nested scrolling child for a given [BottomSheetBehavior].
|
||||
*/
|
||||
object BottomSheetBehaviorHack {
|
||||
fun setNestedScrollingChild(behavior: BottomSheetBehavior<FrameLayout>, view: View) {
|
||||
behavior.nestedScrollingChildRef = WeakReference(view)
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,6 @@ public final class AppCapabilities {
|
||||
* asking if the user has set a Signal PIN or not.
|
||||
*/
|
||||
public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
|
||||
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, SENDER_KEY, ANNOUNCEMENT_GROUPS, CHANGE_NUMBER, FeatureFlags.stories());
|
||||
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, SENDER_KEY, ANNOUNCEMENT_GROUPS, CHANGE_NUMBER, FeatureFlags.stories(), FeatureFlags.giftBadges());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.signal.core.util.logging.AndroidLogger;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.core.util.tracing.Tracer;
|
||||
import org.signal.glide.SignalGlideCodecs;
|
||||
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
||||
@@ -53,6 +54,7 @@ import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob;
|
||||
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.FontDownloaderJob;
|
||||
import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
@@ -91,7 +93,6 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.VersionTracker;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider;
|
||||
|
||||
import java.net.SocketException;
|
||||
import java.net.SocketTimeoutException;
|
||||
@@ -201,6 +202,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addPostRender(() -> AndroidTelecomUtil.registerPhoneAccount())
|
||||
.addPostRender(() -> ApplicationDependencies.getJobManager().add(new FontDownloaderJob()))
|
||||
.addPostRender(CheckServiceReachabilityJob::enqueueIfNecessary)
|
||||
.addPostRender(GroupV2UpdateSelfProfileKeyJob::enqueueForGroupsIfNecessary)
|
||||
.execute();
|
||||
|
||||
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
|
||||
@@ -14,6 +14,7 @@ import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
|
||||
@@ -58,6 +59,12 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
|
||||
return ActivityOptionsCompat.makeSceneTransitionAnimation(activity, from, "avatar").toBundle();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(@NonNull Context newBase) {
|
||||
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
|
||||
super.attachBaseContext(newBase);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||
super.onCreate(savedInstanceState, ready);
|
||||
@@ -132,7 +139,7 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
|
||||
|
||||
findViewById(android.R.id.content).setOnClickListener(v -> fullscreenHelper.toggleUiVisibility());
|
||||
|
||||
fullscreenHelper.configureToolbarSpacer(findViewById(R.id.toolbar_cutout_spacer));
|
||||
fullscreenHelper.configureToolbarLayout(findViewById(R.id.toolbar_cutout_spacer), toolbar);
|
||||
|
||||
fullscreenHelper.showAndHideWithSystemUI(getWindow(), findViewById(R.id.toolbar_layout));
|
||||
}
|
||||
|
||||
@@ -26,10 +26,10 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
public interface BindableConversationItem extends Unbindable, GiphyMp4Playable, Colorizable, Multiselectable {
|
||||
@@ -60,6 +60,10 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
// Intentionally Blank.
|
||||
}
|
||||
|
||||
default void updateSelectedState() {
|
||||
// Intentionall Blank.
|
||||
}
|
||||
|
||||
interface EventListener {
|
||||
void onQuoteClicked(MmsMessageRecord messageRecord);
|
||||
void onLinkPreviewClicked(@NonNull LinkPreview linkPreview);
|
||||
@@ -94,8 +98,12 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
void onChangeNumberUpdateContact(@NonNull Recipient recipient);
|
||||
void onCallToAction(@NonNull String action);
|
||||
void onDonateClicked();
|
||||
void onBlockJoinRequest(@NonNull Recipient recipient);
|
||||
void onRecipientNameClicked(@NonNull RecipientId target);
|
||||
|
||||
/** @return true if handled, false if you want to let the normal url handling continue */
|
||||
boolean onUrlClicked(@NonNull String url);
|
||||
|
||||
void onViewGiftBadgeClicked(@NonNull MessageRecord messageRecord);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
@@ -13,7 +13,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.signal.core.util.concurrent.SimpleTask;
|
||||
|
||||
/**
|
||||
* This should be used whenever we want to prompt the user to block/unblock a recipient.
|
||||
|
||||
@@ -27,16 +27,16 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
@@ -153,7 +153,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
||||
@Override
|
||||
protected Void doInBackground(Context... params) {
|
||||
try {
|
||||
DirectoryHelper.refreshDirectory(params[0], true);
|
||||
ContactDiscovery.refreshAll(params[0], true);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.HeaderAction;
|
||||
import org.thoughtcrime.securesms.contacts.LetterHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
@@ -85,13 +85,13 @@ import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.adapter.FixedViewsAdapter;
|
||||
import org.thoughtcrime.securesms.util.adapter.RecyclerViewConcatenateAdapterStickyHeader;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.signal.core.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -576,7 +576,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... voids) {
|
||||
try {
|
||||
DirectoryHelper.refreshDirectory(context, false);
|
||||
ContactDiscovery.refreshAll(context, false);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
@@ -637,8 +637,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
private class ListClickListener implements ContactSelectionListAdapter.ItemClickListener {
|
||||
@Override
|
||||
public void onItemClick(ContactSelectionListItem contact) {
|
||||
SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orNull(), contact.getNumber())
|
||||
: SelectedContact.forPhone(contact.getRecipientId().orNull(), contact.getNumber());
|
||||
SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orElse(null), contact.getNumber())
|
||||
: SelectedContact.forPhone(contact.getRecipientId().orElse(null), contact.getNumber());
|
||||
|
||||
if (!canSelectSelf && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) {
|
||||
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_you_do_not_need_to_add_yourself_to_the_group, Toast.LENGTH_SHORT).show();
|
||||
@@ -774,7 +774,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
markContactUnselected(selectedContact);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactDeselected(Optional.of(recipient.getId()), recipient.getE164().orNull());
|
||||
onContactSelectedListener.onContactDeselected(Optional.of(recipient.getId()), recipient.getE164().orElse(null));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -17,12 +17,14 @@ import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.ecc.Curve;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
@@ -34,11 +36,6 @@ import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.ecc.Curve;
|
||||
import org.whispersystems.libsignal.ecc.ECPublicKey;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
|
||||
import org.whispersystems.signalservice.internal.push.DeviceLimitExceededException;
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
|
||||
@@ -38,8 +38,8 @@ import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture.Listener;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -251,7 +251,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
for (SelectedContact contact : contacts) {
|
||||
RecipientId recipientId = contact.getOrCreateRecipientId(context);
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
int subscriptionId = recipient.getDefaultSubscriptionId().or(-1);
|
||||
int subscriptionId = recipient.getDefaultSubscriptionId().orElse(-1);
|
||||
|
||||
MessageSender.send(context, new OutgoingTextMessage(recipient, message, subscriptionId), -1L, true, null, null);
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import android.view.View;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
|
||||
@@ -19,14 +18,13 @@ import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferLock
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.stories.Stories;
|
||||
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabRepository;
|
||||
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsState;
|
||||
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
import org.thoughtcrime.securesms.util.CachedInflater;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.SplashScreenUtil;
|
||||
import org.thoughtcrime.securesms.util.WindowUtil;
|
||||
|
||||
public class MainActivity extends PassphraseRequiredActivity implements VoiceNoteMediaControllerOwner {
|
||||
@@ -61,8 +59,6 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
ConversationListTabRepository repository = new ConversationListTabRepository();
|
||||
ConversationListTabsViewModel.Factory factory = new ConversationListTabsViewModel.Factory(repository);
|
||||
|
||||
navigator.onCreate(savedInstanceState);
|
||||
|
||||
handleGroupLinkInIntent(getIntent());
|
||||
handleProxyInIntent(getIntent());
|
||||
handleSignalMeIntent(getIntent());
|
||||
@@ -70,18 +66,6 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
CachedInflater.from(this).clear();
|
||||
|
||||
conversationListTabsViewModel = new ViewModelProvider(this, factory).get(ConversationListTabsViewModel.class);
|
||||
Transformations.map(conversationListTabsViewModel.getState(), ConversationListTabsState::getTab)
|
||||
.observe(this, tab -> {
|
||||
switch (tab) {
|
||||
case CHATS:
|
||||
getSupportFragmentManager().popBackStack();
|
||||
break;
|
||||
case STORIES:
|
||||
navigator.goToStories();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
updateTabVisibility();
|
||||
}
|
||||
|
||||
@@ -117,6 +101,12 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
updateTabVisibility();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
SplashScreenUtil.setSplashScreenThemeIfNecessary(this, SignalStore.settings().getTheme());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (!navigator.onBackPressed()) {
|
||||
@@ -135,11 +125,11 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
private void updateTabVisibility() {
|
||||
if (Stories.isFeatureEnabled()) {
|
||||
findViewById(R.id.conversation_list_tabs).setVisibility(View.VISIBLE);
|
||||
WindowUtil.setNavigationBarColor(getWindow(), ContextCompat.getColor(this, R.color.signal_background_secondary));
|
||||
WindowUtil.setNavigationBarColor(getWindow(), ContextCompat.getColor(this, R.color.signal_colorSecondaryContainer));
|
||||
} else {
|
||||
findViewById(R.id.conversation_list_tabs).setVisibility(View.GONE);
|
||||
WindowUtil.setNavigationBarColor(getWindow(), ContextCompat.getColor(this, R.color.signal_background_primary));
|
||||
navigator.goToChats();
|
||||
conversationListTabsViewModel.onChatsSelected();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,26 +2,19 @@ package org.thoughtcrime.securesms;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
||||
import org.thoughtcrime.securesms.conversationlist.ConversationListArchiveFragment;
|
||||
import org.thoughtcrime.securesms.conversationlist.ConversationListFragment;
|
||||
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
|
||||
import org.thoughtcrime.securesms.insights.InsightsLauncher;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.stories.landing.StoriesLandingFragment;
|
||||
|
||||
public class MainNavigator {
|
||||
|
||||
public static final String STORIES_TAG = "STORIES";
|
||||
|
||||
public static final int REQUEST_CONFIG_CHANGES = 901;
|
||||
|
||||
private final MainActivity activity;
|
||||
@@ -38,16 +31,6 @@ public class MainNavigator {
|
||||
return ((MainActivity) activity).getNavigator();
|
||||
}
|
||||
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
if (savedInstanceState != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
getFragmentManager().beginTransaction()
|
||||
.add(R.id.fragment_container, ConversationListFragment.newInstance())
|
||||
.commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* @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.
|
||||
@@ -76,29 +59,6 @@ public class MainNavigator {
|
||||
activity.startActivityForResult(AppSettingsActivity.home(activity), REQUEST_CONFIG_CHANGES);
|
||||
}
|
||||
|
||||
public void goToArchiveList() {
|
||||
getFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end)
|
||||
.replace(R.id.fragment_container, ConversationListArchiveFragment.newInstance())
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
|
||||
public void goToStories() {
|
||||
if (getFragmentManager().findFragmentByTag(STORIES_TAG) == null) {
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(R.id.fragment_container, new StoriesLandingFragment(), STORIES_TAG)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
|
||||
public void goToChats() {
|
||||
if (getFragmentManager().findFragmentByTag(STORIES_TAG) != null) {
|
||||
getFragmentManager().popBackStack();
|
||||
}
|
||||
}
|
||||
|
||||
public void goToGroupCreation() {
|
||||
activity.startActivity(CreateGroupActivity.newIntent(activity));
|
||||
}
|
||||
|
||||
@@ -55,6 +55,10 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.animation.DepthPageTransformer;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.components.viewpager.ExtendedOnPageChangedListener;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
|
||||
import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader;
|
||||
@@ -67,7 +71,6 @@ import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.sharing.ShareActivity;
|
||||
import org.thoughtcrime.securesms.util.AttachmentUtil;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.FullscreenHelper;
|
||||
@@ -87,7 +90,8 @@ import java.util.Objects;
|
||||
public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
implements LoaderManager.LoaderCallbacks<Pair<Cursor, Integer>>,
|
||||
MediaRailAdapter.RailItemListener,
|
||||
MediaPreviewFragment.Events
|
||||
MediaPreviewFragment.Events,
|
||||
VoiceNoteMediaControllerOwner
|
||||
{
|
||||
|
||||
private final static String TAG = Log.tag(MediaPreviewActivity.class);
|
||||
@@ -127,6 +131,8 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
private MediaDatabase.Sorting sorting;
|
||||
private FullscreenHelper fullscreenHelper;
|
||||
|
||||
private VoiceNoteMediaController voiceNoteMediaController;
|
||||
|
||||
private @Nullable Cursor cursor = null;
|
||||
|
||||
public static @NonNull Intent intentFromMediaRecord(@NonNull Context context,
|
||||
@@ -159,6 +165,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
|
||||
voiceNoteMediaController = new VoiceNoteMediaController(this);
|
||||
viewModel = ViewModelProviders.of(this).get(MediaPreviewViewModel.class);
|
||||
|
||||
fullscreenHelper = new FullscreenHelper(this);
|
||||
@@ -203,23 +210,25 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
else from = "";
|
||||
|
||||
if (showThread) {
|
||||
String to = null;
|
||||
String titleText = null;
|
||||
Recipient threadRecipient = mediaItem.threadRecipient;
|
||||
|
||||
if (threadRecipient != null) {
|
||||
if (mediaItem.outgoing || threadRecipient.isGroup()) {
|
||||
if (mediaItem.outgoing) {
|
||||
if (threadRecipient.isSelf()) {
|
||||
from = getString(R.string.note_to_self);
|
||||
titleText = getString(R.string.note_to_self);
|
||||
} else {
|
||||
to = threadRecipient.getDisplayName(this);
|
||||
titleText = getString(R.string.MediaPreviewActivity_you_to_s, threadRecipient.getDisplayName(this));
|
||||
}
|
||||
} else {
|
||||
to = getString(R.string.MediaPreviewActivity_you);
|
||||
if (threadRecipient.isGroup()) {
|
||||
titleText = getString(R.string.MediaPreviewActivity_s_to_s, from, threadRecipient.getDisplayName(this));
|
||||
} else {
|
||||
titleText = getString(R.string.MediaPreviewActivity_s_to_you, from);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return to != null ? getString(R.string.MediaPreviewActivity_s_to_s, from, to)
|
||||
: from;
|
||||
return titleText != null ? titleText : from;
|
||||
} else {
|
||||
return from;
|
||||
}
|
||||
@@ -286,7 +295,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
|
||||
anchorMarginsToBottomInsets(detailsContainer);
|
||||
|
||||
fullscreenHelper.configureToolbarSpacer(findViewById(R.id.toolbar_cutout_spacer));
|
||||
fullscreenHelper.configureToolbarLayout(findViewById(R.id.toolbar_cutout_spacer), findViewById(R.id.toolbar));
|
||||
|
||||
fullscreenHelper.showAndHideWithSystemUI(getWindow(), detailsContainer, toolbarLayout);
|
||||
}
|
||||
@@ -386,10 +395,12 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
MediaItem mediaItem = getCurrentMediaItem();
|
||||
|
||||
if (mediaItem != null) {
|
||||
Intent composeIntent = new Intent(this, ShareActivity.class);
|
||||
composeIntent.putExtra(Intent.EXTRA_STREAM, mediaItem.uri);
|
||||
composeIntent.setType(mediaItem.type);
|
||||
startActivity(composeIntent);
|
||||
MultiselectForwardFragmentArgs.create(
|
||||
this,
|
||||
mediaItem.uri,
|
||||
mediaItem.type,
|
||||
args -> MultiselectForwardFragment.showBottomSheet(getSupportFragmentManager(), args)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -597,6 +608,15 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaReady() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull VoiceNoteMediaController getVoiceNoteMediaController() {
|
||||
return voiceNoteMediaController;
|
||||
}
|
||||
|
||||
private class ViewPagerListener extends ExtendedOnPageChangedListener {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@@ -25,7 +25,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
|
||||
@@ -33,11 +33,11 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.signal.core.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
@@ -79,7 +79,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
if (!resolved.isRegistered() || !resolved.hasServiceId()) {
|
||||
Log.i(TAG, "[onContactSelected] Not registered or no UUID. Doing a directory refresh.");
|
||||
try {
|
||||
DirectoryHelper.refreshDirectoryFor(this, resolved, false);
|
||||
ContactDiscovery.refresh(this, resolved, false);
|
||||
resolved = Recipient.resolved(resolved.getId());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "[onContactSelected] Failed to refresh directory for new contact.");
|
||||
|
||||
@@ -61,7 +61,7 @@ public class PassphraseCreateActivity extends PassphraseActivity {
|
||||
passphrase);
|
||||
|
||||
MasterSecretUtil.generateAsymmetricMasterSecret(PassphraseCreateActivity.this, masterSecret);
|
||||
SignalStore.account().generateAciIdentityKey();
|
||||
SignalStore.account().generateAciIdentityKeyIfNecessary();
|
||||
SignalStore.account().generatePniIdentityKeyIfNecessary();
|
||||
VersionTracker.updateLastSeenVersion(PassphraseCreateActivity.this);
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.util.CharacterCalculator;
|
||||
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
|
||||
public class TransportOption implements Parcelable {
|
||||
|
||||
@@ -18,8 +20,8 @@ public class TransportOption implements Parcelable {
|
||||
TEXTSECURE
|
||||
}
|
||||
|
||||
private final int drawable;
|
||||
private final int backgroundColor;
|
||||
private final int drawable;
|
||||
private final int backgroundColor;
|
||||
private final @NonNull String text;
|
||||
private final @NonNull Type type;
|
||||
private final @NonNull String composeHint;
|
||||
@@ -35,7 +37,7 @@ public class TransportOption implements Parcelable {
|
||||
@NonNull CharacterCalculator characterCalculator)
|
||||
{
|
||||
this(type, drawable, backgroundColor, text, composeHint, characterCalculator,
|
||||
Optional.<CharSequence>absent(), Optional.<Integer>absent());
|
||||
Optional.empty(), Optional.empty());
|
||||
}
|
||||
|
||||
public TransportOption(@NonNull Type type,
|
||||
@@ -64,8 +66,8 @@ public class TransportOption implements Parcelable {
|
||||
in.readString(),
|
||||
in.readString(),
|
||||
CharacterCalculator.readFromParcel(in),
|
||||
Optional.fromNullable(TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in)),
|
||||
in.readInt() == 1 ? Optional.of(in.readInt()) : Optional.absent());
|
||||
Optional.ofNullable(TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in)),
|
||||
in.readInt() == 1 ? Optional.of(in.readInt()) : Optional.empty());
|
||||
}
|
||||
|
||||
public @NonNull Type getType() {
|
||||
@@ -123,7 +125,7 @@ public class TransportOption implements Parcelable {
|
||||
dest.writeString(text);
|
||||
dest.writeString(composeHint);
|
||||
CharacterCalculator.writeToParcel(dest, characterCalculator);
|
||||
TextUtils.writeToParcel(simName.orNull(), dest, flags);
|
||||
TextUtils.writeToParcel(simName.orElse(null), dest, flags);
|
||||
|
||||
if (simSubscriptionId.isPresent()) {
|
||||
dest.writeInt(1);
|
||||
|
||||
@@ -14,13 +14,14 @@ import org.thoughtcrime.securesms.util.PushCharacterCalculator;
|
||||
import org.thoughtcrime.securesms.util.SmsCharacterCalculator;
|
||||
import org.thoughtcrime.securesms.util.dualsim.SubscriptionInfoCompat;
|
||||
import org.thoughtcrime.securesms.util.dualsim.SubscriptionManagerCompat;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.util.OptionalUtil;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.thoughtcrime.securesms.TransportOption.Type;
|
||||
|
||||
@@ -33,8 +34,8 @@ public class TransportOptions {
|
||||
private final List<TransportOption> enabledTransports;
|
||||
|
||||
private Type defaultTransportType = Type.SMS;
|
||||
private Optional<Integer> defaultSubscriptionId = Optional.absent();
|
||||
private Optional<TransportOption> selectedOption = Optional.absent();
|
||||
private Optional<Integer> defaultSubscriptionId = Optional.empty();
|
||||
private Optional<TransportOption> selectedOption = Optional.empty();
|
||||
|
||||
private final Optional<Integer> systemSubscriptionId;
|
||||
|
||||
@@ -54,7 +55,7 @@ public class TransportOptions {
|
||||
setSelectedTransport(null);
|
||||
} else {
|
||||
this.defaultTransportType = Type.SMS;
|
||||
this.defaultSubscriptionId = Optional.absent();
|
||||
this.defaultSubscriptionId = Optional.empty();
|
||||
|
||||
notifyTransportChangeListeners();
|
||||
}
|
||||
@@ -81,7 +82,7 @@ public class TransportOptions {
|
||||
}
|
||||
|
||||
public void setSelectedTransport(@Nullable TransportOption transportOption) {
|
||||
this.selectedOption = Optional.fromNullable(transportOption);
|
||||
this.selectedOption = Optional.ofNullable(transportOption);
|
||||
notifyTransportChangeListeners();
|
||||
}
|
||||
|
||||
@@ -93,7 +94,7 @@ public class TransportOptions {
|
||||
if (selectedOption.isPresent()) return selectedOption.get();
|
||||
|
||||
if (defaultTransportType == Type.SMS) {
|
||||
TransportOption transportOption = findEnabledSmsTransportOption(defaultSubscriptionId.or(systemSubscriptionId));
|
||||
TransportOption transportOption = findEnabledSmsTransportOption(OptionalUtil.or(defaultSubscriptionId, systemSubscriptionId));
|
||||
if (transportOption != null) {
|
||||
return transportOption;
|
||||
}
|
||||
@@ -124,7 +125,7 @@ public class TransportOptions {
|
||||
|
||||
for (TransportOption transportOption : enabledTransports) {
|
||||
if (transportOption.getType() == Type.SMS &&
|
||||
subId == transportOption.getSimSubscriptionId().or(-1)) {
|
||||
subId == transportOption.getSimSubscriptionId().orElse(-1)) {
|
||||
return transportOption;
|
||||
}
|
||||
}
|
||||
@@ -133,7 +134,7 @@ public class TransportOptions {
|
||||
}
|
||||
|
||||
public void disableTransport(Type type) {
|
||||
TransportOption selected = selectedOption.orNull();
|
||||
TransportOption selected = selectedOption.orElse(null);
|
||||
|
||||
Iterator<TransportOption> iterator = enabledTransports.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
|
||||
@@ -39,7 +39,7 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.window.DisplayFeature;
|
||||
import androidx.window.FoldingFeature;
|
||||
import androidx.window.WindowLayoutInfo;
|
||||
@@ -49,6 +49,7 @@ import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||
import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor;
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsListUpdatePopupWindow;
|
||||
@@ -75,16 +76,19 @@ import org.thoughtcrime.securesms.util.FullscreenHelper;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ThrottledDebouncer;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.VibrateUtil;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.thoughtcrime.securesms.webrtc.CallParticipantsViewState;
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
|
||||
|
||||
public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChangeDialog.Callback {
|
||||
@@ -92,6 +96,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
private static final String TAG = Log.tag(WebRtcCallActivity.class);
|
||||
|
||||
private static final int STANDARD_DELAY_FINISH = 1000;
|
||||
private static final int VIBRATE_DURATION = 50;
|
||||
|
||||
public static final String ANSWER_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_ACTION";
|
||||
public static final String DENY_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".DENY_ACTION";
|
||||
@@ -111,6 +116,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
private WindowLayoutInfoConsumer windowLayoutInfoConsumer;
|
||||
private ThrottledDebouncer requestNewSizesThrottle;
|
||||
|
||||
private Disposable ephemeralStateDisposable = Disposable.empty();
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(@NonNull Context newBase) {
|
||||
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
|
||||
@@ -153,6 +160,16 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
requestNewSizesThrottle = new ThrottledDebouncer(TimeUnit.SECONDS.toMillis(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
|
||||
ephemeralStateDisposable = ApplicationDependencies.getSignalCallManager()
|
||||
.ephemeralStates()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(viewModel::updateFromEphemeralState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
Log.i(TAG, "onResume()");
|
||||
@@ -193,6 +210,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
Log.i(TAG, "onStop");
|
||||
super.onStop();
|
||||
|
||||
ephemeralStateDisposable.dispose();
|
||||
|
||||
if (!isInPipMode() || isFinishing()) {
|
||||
EventBus.getDefault().unregister(this);
|
||||
requestNewSizesThrottle.clear();
|
||||
@@ -285,7 +304,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
|
||||
WebRtcCallViewModel.Factory factory = new WebRtcCallViewModel.Factory(deviceOrientationMonitor);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, factory).get(WebRtcCallViewModel.class);
|
||||
viewModel = new ViewModelProvider(this, factory).get(WebRtcCallViewModel.class);
|
||||
viewModel.setIsLandscapeEnabled(isLandscapeEnabled);
|
||||
viewModel.setIsInPipMode(isInPipMode());
|
||||
viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled);
|
||||
@@ -295,7 +314,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
|
||||
LiveDataUtil.combineLatest(viewModel.getCallParticipantsState(),
|
||||
viewModel.getOrientationAndLandscapeEnabled(),
|
||||
(s, o) -> new CallParticipantsViewState(s, o.first == PORTRAIT_BOTTOM_EDGE, o.second))
|
||||
viewModel.getEphemeralState(),
|
||||
(s, o, e) -> new CallParticipantsViewState(s, e, o.first == PORTRAIT_BOTTOM_EDGE, o.second))
|
||||
.observe(this, p -> callScreen.updateCallParticipants(p));
|
||||
viewModel.getCallParticipantListUpdate().observe(this, participantUpdateWindow::addCallParticipantListUpdate);
|
||||
viewModel.getSafetyNumberChangeEvent().observe(this, this::handleSafetyNumberChangeEvent);
|
||||
@@ -501,6 +521,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCallReconnecting() {
|
||||
callScreen.setStatus(getString(R.string.WebRtcCallActivity__reconnecting));
|
||||
VibrateUtil.vibrate(this, VIBRATE_DURATION);
|
||||
}
|
||||
|
||||
private void handleRecipientUnavailable() {
|
||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
||||
callScreen.setStatus(getString(R.string.RedPhone_recipient_unavailable));
|
||||
@@ -623,6 +648,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
handleCallPreJoin(event); break;
|
||||
case CALL_CONNECTED:
|
||||
handleCallConnected(event); break;
|
||||
case CALL_RECONNECTING:
|
||||
handleCallReconnecting(); break;
|
||||
case NETWORK_FAILURE:
|
||||
handleServerFailure(); break;
|
||||
case CALL_RINGING:
|
||||
@@ -807,7 +834,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
if (feature.isPresent()) {
|
||||
FoldingFeature foldingFeature = (FoldingFeature) feature.get();
|
||||
Rect bounds = foldingFeature.getBounds();
|
||||
if (foldingFeature.getState() == FoldingFeature.State.HALF_OPENED && bounds.top == bounds.bottom) {
|
||||
if (foldingFeature.isSeparating()) {
|
||||
Log.d(TAG, "OnWindowLayoutInfo accepted: ensure call view is in table-top display mode");
|
||||
viewModel.setFoldableState(WebRtcControls.FoldableState.folded(bounds.top));
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.thoughtcrime.securesms.animation.transitions
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.transition.Transition
|
||||
import android.transition.TransitionValues
|
||||
import android.util.AttributeSet
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.core.animation.doOnStart
|
||||
|
||||
@RequiresApi(21)
|
||||
class CrossfaderTransition(context: Context, attrs: AttributeSet?) : Transition(context, attrs) {
|
||||
|
||||
companion object {
|
||||
private const val WIDTH = "CrossfaderTransition.WIDTH"
|
||||
}
|
||||
|
||||
override fun captureStartValues(transitionValues: TransitionValues) {
|
||||
if (transitionValues.view is Crossfadeable) {
|
||||
transitionValues.values[WIDTH] = transitionValues.view.width
|
||||
}
|
||||
}
|
||||
|
||||
override fun captureEndValues(transitionValues: TransitionValues) {
|
||||
if (transitionValues.view is Crossfadeable) {
|
||||
transitionValues.values[WIDTH] = transitionValues.view.width
|
||||
}
|
||||
}
|
||||
|
||||
override fun createAnimator(sceneRoot: ViewGroup?, startValues: TransitionValues?, endValues: TransitionValues?): Animator? {
|
||||
if (startValues == null || endValues == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val startWidth = (startValues.values[WIDTH] ?: 0) as Int
|
||||
val endWidth = (endValues.values[WIDTH] ?: 0) as Int
|
||||
val view: Crossfadeable = endValues.view as? Crossfadeable ?: return null
|
||||
val reverse = startWidth > endWidth
|
||||
|
||||
return ValueAnimator.ofFloat(0f, 1f).apply {
|
||||
addUpdateListener {
|
||||
view.onCrossfadeAnimationUpdated(it.animatedValue as Float, reverse)
|
||||
}
|
||||
|
||||
doOnStart {
|
||||
view.onCrossfadeStarted(reverse)
|
||||
}
|
||||
|
||||
doOnEnd {
|
||||
view.onCrossfadeFinished(reverse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface Crossfadeable {
|
||||
fun onCrossfadeAnimationUpdated(progress: Float, reverse: Boolean)
|
||||
fun onCrossfadeStarted(reverse: Boolean)
|
||||
fun onCrossfadeFinished(reverse: Boolean)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.thoughtcrime.securesms.animation.transitions
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.transition.Transition
|
||||
import android.transition.TransitionValues
|
||||
import android.util.AttributeSet
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
|
||||
@RequiresApi(21)
|
||||
class FabElevationFadeTransform(context: Context, attrs: AttributeSet?) : Transition(context, attrs) {
|
||||
|
||||
companion object {
|
||||
private const val ELEVATION = "CrossfaderTransition.ELEVATION"
|
||||
}
|
||||
|
||||
override fun captureStartValues(transitionValues: TransitionValues) {
|
||||
if (transitionValues.view is FloatingActionButton) {
|
||||
transitionValues.values[ELEVATION] = transitionValues.view.elevation
|
||||
}
|
||||
}
|
||||
|
||||
override fun captureEndValues(transitionValues: TransitionValues) {
|
||||
if (transitionValues.view is FloatingActionButton) {
|
||||
transitionValues.values[ELEVATION] = transitionValues.view.elevation
|
||||
}
|
||||
}
|
||||
|
||||
override fun createAnimator(sceneRoot: ViewGroup?, startValues: TransitionValues?, endValues: TransitionValues?): Animator? {
|
||||
if (startValues?.view !is FloatingActionButton || endValues?.view !is FloatingActionButton) {
|
||||
return null
|
||||
}
|
||||
|
||||
val startElevation = startValues.view.elevation
|
||||
val endElevation = endValues.view.elevation
|
||||
if (startElevation == endElevation) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ValueAnimator.ofFloat(
|
||||
startValues.values[ELEVATION] as Float,
|
||||
endValues.values[ELEVATION] as Float
|
||||
).apply {
|
||||
addUpdateListener {
|
||||
val elevation = it.animatedValue as Float
|
||||
endValues.view.elevation = elevation
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,12 @@ import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class PointerAttachment extends Attachment {
|
||||
|
||||
@@ -93,7 +93,7 @@ public class PointerAttachment extends Attachment {
|
||||
}
|
||||
|
||||
public static Optional<Attachment> forPointer(Optional<SignalServiceAttachment> pointer, @Nullable StickerLocator stickerLocator, @Nullable String fastPreflightId) {
|
||||
if (!pointer.isPresent() || !pointer.get().isPointer()) return Optional.absent();
|
||||
if (!pointer.isPresent() || !pointer.get().isPointer()) return Optional.empty();
|
||||
|
||||
String encodedKey = null;
|
||||
|
||||
@@ -103,12 +103,12 @@ public class PointerAttachment extends Attachment {
|
||||
|
||||
return Optional.of(new PointerAttachment(pointer.get().getContentType(),
|
||||
AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
|
||||
pointer.get().asPointer().getSize().or(0),
|
||||
pointer.get().asPointer().getFileName().orNull(),
|
||||
pointer.get().asPointer().getSize().orElse(0),
|
||||
pointer.get().asPointer().getFileName().orElse(null),
|
||||
pointer.get().asPointer().getCdnNumber(),
|
||||
pointer.get().asPointer().getRemoteId().toString(),
|
||||
encodedKey, null,
|
||||
pointer.get().asPointer().getDigest().orNull(),
|
||||
pointer.get().asPointer().getDigest().orElse(null),
|
||||
fastPreflightId,
|
||||
pointer.get().asPointer().getVoiceNote(),
|
||||
pointer.get().asPointer().isBorderless(),
|
||||
@@ -116,9 +116,9 @@ public class PointerAttachment extends Attachment {
|
||||
pointer.get().asPointer().getWidth(),
|
||||
pointer.get().asPointer().getHeight(),
|
||||
pointer.get().asPointer().getUploadTimestamp(),
|
||||
pointer.get().asPointer().getCaption().orNull(),
|
||||
pointer.get().asPointer().getCaption().orElse(null),
|
||||
stickerLocator,
|
||||
BlurHash.parseOrNull(pointer.get().asPointer().getBlurHash().orNull())));
|
||||
BlurHash.parseOrNull(pointer.get().asPointer().getBlurHash().orElse(null))));
|
||||
|
||||
}
|
||||
|
||||
@@ -127,13 +127,13 @@ public class PointerAttachment extends Attachment {
|
||||
|
||||
return Optional.of(new PointerAttachment(pointer.getContentType(),
|
||||
AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
|
||||
thumbnail != null ? thumbnail.asPointer().getSize().or(0) : 0,
|
||||
thumbnail != null ? thumbnail.asPointer().getSize().orElse(0) : 0,
|
||||
pointer.getFileName(),
|
||||
thumbnail != null ? thumbnail.asPointer().getCdnNumber() : 0,
|
||||
thumbnail != null ? thumbnail.asPointer().getRemoteId().toString() : "0",
|
||||
thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeBytes(thumbnail.asPointer().getKey()) : null,
|
||||
null,
|
||||
thumbnail != null ? thumbnail.asPointer().getDigest().orNull() : null,
|
||||
thumbnail != null ? thumbnail.asPointer().getDigest().orElse(null) : null,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
@@ -141,7 +141,7 @@ public class PointerAttachment extends Attachment {
|
||||
thumbnail != null ? thumbnail.asPointer().getWidth() : 0,
|
||||
thumbnail != null ? thumbnail.asPointer().getHeight() : 0,
|
||||
thumbnail != null ? thumbnail.asPointer().getUploadTimestamp() : 0,
|
||||
thumbnail != null ? thumbnail.asPointer().getCaption().orNull() : null,
|
||||
thumbnail != null ? thumbnail.asPointer().getCaption().orElse(null) : null,
|
||||
null,
|
||||
null));
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
||||
|
||||
@@ -14,10 +14,10 @@ import org.thoughtcrime.securesms.mms.PartAuthority
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.Optional
|
||||
import javax.annotation.meta.Exhaustive
|
||||
|
||||
/**
|
||||
@@ -128,6 +128,6 @@ object AvatarRenderer {
|
||||
}
|
||||
|
||||
private fun createMedia(uri: Uri, size: Long): Media {
|
||||
return Media(uri, MediaUtil.IMAGE_JPEG, System.currentTimeMillis(), DIMENSIONS, DIMENSIONS, size, 0, false, false, Optional.absent(), Optional.absent(), Optional.absent())
|
||||
return Media(uri, MediaUtil.IMAGE_JPEG, System.currentTimeMillis(), DIMENSIONS, DIMENSIONS, size, 0, false, false, Optional.empty(), Optional.empty(), Optional.empty())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,8 +31,8 @@ class TextAvatarDrawable(
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
val textSize = Avatars.getTextSizeForLength(context, avatar.text, size * 0.8f, size * 0.45f)
|
||||
val width = bounds.width()
|
||||
val textSize = Avatars.getTextSizeForLength(context, avatar.text, width * 0.8f, width * 0.45f)
|
||||
val candidates = EmojiProvider.getCandidates(avatar.text)
|
||||
|
||||
textPaint.textSize = textSize
|
||||
|
||||
@@ -26,7 +26,10 @@ class AvatarView @JvmOverloads constructor(
|
||||
isClickable = false
|
||||
}
|
||||
|
||||
private val avatar: AvatarImageView = findViewById(R.id.avatar_image_view)
|
||||
private val avatar: AvatarImageView = findViewById<AvatarImageView>(R.id.avatar_image_view).apply {
|
||||
initialize(context, attrs)
|
||||
}
|
||||
|
||||
private val storyRing: View = findViewById(R.id.avatar_story_ring)
|
||||
|
||||
private fun showStoryRing(hasUnreadStory: Boolean) {
|
||||
@@ -37,8 +40,8 @@ class AvatarView @JvmOverloads constructor(
|
||||
storyRing.visible = true
|
||||
storyRing.isActivated = hasUnreadStory
|
||||
|
||||
avatar.scaleX = 0.82f
|
||||
avatar.scaleY = 0.82f
|
||||
avatar.scaleX = 0.8f
|
||||
avatar.scaleY = 0.8f
|
||||
}
|
||||
|
||||
private fun hideStoryRing() {
|
||||
@@ -48,6 +51,10 @@ class AvatarView @JvmOverloads constructor(
|
||||
avatar.scaleY = 1f
|
||||
}
|
||||
|
||||
fun hasStory(): Boolean {
|
||||
return storyRing.visible
|
||||
}
|
||||
|
||||
fun setStoryRingFromState(storyViewState: StoryViewState) {
|
||||
when (storyViewState) {
|
||||
StoryViewState.NONE -> hideStoryRing()
|
||||
|
||||
@@ -23,6 +23,8 @@ import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
@@ -110,7 +112,7 @@ public class BackupDialog {
|
||||
|
||||
@RequiresApi(29)
|
||||
public static void showChooseBackupLocationDialog(@NonNull Fragment fragment, int requestCode) {
|
||||
new AlertDialog.Builder(fragment.requireContext())
|
||||
new MaterialAlertDialogBuilder(fragment.requireContext())
|
||||
.setView(R.layout.backup_choose_location_dialog)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
|
||||
@@ -141,7 +143,7 @@ public class BackupDialog {
|
||||
}
|
||||
|
||||
public static void showDisableBackupDialog(@NonNull Context context, @NonNull Runnable onBackupsDisabled) {
|
||||
new AlertDialog.Builder(context)
|
||||
new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.BackupDialog_delete_backups)
|
||||
.setMessage(R.string.BackupDialog_disable_and_delete_all_local_backups)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
|
||||
@@ -5,8 +5,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.whispersystems.libsignal.util.ByteUtil;
|
||||
import org.signal.libsignal.protocol.util.ByteUtil;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
@@ -18,10 +18,11 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.signal.core.util.Conversions;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.protocol.kdf.HKDFv3;
|
||||
import org.signal.libsignal.protocol.util.ByteUtil;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
|
||||
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
|
||||
@@ -47,13 +48,11 @@ import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.CursorUtil;
|
||||
import org.thoughtcrime.securesms.util.SetUtil;
|
||||
import org.signal.core.util.CursorUtil;
|
||||
import org.signal.core.util.SetUtil;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.kdf.HKDFv3;
|
||||
import org.whispersystems.libsignal.util.ByteUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
|
||||
@@ -17,6 +17,8 @@ import org.greenrobot.eventbus.EventBus;
|
||||
import org.signal.core.util.Conversions;
|
||||
import org.signal.core.util.StreamUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.protocol.kdf.HKDFv3;
|
||||
import org.signal.libsignal.protocol.util.ByteUtil;
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.Attachment;
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame;
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion;
|
||||
@@ -36,9 +38,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
import org.whispersystems.libsignal.kdf.HKDFv3;
|
||||
import org.whispersystems.libsignal.util.ByteUtil;
|
||||
import org.signal.core.util.SqlUtil;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
@@ -69,6 +69,18 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = Log.tag(FullBackupImporter.class);
|
||||
|
||||
private static final String[] TABLES_TO_DROP_FIRST = {
|
||||
"distribution_list_member",
|
||||
"distribution_list",
|
||||
"message_send_log_recipients",
|
||||
"msl_recipient",
|
||||
"msl_message",
|
||||
"reaction",
|
||||
"notification_profile_schedule",
|
||||
"notification_profile_allowed_members",
|
||||
"story_sends"
|
||||
};
|
||||
|
||||
public static void importFile(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret,
|
||||
@NonNull SQLiteDatabase db, @NonNull Uri uri, @NonNull String passphrase)
|
||||
throws IOException
|
||||
@@ -85,12 +97,12 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
int count = 0;
|
||||
|
||||
SQLiteDatabase keyValueDatabase = KeyValueDatabase.getInstance(ApplicationDependencies.getApplication()).getSqlCipherDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
keyValueDatabase.beginTransaction();
|
||||
try {
|
||||
BackupRecordInputStream inputStream = new BackupRecordInputStream(is, passphrase);
|
||||
|
||||
db.beginTransaction();
|
||||
keyValueDatabase.beginTransaction();
|
||||
|
||||
dropAllTables(db);
|
||||
|
||||
BackupFrame frame;
|
||||
@@ -272,12 +284,17 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
}
|
||||
|
||||
private static void dropAllTables(@NonNull SQLiteDatabase db) {
|
||||
for (String name : TABLES_TO_DROP_FIRST) {
|
||||
db.execSQL("DROP TABLE IF EXISTS " + name);
|
||||
}
|
||||
|
||||
try (Cursor cursor = db.rawQuery("SELECT name, type FROM sqlite_master", null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String name = cursor.getString(0);
|
||||
String type = cursor.getString(1);
|
||||
|
||||
if ("table".equals(type) && !name.startsWith("sqlite_")) {
|
||||
Log.i(TAG, "Dropping table: " + name);
|
||||
db.execSQL("DROP TABLE IF EXISTS " + name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,13 @@ import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.glide.BadgeSpriteTransformation
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
|
||||
import org.thoughtcrime.securesms.glide.GiftBadgeModel
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.ScreenDensity
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil
|
||||
import java.lang.IllegalArgumentException
|
||||
|
||||
class BadgeImageView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
@@ -78,6 +80,22 @@ class BadgeImageView @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun setGiftBadge(badge: GiftBadge?, glideRequests: GlideRequests) {
|
||||
if (badge != null) {
|
||||
glideRequests
|
||||
.load(GiftBadgeModel(badge))
|
||||
.downsample(DownsampleStrategy.NONE)
|
||||
.transform(BadgeSpriteTransformation(BadgeSpriteTransformation.Size.fromInteger(badgeSize), ScreenDensity.getBestDensityBucketForDevice(), ThemeUtil.isDarkTheme(context)))
|
||||
.into(this)
|
||||
|
||||
isClickable = true
|
||||
} else {
|
||||
glideRequests
|
||||
.clear(this)
|
||||
clearDrawable()
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearDrawable() {
|
||||
setImageDrawable(null)
|
||||
isClickable = false
|
||||
|
||||
@@ -9,13 +9,13 @@ import com.google.android.flexbox.FlexboxLayoutManager
|
||||
import com.google.android.flexbox.JustifyContent
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.protocol.util.Pair
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.models.Badge.Category.Companion.fromCode
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BadgeList
|
||||
import org.thoughtcrime.securesms.util.ScreenDensity
|
||||
import org.whispersystems.libsignal.util.Pair
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
|
||||
import java.math.BigDecimal
|
||||
import java.sql.Timestamp
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.thoughtcrime.securesms.badges.gifts
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import org.thoughtcrime.securesms.badges.gifts.ExpiredGiftSheetConfiguration.forExpiredBadge
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
|
||||
/**
|
||||
* Displays expired gift information and gives the user the option to start a recurring monthly donation.
|
||||
*/
|
||||
class ExpiredGiftSheet : DSLSettingsBottomSheetFragment() {
|
||||
|
||||
companion object {
|
||||
private const val ARG_BADGE = "arg.badge"
|
||||
|
||||
fun show(fragmentManager: FragmentManager, badge: Badge) {
|
||||
ExpiredGiftSheet().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(ARG_BADGE, badge)
|
||||
}
|
||||
}.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
}
|
||||
}
|
||||
|
||||
private val badge: Badge
|
||||
get() = requireArguments().getParcelable(ARG_BADGE)!!
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
ExpiredGiftSheetConfiguration.register(adapter)
|
||||
adapter.submitList(
|
||||
configure {
|
||||
forExpiredBadge(
|
||||
badge = badge,
|
||||
onMakeAMonthlyDonation = {
|
||||
requireListener<Callback>().onMakeAMonthlyDonation()
|
||||
},
|
||||
onNotNow = {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
)
|
||||
}.toMappingModelList()
|
||||
)
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onMakeAMonthlyDonation()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package org.thoughtcrime.securesms.badges.gifts
|
||||
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.models.BadgeDisplay112
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
|
||||
/**
|
||||
* Contains shared DSL layout for expired gifts, creatable using a GiftBadge or a Badge.
|
||||
*/
|
||||
object ExpiredGiftSheetConfiguration {
|
||||
fun register(mappingAdapter: MappingAdapter) {
|
||||
BadgeDisplay112.register(mappingAdapter)
|
||||
}
|
||||
|
||||
fun DSLConfiguration.forExpiredBadge(badge: Badge, onMakeAMonthlyDonation: () -> Unit, onNotNow: () -> Unit) {
|
||||
customPref(BadgeDisplay112.Model(badge, withDisplayText = false))
|
||||
expiredSheet(onMakeAMonthlyDonation, onNotNow)
|
||||
}
|
||||
|
||||
fun DSLConfiguration.forExpiredGiftBadge(giftBadge: GiftBadge, onMakeAMonthlyDonation: () -> Unit, onNotNow: () -> Unit) {
|
||||
customPref(BadgeDisplay112.GiftModel(giftBadge))
|
||||
expiredSheet(onMakeAMonthlyDonation, onNotNow)
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.expiredSheet(onMakeAMonthlyDonation: () -> Unit, onNotNow: () -> Unit) {
|
||||
textPref(
|
||||
title = DSLSettingsText.from(
|
||||
stringId = R.string.ExpiredGiftSheetConfiguration__your_gift_badge_has_expired,
|
||||
DSLSettingsText.CenterModifier, DSLSettingsText.Title2BoldModifier
|
||||
)
|
||||
)
|
||||
|
||||
textPref(
|
||||
title = DSLSettingsText.from(
|
||||
stringId = R.string.ExpiredGiftSheetConfiguration__your_gift_badge_has_expired_and_is,
|
||||
DSLSettingsText.CenterModifier
|
||||
)
|
||||
)
|
||||
|
||||
if (SignalStore.donationsValues().isLikelyASustainer()) {
|
||||
primaryButton(
|
||||
text = DSLSettingsText.from(
|
||||
stringId = android.R.string.ok
|
||||
),
|
||||
onClick = {
|
||||
onNotNow()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
textPref(
|
||||
title = DSLSettingsText.from(
|
||||
stringId = R.string.ExpiredGiftSheetConfiguration__to_continue,
|
||||
DSLSettingsText.CenterModifier
|
||||
)
|
||||
)
|
||||
|
||||
primaryButton(
|
||||
text = DSLSettingsText.from(
|
||||
stringId = R.string.ExpiredGiftSheetConfiguration__make_a_monthly_donation
|
||||
),
|
||||
onClick = {
|
||||
onMakeAMonthlyDonation()
|
||||
}
|
||||
)
|
||||
|
||||
secondaryButtonNoOutline(
|
||||
text = DSLSettingsText.from(
|
||||
stringId = R.string.ExpiredGiftSheetConfiguration__not_now
|
||||
),
|
||||
onClick = {
|
||||
onNotNow()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package org.thoughtcrime.securesms.badges.gifts
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.util.AttributeSet
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.use
|
||||
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
|
||||
/**
|
||||
* Displays a gift badge sent to or received from a user, and allows the user to
|
||||
* perform an action based off the badge's redemption state.
|
||||
*/
|
||||
class GiftMessageView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : FrameLayout(context, attrs) {
|
||||
init {
|
||||
inflate(context, R.layout.gift_message_view, this)
|
||||
}
|
||||
|
||||
private val badgeView: BadgeImageView = findViewById(R.id.gift_message_view_badge)
|
||||
private val titleView: TextView = findViewById(R.id.gift_message_view_title)
|
||||
private val descriptionView: TextView = findViewById(R.id.gift_message_view_description)
|
||||
private val actionView: MaterialButton = findViewById(R.id.gift_message_view_action)
|
||||
|
||||
init {
|
||||
context.obtainStyledAttributes(attrs, R.styleable.GiftMessageView).use {
|
||||
val textColor = it.getColor(R.styleable.GiftMessageView_giftMessageView__textColor, Color.RED)
|
||||
titleView.setTextColor(textColor)
|
||||
descriptionView.setTextColor(textColor)
|
||||
|
||||
val buttonTextColor = it.getColor(R.styleable.GiftMessageView_giftMessageView__buttonTextColor, Color.RED)
|
||||
actionView.setTextColor(buttonTextColor)
|
||||
actionView.iconTint = ColorStateList.valueOf(buttonTextColor)
|
||||
|
||||
val buttonBackgroundTint = it.getColor(R.styleable.GiftMessageView_giftMessageView__buttonBackgroundTint, Color.RED)
|
||||
actionView.backgroundTintList = ColorStateList.valueOf(buttonBackgroundTint)
|
||||
}
|
||||
}
|
||||
|
||||
fun setGiftBadge(glideRequests: GlideRequests, giftBadge: GiftBadge, isOutgoing: Boolean, callback: Callback) {
|
||||
titleView.setText(R.string.GiftMessageView__gift_badge)
|
||||
descriptionView.text = resources.getQuantityString(R.plurals.GiftMessageView__lasts_for_d_months, 1, 1)
|
||||
actionView.icon = null
|
||||
actionView.setOnClickListener { callback.onViewGiftBadgeClicked() }
|
||||
actionView.isEnabled = true
|
||||
|
||||
if (isOutgoing) {
|
||||
actionView.setText(R.string.GiftMessageView__view)
|
||||
} else {
|
||||
when (giftBadge.redemptionState) {
|
||||
GiftBadge.RedemptionState.REDEEMED -> {
|
||||
stopAnimationIfNeeded()
|
||||
actionView.setIconResource(R.drawable.ic_check_circle_24)
|
||||
}
|
||||
GiftBadge.RedemptionState.STARTED -> actionView.icon = CircularProgressDrawable(context).apply {
|
||||
actionView.isEnabled = false
|
||||
setColorSchemeColors(ContextCompat.getColor(context, R.color.core_ultramarine))
|
||||
strokeWidth = DimensionUnit.DP.toPixels(2f)
|
||||
start()
|
||||
}
|
||||
else -> {
|
||||
stopAnimationIfNeeded()
|
||||
actionView.icon = null
|
||||
}
|
||||
}
|
||||
|
||||
actionView.setText(
|
||||
when (giftBadge.redemptionState ?: GiftBadge.RedemptionState.UNRECOGNIZED) {
|
||||
GiftBadge.RedemptionState.PENDING -> R.string.GiftMessageView__redeem
|
||||
GiftBadge.RedemptionState.STARTED -> R.string.GiftMessageView__redeeming
|
||||
GiftBadge.RedemptionState.REDEEMED -> R.string.GiftMessageView__redeemed
|
||||
GiftBadge.RedemptionState.FAILED -> R.string.GiftMessageView__redeem
|
||||
GiftBadge.RedemptionState.UNRECOGNIZED -> R.string.GiftMessageView__redeem
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
badgeView.setGiftBadge(giftBadge, glideRequests)
|
||||
}
|
||||
|
||||
fun onGiftNotOpened() {
|
||||
actionView.isClickable = false
|
||||
}
|
||||
|
||||
fun onGiftOpened() {
|
||||
actionView.isClickable = true
|
||||
}
|
||||
|
||||
private fun stopAnimationIfNeeded() {
|
||||
val icon = actionView.icon
|
||||
if (icon is CircularProgressDrawable) {
|
||||
icon.stop()
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onViewGiftBadgeClicked()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.thoughtcrime.securesms.badges.gifts
|
||||
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
|
||||
/**
|
||||
* Helper object for Gift badges
|
||||
*/
|
||||
object Gifts {
|
||||
|
||||
/**
|
||||
* Request Code for getting token from Google Pay
|
||||
*/
|
||||
const val GOOGLE_PAY_REQUEST_CODE = 3000
|
||||
|
||||
/**
|
||||
* Creates an OutgoingSecureMediaMessage which contains the given gift badge.
|
||||
*/
|
||||
fun createOutgoingGiftMessage(
|
||||
recipient: Recipient,
|
||||
giftBadge: GiftBadge,
|
||||
sentTimestamp: Long,
|
||||
expiresIn: Long
|
||||
): OutgoingMediaMessage {
|
||||
return OutgoingSecureMediaMessage(
|
||||
recipient,
|
||||
Base64.encodeBytes(giftBadge.toByteArray()),
|
||||
listOf(),
|
||||
sentTimestamp,
|
||||
ThreadDatabase.DistributionTypes.CONVERSATION,
|
||||
expiresIn,
|
||||
false,
|
||||
StoryType.NONE,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
listOf(),
|
||||
listOf(),
|
||||
listOf(),
|
||||
giftBadge
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.thoughtcrime.securesms.badges.gifts
|
||||
|
||||
import org.thoughtcrime.securesms.util.Projection
|
||||
|
||||
/**
|
||||
* Notes that a given item can have a gift box drawn over it.
|
||||
*/
|
||||
interface OpenableGift {
|
||||
/**
|
||||
* Returns a projection to draw a top, or null to not do so.
|
||||
*/
|
||||
fun getOpenableGiftProjection(): Projection?
|
||||
|
||||
/**
|
||||
* Returns a unique id assosicated with this gift.
|
||||
*/
|
||||
fun getGiftId(): Long
|
||||
|
||||
/**
|
||||
* Registers a callback to start the open animation
|
||||
*/
|
||||
fun setOpenGiftCallback(openGift: (OpenableGift) -> Unit)
|
||||
|
||||
/**
|
||||
* Clears any callback created to start the open animation
|
||||
*/
|
||||
fun clearOpenGiftCallback()
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
package org.thoughtcrime.securesms.badges.gifts
|
||||
|
||||
import android.animation.FloatEvaluator
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.provider.Settings
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.withSave
|
||||
import androidx.core.graphics.withTranslation
|
||||
import androidx.core.view.children
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.Projection
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.max
|
||||
import kotlin.math.sin
|
||||
|
||||
/**
|
||||
* Controls the gift box top and related animations for Gift bubbles.
|
||||
*/
|
||||
class OpenableGiftItemDecoration(context: Context) : RecyclerView.ItemDecoration(), DefaultLifecycleObserver {
|
||||
|
||||
private val animatorDurationScale = Settings.Global.getFloat(context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f)
|
||||
private val messageIdsShakenThisSession = mutableSetOf<Long>()
|
||||
private val messageIdsOpenedThisSession = mutableSetOf<Long>()
|
||||
private val animationState = mutableMapOf<Long, GiftAnimationState>()
|
||||
|
||||
private val rect = RectF()
|
||||
private val lineWidth = DimensionUnit.DP.toPixels(24f).toInt()
|
||||
|
||||
private val boxPaint = Paint().apply {
|
||||
isAntiAlias = true
|
||||
color = ContextCompat.getColor(context, R.color.core_ultramarine)
|
||||
}
|
||||
|
||||
private val ribbonPaint = Paint().apply {
|
||||
isAntiAlias = true
|
||||
color = Color.WHITE
|
||||
}
|
||||
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
super.onDestroy(owner)
|
||||
animationState.clear()
|
||||
}
|
||||
|
||||
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
var needsInvalidation = false
|
||||
val openableChildren = parent.children.filterIsInstance(OpenableGift::class.java)
|
||||
|
||||
val deadKeys = animationState.keys.filterNot { giftId -> openableChildren.any { it.getGiftId() == giftId } }
|
||||
deadKeys.forEach {
|
||||
animationState.remove(it)
|
||||
}
|
||||
|
||||
val notAnimated = openableChildren.filterNot { animationState.containsKey(it.getGiftId()) }
|
||||
|
||||
notAnimated.filterNot { messageIdsOpenedThisSession.contains(it.getGiftId()) }.forEach { child ->
|
||||
val projection = child.getOpenableGiftProjection()
|
||||
if (projection != null) {
|
||||
if (messageIdsShakenThisSession.contains(child.getGiftId())) {
|
||||
child.setOpenGiftCallback {
|
||||
child.clearOpenGiftCallback()
|
||||
val proj = it.getOpenableGiftProjection()
|
||||
if (proj != null) {
|
||||
messageIdsOpenedThisSession.add(it.getGiftId())
|
||||
startOpenAnimation(it)
|
||||
parent.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
drawGiftBox(c, projection)
|
||||
drawGiftBow(c, projection)
|
||||
} else {
|
||||
messageIdsShakenThisSession.add(child.getGiftId())
|
||||
startShakeAnimation(child)
|
||||
|
||||
drawGiftBox(c, projection)
|
||||
drawGiftBow(c, projection)
|
||||
|
||||
needsInvalidation = true
|
||||
}
|
||||
|
||||
projection.release()
|
||||
}
|
||||
}
|
||||
|
||||
openableChildren.filter { animationState.containsKey(it.getGiftId()) }.forEach { child ->
|
||||
val runningAnimation = animationState[child.getGiftId()]!!
|
||||
c.withSave {
|
||||
val isThisAnimationRunning = runningAnimation.update(
|
||||
animatorDurationScale = animatorDurationScale,
|
||||
canvas = c,
|
||||
drawBox = this@OpenableGiftItemDecoration::drawGiftBox,
|
||||
drawBow = this@OpenableGiftItemDecoration::drawGiftBow
|
||||
)
|
||||
|
||||
if (!isThisAnimationRunning) {
|
||||
animationState.remove(child.getGiftId())
|
||||
}
|
||||
|
||||
needsInvalidation = true
|
||||
}
|
||||
}
|
||||
|
||||
if (needsInvalidation) {
|
||||
parent.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawGiftBox(canvas: Canvas, projection: Projection) {
|
||||
canvas.drawPath(projection.path, boxPaint)
|
||||
|
||||
rect.set(
|
||||
projection.x + (projection.width / 2) - lineWidth / 2,
|
||||
projection.y,
|
||||
projection.x + (projection.width / 2) + lineWidth / 2,
|
||||
projection.y + projection.height
|
||||
)
|
||||
|
||||
canvas.drawRect(rect, ribbonPaint)
|
||||
|
||||
rect.set(
|
||||
projection.x,
|
||||
projection.y + (projection.height / 2) - lineWidth / 2,
|
||||
projection.x + projection.width,
|
||||
projection.y + (projection.height / 2) + lineWidth / 2
|
||||
)
|
||||
|
||||
canvas.drawRect(rect, ribbonPaint)
|
||||
}
|
||||
|
||||
private fun drawGiftBow(canvas: Canvas, projection: Projection) {
|
||||
rect.set(
|
||||
projection.x + (projection.width / 2) - lineWidth,
|
||||
projection.y + (projection.height / 2) - lineWidth,
|
||||
projection.x + (projection.width / 2) + lineWidth,
|
||||
projection.y + (projection.height / 2) + lineWidth
|
||||
)
|
||||
|
||||
canvas.drawRect(rect, ribbonPaint)
|
||||
}
|
||||
|
||||
private fun startShakeAnimation(child: OpenableGift) {
|
||||
animationState[child.getGiftId()] = GiftAnimationState.ShakeAnimationState(child, System.currentTimeMillis())
|
||||
}
|
||||
|
||||
private fun startOpenAnimation(child: OpenableGift) {
|
||||
animationState[child.getGiftId()] = GiftAnimationState.OpenAnimationState(child, System.currentTimeMillis())
|
||||
}
|
||||
|
||||
sealed class GiftAnimationState(val openableGift: OpenableGift, val startTime: Long) {
|
||||
|
||||
/**
|
||||
* Shakes the gift box to the left and right, slightly revealing the contents underneath.
|
||||
* Uses a lag value to keep the bow one "frame" behind the box, to give it the effect of
|
||||
* following behind.
|
||||
*/
|
||||
class ShakeAnimationState(openableGift: OpenableGift, startTime: Long) : GiftAnimationState(openableGift, startTime) {
|
||||
override fun update(canvas: Canvas, projection: Projection, progress: Float, lastFrameProgress: Float, drawBox: (Canvas, Projection) -> Unit, drawBow: (Canvas, Projection) -> Unit) {
|
||||
canvas.withTranslation(x = getTranslation(progress).toFloat()) {
|
||||
drawBox(canvas, projection)
|
||||
}
|
||||
|
||||
canvas.withTranslation(x = getTranslation(lastFrameProgress).toFloat()) {
|
||||
drawBow(canvas, projection)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTranslation(progress: Float): Double {
|
||||
val interpolated = INTERPOLATOR.getInterpolation(progress)
|
||||
val evaluated = EVALUATOR.evaluate(interpolated, 0f, 360f)
|
||||
|
||||
return 0.25f * sin(4 * evaluated * PI / 180f) * 180f / PI
|
||||
}
|
||||
}
|
||||
|
||||
class OpenAnimationState(openableGift: OpenableGift, startTime: Long) : GiftAnimationState(openableGift, startTime) {
|
||||
override fun update(canvas: Canvas, projection: Projection, progress: Float, lastFrameProgress: Float, drawBox: (Canvas, Projection) -> Unit, drawBow: (Canvas, Projection) -> Unit) {
|
||||
val interpolatedProgress = INTERPOLATOR.getInterpolation(progress)
|
||||
val evaluatedValue = EVALUATOR.evaluate(interpolatedProgress, 0f, DimensionUnit.DP.toPixels(300f))
|
||||
|
||||
canvas.translate(evaluatedValue, -evaluatedValue)
|
||||
|
||||
drawBox(canvas, projection)
|
||||
drawBow(canvas, projection)
|
||||
}
|
||||
}
|
||||
|
||||
fun update(animatorDurationScale: Float, canvas: Canvas, drawBox: (Canvas, Projection) -> Unit, drawBow: (Canvas, Projection) -> Unit): Boolean {
|
||||
val projection = openableGift.getOpenableGiftProjection() ?: return false
|
||||
|
||||
if (animatorDurationScale <= 0f) {
|
||||
update(canvas, projection, 0f, 0f, drawBox, drawBow)
|
||||
projection.release()
|
||||
return false
|
||||
}
|
||||
|
||||
val currentFrameTime = System.currentTimeMillis()
|
||||
val lastFrameProgress = max(0f, (currentFrameTime - startTime - ONE_FRAME_RELATIVE_TO_30_FPS_MILLIS) / (DURATION_MILLIS.toFloat() * animatorDurationScale))
|
||||
val progress = (currentFrameTime - startTime) / (DURATION_MILLIS.toFloat() * animatorDurationScale)
|
||||
|
||||
if (progress > 1f) {
|
||||
update(canvas, projection, 1f, 1f, drawBox, drawBow)
|
||||
projection.release()
|
||||
return false
|
||||
}
|
||||
|
||||
update(canvas, projection, progress, lastFrameProgress, drawBox, drawBow)
|
||||
projection.release()
|
||||
return true
|
||||
}
|
||||
|
||||
protected abstract fun update(
|
||||
canvas: Canvas,
|
||||
projection: Projection,
|
||||
progress: Float,
|
||||
lastFrameProgress: Float,
|
||||
drawBox: (Canvas, Projection) -> Unit,
|
||||
drawBow: (Canvas, Projection) -> Unit
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val INTERPOLATOR = AccelerateDecelerateInterpolator()
|
||||
private val EVALUATOR = FloatEvaluator()
|
||||
|
||||
private const val DURATION_MILLIS = 1000
|
||||
private const val ONE_FRAME_RELATIVE_TO_30_FPS_MILLIS = 33
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.thoughtcrime.securesms.badges.gifts.flow
|
||||
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
|
||||
/**
|
||||
* Convenience wrapper for a gift at a particular price point.
|
||||
*/
|
||||
data class Gift(val level: Long, val price: FiatMoney)
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.thoughtcrime.securesms.badges.gifts.flow
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import io.reactivex.rxjava3.subjects.Subject
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.FragmentWrapperActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository
|
||||
|
||||
/**
|
||||
* Activity which houses the gift flow.
|
||||
*/
|
||||
class GiftFlowActivity : FragmentWrapperActivity(), DonationPaymentComponent {
|
||||
|
||||
override val donationPaymentRepository: DonationPaymentRepository by lazy { DonationPaymentRepository(this) }
|
||||
|
||||
override val googlePayResultPublisher: Subject<DonationPaymentComponent.GooglePayResult> = PublishSubject.create()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
super.onCreate(savedInstanceState, ready)
|
||||
onBackPressedDispatcher.addCallback(this, OnBackPressed())
|
||||
}
|
||||
|
||||
override fun getFragment(): Fragment {
|
||||
return NavHostFragment.create(R.navigation.gift_flow)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
googlePayResultPublisher.onNext(DonationPaymentComponent.GooglePayResult(requestCode, resultCode, data))
|
||||
}
|
||||
|
||||
private inner class OnBackPressed : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (!findNavController(R.id.fragment_container).popBackStack()) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package org.thoughtcrime.securesms.badges.gifts.flow
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.view.KeyEvent
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.InputAwareLayout
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiEventListener
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorDialogs
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.RecipientPreference
|
||||
import org.thoughtcrime.securesms.components.settings.models.TextInput
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents
|
||||
import org.thoughtcrime.securesms.keyboard.KeyboardPage
|
||||
import org.thoughtcrime.securesms.keyboard.KeyboardPagerViewModel
|
||||
import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageFragment
|
||||
import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
|
||||
/**
|
||||
* Allows the user to confirm details about a gift, add a message, and finally make a payment.
|
||||
*/
|
||||
class GiftFlowConfirmationFragment :
|
||||
DSLSettingsFragment(
|
||||
titleId = R.string.GiftFlowConfirmationFragment__confirm_gift,
|
||||
layoutId = R.layout.gift_flow_confirmation_fragment
|
||||
),
|
||||
EmojiKeyboardPageFragment.Callback,
|
||||
EmojiEventListener,
|
||||
EmojiSearchFragment.Callback {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(GiftFlowConfirmationFragment::class.java)
|
||||
}
|
||||
|
||||
private val viewModel: GiftFlowViewModel by viewModels(
|
||||
ownerProducer = { requireActivity() }
|
||||
)
|
||||
|
||||
private val keyboardPagerViewModel: KeyboardPagerViewModel by viewModels(
|
||||
ownerProducer = { requireActivity() }
|
||||
)
|
||||
|
||||
private lateinit var inputAwareLayout: InputAwareLayout
|
||||
private lateinit var emojiKeyboard: MediaKeyboard
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
private var errorDialog: DialogInterface? = null
|
||||
private lateinit var processingDonationPaymentDialog: AlertDialog
|
||||
private lateinit var donationPaymentComponent: DonationPaymentComponent
|
||||
private lateinit var textInputViewHolder: TextInput.MultilineViewHolder
|
||||
|
||||
private val eventPublisher = PublishSubject.create<TextInput.TextInputEvent>()
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
RecipientPreference.register(adapter)
|
||||
GiftRowItem.register(adapter)
|
||||
|
||||
keyboardPagerViewModel.setOnlyPage(KeyboardPage.EMOJI)
|
||||
|
||||
donationPaymentComponent = requireListener()
|
||||
|
||||
processingDonationPaymentDialog = MaterialAlertDialogBuilder(requireContext())
|
||||
.setView(R.layout.processing_payment_dialog)
|
||||
.setCancelable(false)
|
||||
.create()
|
||||
|
||||
inputAwareLayout = requireView().findViewById(R.id.input_aware_layout)
|
||||
emojiKeyboard = requireView().findViewById(R.id.emoji_drawer)
|
||||
|
||||
emojiKeyboard.setFragmentManager(childFragmentManager)
|
||||
|
||||
val googlePayButton = requireView().findViewById<GooglePayButton>(R.id.google_pay_button)
|
||||
googlePayButton.setOnGooglePayClickListener {
|
||||
viewModel.requestTokenFromGooglePay(getString(R.string.preferences__one_time))
|
||||
}
|
||||
|
||||
val textInput = requireView().findViewById<FrameLayout>(R.id.text_input)
|
||||
val emojiToggle = textInput.findViewById<ImageView>(R.id.emoji_toggle)
|
||||
textInputViewHolder = TextInput.MultilineViewHolder(textInput, eventPublisher)
|
||||
textInputViewHolder.onAttachedToWindow()
|
||||
|
||||
inputAwareLayout.addOnKeyboardShownListener {
|
||||
inputAwareLayout.hideAttachedInput(true)
|
||||
emojiToggle.setImageResource(R.drawable.ic_emoji_smiley_24)
|
||||
}
|
||||
|
||||
requireActivity().onBackPressedDispatcher.addCallback(
|
||||
viewLifecycleOwner,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (inputAwareLayout.isInputOpen) {
|
||||
inputAwareLayout.hideAttachedInput(true)
|
||||
emojiToggle.setImageResource(R.drawable.ic_emoji_smiley_24)
|
||||
} else {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
lifecycleDisposable += viewModel.state.observeOn(AndroidSchedulers.mainThread()).subscribe { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
|
||||
if (state.stage == GiftFlowState.Stage.PAYMENT_PIPELINE) {
|
||||
processingDonationPaymentDialog.show()
|
||||
} else {
|
||||
processingDonationPaymentDialog.hide()
|
||||
}
|
||||
|
||||
textInputViewHolder.bind(
|
||||
TextInput.MultilineModel(
|
||||
text = state.additionalMessage,
|
||||
hint = DSLSettingsText.from(R.string.GiftFlowConfirmationFragment__add_a_message),
|
||||
onTextChanged = {
|
||||
viewModel.setAdditionalMessage(it)
|
||||
},
|
||||
onEmojiToggleClicked = {
|
||||
if (inputAwareLayout.isKeyboardOpen || (!inputAwareLayout.isKeyboardOpen && !inputAwareLayout.isInputOpen)) {
|
||||
inputAwareLayout.show(it, emojiKeyboard)
|
||||
emojiToggle.setImageResource(R.drawable.ic_keyboard_24)
|
||||
} else {
|
||||
inputAwareLayout.showSoftkey(it)
|
||||
emojiToggle.setImageResource(R.drawable.ic_emoji_smiley_24)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
||||
|
||||
lifecycleDisposable += DonationError
|
||||
.getErrorsForSource(DonationErrorSource.GIFT)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { donationError ->
|
||||
onPaymentError(donationError)
|
||||
}
|
||||
|
||||
lifecycleDisposable += viewModel.events.observeOn(AndroidSchedulers.mainThread()).subscribe { donationEvent ->
|
||||
when (donationEvent) {
|
||||
is DonationEvent.PaymentConfirmationSuccess -> onPaymentConfirmed()
|
||||
DonationEvent.RequestTokenSuccess -> Log.i(TAG, "Successfully got request token from Google Pay")
|
||||
DonationEvent.SubscriptionCancelled -> Unit
|
||||
is DonationEvent.SubscriptionCancellationFailed -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleDisposable += donationPaymentComponent.googlePayResultPublisher.subscribe {
|
||||
viewModel.onActivityResult(it.requestCode, it.resultCode, it.data)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
textInputViewHolder.onDetachedFromWindow()
|
||||
processingDonationPaymentDialog.dismiss()
|
||||
}
|
||||
|
||||
private fun getConfiguration(giftFlowState: GiftFlowState): DSLConfiguration {
|
||||
return configure {
|
||||
if (giftFlowState.giftBadge != null) {
|
||||
giftFlowState.giftPrices[giftFlowState.currency]?.let {
|
||||
customPref(
|
||||
GiftRowItem.Model(
|
||||
giftBadge = giftFlowState.giftBadge,
|
||||
price = it
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sectionHeaderPref(R.string.GiftFlowConfirmationFragment__send_to)
|
||||
|
||||
customPref(
|
||||
RecipientPreference.Model(
|
||||
recipient = giftFlowState.recipient!!
|
||||
)
|
||||
)
|
||||
|
||||
textPref(
|
||||
summary = DSLSettingsText.from(R.string.GiftFlowConfirmationFragment__your_gift_will_be_sent_in)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onPaymentConfirmed() {
|
||||
val mainActivityIntent = MainActivity.clearTop(requireContext())
|
||||
val conversationIntent = ConversationIntents
|
||||
.createBuilder(requireContext(), viewModel.snapshot.recipient!!.id, -1L)
|
||||
.withGiftBadge(viewModel.snapshot.giftBadge!!)
|
||||
.build()
|
||||
|
||||
requireActivity().startActivities(arrayOf(mainActivityIntent, conversationIntent))
|
||||
}
|
||||
|
||||
private fun onPaymentError(throwable: Throwable?) {
|
||||
Log.w(TAG, "onPaymentError", throwable, true)
|
||||
|
||||
if (errorDialog != null) {
|
||||
Log.i(TAG, "Already displaying an error dialog. Skipping.")
|
||||
return
|
||||
}
|
||||
|
||||
errorDialog = DonationErrorDialogs.show(
|
||||
requireContext(), throwable,
|
||||
object : DonationErrorDialogs.DialogCallback() {
|
||||
override fun onDialogDismissed() {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun openEmojiSearch() {
|
||||
emojiKeyboard.onOpenEmojiSearch()
|
||||
}
|
||||
|
||||
override fun closeEmojiSearch() {
|
||||
emojiKeyboard.onCloseEmojiSearch()
|
||||
}
|
||||
|
||||
override fun onEmojiSelected(emoji: String?) {
|
||||
if (emoji?.isNotEmpty() == true) {
|
||||
eventPublisher.onNext(TextInput.TextInputEvent.OnEmojiEvent(emoji))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onKeyEvent(keyEvent: KeyEvent?) {
|
||||
if (keyEvent != null) {
|
||||
eventPublisher.onNext(TextInput.TextInputEvent.OnKeyEvent(keyEvent))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package org.thoughtcrime.securesms.badges.gifts.flow
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchState
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.SearchConfigurationProvider
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
/**
|
||||
* Allows the user to select a recipient to send a gift to.
|
||||
*/
|
||||
class GiftFlowRecipientSelectionFragment : Fragment(R.layout.gift_flow_recipient_selection_fragment), MultiselectForwardFragment.Callback, SearchConfigurationProvider {
|
||||
|
||||
private val viewModel: GiftFlowViewModel by viewModels(
|
||||
ownerProducer = { requireActivity() }
|
||||
)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val toolbar = view.findViewById<Toolbar>(R.id.toolbar)
|
||||
toolbar.setNavigationOnClickListener { requireActivity().onBackPressed() }
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
childFragmentManager.beginTransaction()
|
||||
.replace(
|
||||
R.id.multiselect_container,
|
||||
MultiselectForwardFragment.create(
|
||||
MultiselectForwardFragmentArgs(
|
||||
canSendToNonPush = false,
|
||||
multiShareArgs = emptyList(),
|
||||
forceDisableAddMessage = true,
|
||||
selectSingleRecipient = true
|
||||
)
|
||||
)
|
||||
)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSearchConfiguration(fragmentManager: FragmentManager, contactSearchState: ContactSearchState): ContactSearchConfiguration {
|
||||
return ContactSearchConfiguration.build {
|
||||
query = contactSearchState.query
|
||||
|
||||
if (query.isNullOrEmpty()) {
|
||||
addSection(
|
||||
ContactSearchConfiguration.Section.Recents(
|
||||
includeHeader = true,
|
||||
mode = ContactSearchConfiguration.Section.Recents.Mode.INDIVIDUALS
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
addSection(
|
||||
ContactSearchConfiguration.Section.Individuals(
|
||||
includeSelf = false,
|
||||
transportType = ContactSearchConfiguration.TransportType.PUSH,
|
||||
includeHeader = true
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinishForwardAction() = Unit
|
||||
|
||||
override fun exitFlow() = Unit
|
||||
|
||||
override fun onSearchInputFocused() = Unit
|
||||
|
||||
override fun setResult(bundle: Bundle) {
|
||||
val parcelableContacts: List<ContactSearchKey.ParcelableRecipientSearchKey> = bundle.getParcelableArrayList(MultiselectForwardFragment.RESULT_SELECTION)!!
|
||||
val contacts = parcelableContacts.map { it.asRecipientSearchKey() }
|
||||
|
||||
if (contacts.isNotEmpty()) {
|
||||
viewModel.setSelectedContact(contacts.first())
|
||||
findNavController().safeNavigate(R.id.action_giftFlowRecipientSelectionFragment_to_giftFlowConfirmationFragment)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getContainer(): ViewGroup = requireView() as ViewGroup
|
||||
|
||||
override fun getDialogBackgroundColor(): Int = Color.TRANSPARENT
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.thoughtcrime.securesms.badges.gifts.flow
|
||||
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.util.PlatformCurrencyUtil
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Repository for grabbing gift badges and supported currency information.
|
||||
*/
|
||||
class GiftFlowRepository {
|
||||
|
||||
fun getGiftBadge(): Single<Pair<Long, Badge>> {
|
||||
return ApplicationDependencies.getDonationsService()
|
||||
.getGiftBadges(Locale.getDefault())
|
||||
.flatMap(ServiceResponse<Map<Long, SignalServiceProfile.Badge>>::flattenResult)
|
||||
.map { gifts -> gifts.map { it.key to Badges.fromServiceBadge(it.value) } }
|
||||
.map { it.first() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun getGiftPricing(): Single<Map<Currency, FiatMoney>> {
|
||||
return ApplicationDependencies.getDonationsService()
|
||||
.giftAmount
|
||||
.subscribeOn(Schedulers.io())
|
||||
.flatMap { it.flattenResult() }
|
||||
.map { result ->
|
||||
result
|
||||
.filter { PlatformCurrencyUtil.getAvailableCurrencyCodes().contains(it.key) }
|
||||
.mapKeys { (code, _) -> Currency.getInstance(code) }
|
||||
.mapValues { (currency, price) -> FiatMoney(price, currency) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package org.thoughtcrime.securesms.badges.gifts.flow
|
||||
|
||||
import android.view.View
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadingCircle
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
/**
|
||||
* Landing fragment for sending gifts.
|
||||
*/
|
||||
class GiftFlowStartFragment : DSLSettingsFragment(
|
||||
layoutId = R.layout.gift_flow_start_fragment
|
||||
) {
|
||||
|
||||
private val viewModel: GiftFlowViewModel by viewModels(
|
||||
ownerProducer = { requireActivity() },
|
||||
factoryProducer = { GiftFlowViewModel.Factory(GiftFlowRepository(), requireListener<DonationPaymentComponent>().donationPaymentRepository) }
|
||||
)
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
CurrencySelection.register(adapter)
|
||||
GiftRowItem.register(adapter)
|
||||
NetworkFailure.register(adapter)
|
||||
IndeterminateLoadingCircle.register(adapter)
|
||||
|
||||
val next = requireView().findViewById<View>(R.id.next)
|
||||
next.setOnClickListener {
|
||||
findNavController().safeNavigate(R.id.action_giftFlowStartFragment_to_giftFlowRecipientSelectionFragment)
|
||||
}
|
||||
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
||||
lifecycleDisposable += viewModel.state.observeOn(AndroidSchedulers.mainThread()).subscribe { state ->
|
||||
next.isEnabled = state.stage == GiftFlowState.Stage.READY
|
||||
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: GiftFlowState): DSLConfiguration {
|
||||
return configure {
|
||||
customPref(
|
||||
CurrencySelection.Model(
|
||||
selectedCurrency = state.currency,
|
||||
isEnabled = state.stage == GiftFlowState.Stage.READY,
|
||||
onClick = {
|
||||
val action = GiftFlowStartFragmentDirections.actionGiftFlowStartFragmentToSetCurrencyFragment(true, viewModel.getSupportedCurrencyCodes().toTypedArray())
|
||||
findNavController().safeNavigate(action)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@Suppress("CascadeIf")
|
||||
if (state.stage == GiftFlowState.Stage.FAILURE) {
|
||||
customPref(
|
||||
NetworkFailure.Model(
|
||||
onRetryClick = {
|
||||
viewModel.retry()
|
||||
}
|
||||
)
|
||||
)
|
||||
} else if (state.stage == GiftFlowState.Stage.INIT) {
|
||||
customPref(IndeterminateLoadingCircle)
|
||||
} else if (state.giftBadge != null) {
|
||||
state.giftPrices[state.currency]?.let {
|
||||
customPref(
|
||||
GiftRowItem.Model(
|
||||
giftBadge = state.giftBadge,
|
||||
price = it
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.thoughtcrime.securesms.badges.gifts.flow
|
||||
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import java.util.Currency
|
||||
|
||||
/**
|
||||
* State maintained by the GiftFlowViewModel
|
||||
*/
|
||||
data class GiftFlowState(
|
||||
val currency: Currency,
|
||||
val giftLevel: Long? = null,
|
||||
val giftBadge: Badge? = null,
|
||||
val giftPrices: Map<Currency, FiatMoney> = emptyMap(),
|
||||
val stage: Stage = Stage.INIT,
|
||||
val recipient: Recipient? = null,
|
||||
val additionalMessage: CharSequence? = null
|
||||
) {
|
||||
enum class Stage {
|
||||
INIT,
|
||||
READY,
|
||||
TOKEN_REQUEST,
|
||||
PAYMENT_PIPELINE,
|
||||
FAILURE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
package org.thoughtcrime.securesms.badges.gifts.flow
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.google.android.gms.wallet.PaymentData
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.GooglePayApi
|
||||
import org.thoughtcrime.securesms.badges.gifts.Gifts
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.InternetConnectionObserver
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import java.util.Currency
|
||||
|
||||
/**
|
||||
* Maintains state as a user works their way through the gift flow.
|
||||
*/
|
||||
class GiftFlowViewModel(
|
||||
val repository: GiftFlowRepository,
|
||||
val donationPaymentRepository: DonationPaymentRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private var giftToPurchase: Gift? = null
|
||||
|
||||
private val store = RxStore(
|
||||
GiftFlowState(
|
||||
currency = SignalStore.donationsValues().getOneTimeCurrency()
|
||||
)
|
||||
)
|
||||
private val disposables = CompositeDisposable()
|
||||
private val eventPublisher: PublishSubject<DonationEvent> = PublishSubject.create()
|
||||
private val networkDisposable: Disposable
|
||||
|
||||
val state: Flowable<GiftFlowState> = store.stateFlowable
|
||||
val events: Observable<DonationEvent> = eventPublisher
|
||||
val snapshot: GiftFlowState get() = store.state
|
||||
|
||||
init {
|
||||
refresh()
|
||||
|
||||
networkDisposable = InternetConnectionObserver
|
||||
.observe()
|
||||
.distinctUntilChanged()
|
||||
.subscribe { isConnected ->
|
||||
if (isConnected) {
|
||||
retry()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun retry() {
|
||||
if (!disposables.isDisposed && store.state.stage == GiftFlowState.Stage.FAILURE) {
|
||||
store.update { it.copy(stage = GiftFlowState.Stage.INIT) }
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
disposables.clear()
|
||||
disposables += SignalStore.donationsValues().observableOneTimeCurrency.subscribe { currency ->
|
||||
store.update {
|
||||
it.copy(
|
||||
currency = currency
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
disposables += repository.getGiftPricing().subscribe { giftPrices ->
|
||||
store.update {
|
||||
it.copy(
|
||||
giftPrices = giftPrices,
|
||||
stage = getLoadState(it, giftPrices = giftPrices)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
disposables += repository.getGiftBadge().subscribeBy(
|
||||
onSuccess = { (giftLevel, giftBadge) ->
|
||||
store.update {
|
||||
it.copy(
|
||||
giftLevel = giftLevel,
|
||||
giftBadge = giftBadge,
|
||||
stage = getLoadState(it, giftBadge = giftBadge)
|
||||
)
|
||||
}
|
||||
},
|
||||
onError = { throwable ->
|
||||
Log.w(TAG, "Could not load gift badge", throwable)
|
||||
store.update {
|
||||
it.copy(
|
||||
stage = GiftFlowState.Stage.FAILURE
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
fun setSelectedContact(selectedContact: ContactSearchKey.RecipientSearchKey) {
|
||||
store.update {
|
||||
it.copy(recipient = Recipient.resolved(selectedContact.recipientId))
|
||||
}
|
||||
}
|
||||
|
||||
fun getSupportedCurrencyCodes(): List<String> {
|
||||
return store.state.giftPrices.keys.map { it.currencyCode }
|
||||
}
|
||||
|
||||
fun requestTokenFromGooglePay(label: String) {
|
||||
val giftLevel = store.state.giftLevel ?: return
|
||||
val giftPrice = store.state.giftPrices[store.state.currency] ?: return
|
||||
|
||||
this.giftToPurchase = Gift(giftLevel, giftPrice)
|
||||
donationPaymentRepository.requestTokenFromGooglePay(giftToPurchase!!.price, label, Gifts.GOOGLE_PAY_REQUEST_CODE)
|
||||
}
|
||||
|
||||
fun onActivityResult(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
data: Intent?
|
||||
) {
|
||||
val gift = giftToPurchase
|
||||
giftToPurchase = null
|
||||
|
||||
val recipient = store.state.recipient?.id
|
||||
|
||||
donationPaymentRepository.onActivityResult(
|
||||
requestCode, resultCode, data, Gifts.GOOGLE_PAY_REQUEST_CODE,
|
||||
object : GooglePayApi.PaymentRequestCallback {
|
||||
override fun onSuccess(paymentData: PaymentData) {
|
||||
if (gift != null && recipient != null) {
|
||||
eventPublisher.onNext(DonationEvent.RequestTokenSuccess)
|
||||
|
||||
store.update { it.copy(stage = GiftFlowState.Stage.PAYMENT_PIPELINE) }
|
||||
|
||||
donationPaymentRepository.continuePayment(gift.price, paymentData, recipient, store.state.additionalMessage?.toString(), gift.level).subscribeBy(
|
||||
onError = { throwable ->
|
||||
store.update { it.copy(stage = GiftFlowState.Stage.READY) }
|
||||
val donationError: DonationError = if (throwable is DonationError) {
|
||||
throwable
|
||||
} else {
|
||||
Log.w(TAG, "Failed to complete payment or redemption", throwable, true)
|
||||
DonationError.genericBadgeRedemptionFailure(DonationErrorSource.GIFT)
|
||||
}
|
||||
DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError)
|
||||
},
|
||||
onComplete = {
|
||||
store.update { it.copy(stage = GiftFlowState.Stage.READY) }
|
||||
eventPublisher.onNext(DonationEvent.PaymentConfirmationSuccess(store.state.giftBadge!!))
|
||||
}
|
||||
)
|
||||
} else {
|
||||
store.update { it.copy(stage = GiftFlowState.Stage.READY) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(googlePayException: GooglePayApi.GooglePayException) {
|
||||
store.update { it.copy(stage = GiftFlowState.Stage.READY) }
|
||||
DonationError.routeDonationError(ApplicationDependencies.getApplication(), DonationError.getGooglePayRequestTokenError(DonationErrorSource.GIFT, googlePayException))
|
||||
}
|
||||
|
||||
override fun onCancelled() {
|
||||
store.update { it.copy(stage = GiftFlowState.Stage.READY) }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun getLoadState(
|
||||
oldState: GiftFlowState,
|
||||
giftPrices: Map<Currency, FiatMoney>? = null,
|
||||
giftBadge: Badge? = null,
|
||||
): GiftFlowState.Stage {
|
||||
if (oldState.stage != GiftFlowState.Stage.INIT) {
|
||||
return oldState.stage
|
||||
}
|
||||
|
||||
if (giftPrices?.isNotEmpty() == true) {
|
||||
return if (oldState.giftBadge != null) {
|
||||
GiftFlowState.Stage.READY
|
||||
} else {
|
||||
GiftFlowState.Stage.INIT
|
||||
}
|
||||
}
|
||||
|
||||
if (giftBadge != null) {
|
||||
return if (oldState.giftPrices.isNotEmpty()) {
|
||||
GiftFlowState.Stage.READY
|
||||
} else {
|
||||
GiftFlowState.Stage.INIT
|
||||
}
|
||||
}
|
||||
|
||||
return GiftFlowState.Stage.INIT
|
||||
}
|
||||
|
||||
fun setAdditionalMessage(additionalMessage: CharSequence) {
|
||||
store.update { it.copy(additionalMessage = additionalMessage) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(GiftFlowViewModel::class.java)
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val repository: GiftFlowRepository,
|
||||
private val donationPaymentRepository: DonationPaymentRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(
|
||||
GiftFlowViewModel(
|
||||
repository,
|
||||
donationPaymentRepository
|
||||
)
|
||||
) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.thoughtcrime.securesms.badges.gifts.flow
|
||||
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
/**
|
||||
* A line item for gifts, displayed in the Gift flow's start and confirmation fragments.
|
||||
*/
|
||||
object GiftRowItem {
|
||||
fun register(mappingAdapter: MappingAdapter) {
|
||||
mappingAdapter.registerFactory(Model::class.java, LayoutFactory(::ViewHolder, R.layout.subscription_preference))
|
||||
}
|
||||
|
||||
class Model(val giftBadge: Badge, val price: FiatMoney) : MappingModel<Model> {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean = giftBadge.id == newItem.giftBadge.id
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean = giftBadge == newItem.giftBadge && price == newItem.price
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||
|
||||
private val badgeView = itemView.findViewById<BadgeImageView>(R.id.badge)
|
||||
private val titleView = itemView.findViewById<TextView>(R.id.title)
|
||||
private val checkView = itemView.findViewById<View>(R.id.check)
|
||||
private val taglineView = itemView.findViewById<TextView>(R.id.tagline)
|
||||
private val priceView = itemView.findViewById<TextView>(R.id.price)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
checkView.visible = false
|
||||
badgeView.setBadge(model.giftBadge)
|
||||
titleView.text = model.giftBadge.name
|
||||
taglineView.setText(R.string.GiftRowItem__send_a_gift_badge)
|
||||
priceView.text = FiatMoneyUtil.format(
|
||||
context.resources,
|
||||
model.price,
|
||||
FiatMoneyUtil.formatOptions()
|
||||
.trimZerosAfterDecimal()
|
||||
.withDisplayTime(false)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.thoughtcrime.securesms.badges.gifts.flow
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.FrameLayout
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
* Wraps the google pay button in a convenient frame layout.
|
||||
*/
|
||||
class GooglePayButton @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : FrameLayout(context, attrs) {
|
||||
init {
|
||||
inflate(context, R.layout.donate_with_googlepay_button, this)
|
||||
}
|
||||
|
||||
fun setOnGooglePayClickListener(action: () -> Unit) {
|
||||
getChildAt(0).setOnClickListener { action() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package org.thoughtcrime.securesms.badges.gifts.thanks
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.models.BadgePreview
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
|
||||
/**
|
||||
* Displays a "Thank you" message in a conversation when redirected
|
||||
* there after purchasing and sending a gift badge.
|
||||
*/
|
||||
class GiftThanksSheet : DSLSettingsBottomSheetFragment() {
|
||||
|
||||
companion object {
|
||||
private const val ARGS_RECIPIENT_ID = "args.recipient.id"
|
||||
private const val ARGS_BADGE = "args.badge"
|
||||
|
||||
@JvmStatic
|
||||
fun show(fragmentManager: FragmentManager, recipientId: RecipientId, badge: Badge) {
|
||||
GiftThanksSheet().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(ARGS_RECIPIENT_ID, recipientId)
|
||||
putParcelable(ARGS_BADGE, badge)
|
||||
}
|
||||
}.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
}
|
||||
}
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
private val recipientId: RecipientId
|
||||
get() = requireArguments().getParcelable(ARGS_RECIPIENT_ID)!!
|
||||
|
||||
private val badge: Badge
|
||||
get() = requireArguments().getParcelable(ARGS_BADGE)!!
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
BadgePreview.register(adapter)
|
||||
|
||||
lifecycleDisposable += Recipient.observable(recipientId).subscribe {
|
||||
adapter.submitList(getConfiguration(it).toMappingModelList())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfiguration(recipient: Recipient): DSLConfiguration {
|
||||
return configure {
|
||||
textPref(
|
||||
title = DSLSettingsText.from(R.string.SubscribeThanksForYourSupportBottomSheetDialogFragment__thanks_for_your_support, DSLSettingsText.Title2BoldModifier, DSLSettingsText.CenterModifier)
|
||||
)
|
||||
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(getString(R.string.GiftThanksSheet__youve_gifted_a_badge_to_s, recipient.getDisplayName(requireContext())))
|
||||
)
|
||||
|
||||
space(DimensionUnit.DP.toPixels(37f).toInt())
|
||||
|
||||
customPref(
|
||||
BadgePreview.BadgeModel.GiftedBadgeModel(
|
||||
badge = badge,
|
||||
recipient = recipient
|
||||
)
|
||||
)
|
||||
|
||||
space(DimensionUnit.DP.toPixels(60f).toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.thoughtcrime.securesms.badges.gifts.viewgift
|
||||
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Shared repository for getting information about a particular gift.
|
||||
*/
|
||||
class ViewGiftRepository {
|
||||
fun getBadge(giftBadge: GiftBadge): Single<Badge> {
|
||||
val presentation = ReceiptCredentialPresentation(giftBadge.redemptionToken.toByteArray())
|
||||
return ApplicationDependencies
|
||||
.getDonationsService()
|
||||
.getGiftBadge(Locale.getDefault(), presentation.receiptLevel)
|
||||
.flatMap { it.flattenResult() }
|
||||
.map { Badges.fromServiceBadge(it) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun getGiftBadge(messageId: Long): Observable<GiftBadge> {
|
||||
return Observable.create { emitter ->
|
||||
fun refresh() {
|
||||
val record = SignalDatabase.mms.getMessageRecord(messageId)
|
||||
val giftBadge: GiftBadge = (record as MmsMessageRecord).giftBadge!!
|
||||
|
||||
emitter.onNext(giftBadge)
|
||||
}
|
||||
|
||||
val messageObserver = DatabaseObserver.MessageObserver {
|
||||
if (it.mms && messageId == it.id) {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().registerMessageUpdateObserver(messageObserver)
|
||||
emitter.setCancellable {
|
||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageObserver)
|
||||
}
|
||||
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
package org.thoughtcrime.securesms.badges.gifts.viewgift.received
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.LiveDataReactiveStreams
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException
|
||||
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||
import org.thoughtcrime.securesms.badges.gifts.ExpiredGiftSheetConfiguration.forExpiredGiftBadge
|
||||
import org.thoughtcrime.securesms.badges.gifts.viewgift.ViewGiftRepository
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.models.BadgeDisplay112
|
||||
import org.thoughtcrime.securesms.badges.models.BadgeDisplay160
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorDialogs
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadingCircle
|
||||
import org.thoughtcrime.securesms.components.settings.models.OutlinedSwitch
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Handles all interactions for received gift badges.
|
||||
*/
|
||||
class ViewReceivedGiftBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ViewReceivedGiftBottomSheet::class.java)
|
||||
|
||||
private const val ARG_GIFT_BADGE = "arg.gift.badge"
|
||||
private const val ARG_SENT_FROM = "arg.sent.from"
|
||||
private const val ARG_MESSAGE_ID = "arg.message.id"
|
||||
|
||||
@JvmField
|
||||
val REQUEST_KEY: String = TAG
|
||||
|
||||
const val RESULT_NOT_NOW = "result.not.now"
|
||||
|
||||
@JvmStatic
|
||||
fun show(fragmentManager: FragmentManager, messageRecord: MmsMessageRecord) {
|
||||
ViewReceivedGiftBottomSheet().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(ARG_SENT_FROM, messageRecord.recipient.id)
|
||||
putByteArray(ARG_GIFT_BADGE, messageRecord.giftBadge!!.toByteArray())
|
||||
putLong(ARG_MESSAGE_ID, messageRecord.id)
|
||||
}
|
||||
show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
private val sentFrom: RecipientId
|
||||
get() = requireArguments().getParcelable(ARG_SENT_FROM)!!
|
||||
|
||||
private val messageId: Long
|
||||
get() = requireArguments().getLong(ARG_MESSAGE_ID)
|
||||
|
||||
private val viewModel: ViewReceivedGiftViewModel by viewModels(
|
||||
factoryProducer = { ViewReceivedGiftViewModel.Factory(sentFrom, messageId, ViewGiftRepository(), BadgeRepository(requireContext())) }
|
||||
)
|
||||
|
||||
private var errorDialog: DialogInterface? = null
|
||||
private lateinit var progressDialog: AlertDialog
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
BadgeDisplay112.register(adapter)
|
||||
OutlinedSwitch.register(adapter)
|
||||
BadgeDisplay160.register(adapter)
|
||||
IndeterminateLoadingCircle.register(adapter)
|
||||
|
||||
progressDialog = MaterialAlertDialogBuilder(requireContext())
|
||||
.setView(R.layout.redeeming_gift_dialog)
|
||||
.setCancelable(false)
|
||||
.create()
|
||||
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
||||
lifecycleDisposable += DonationError
|
||||
.getErrorsForSource(DonationErrorSource.GIFT_REDEMPTION)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { donationError ->
|
||||
onRedemptionError(donationError)
|
||||
}
|
||||
|
||||
LiveDataReactiveStreams.fromPublisher(viewModel.state).observe(viewLifecycleOwner) { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
progressDialog.hide()
|
||||
}
|
||||
|
||||
private fun onRedemptionError(throwable: Throwable?) {
|
||||
Log.w(TAG, "onRedemptionError", throwable, true)
|
||||
|
||||
if (errorDialog != null) {
|
||||
Log.i(TAG, "Already displaying an error dialog. Skipping.")
|
||||
return
|
||||
}
|
||||
|
||||
errorDialog = DonationErrorDialogs.show(
|
||||
requireContext(), throwable,
|
||||
object : DonationErrorDialogs.DialogCallback() {
|
||||
override fun onDialogDismissed() {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: ViewReceivedGiftState): DSLConfiguration {
|
||||
return configure {
|
||||
if (state.giftBadge == null) {
|
||||
customPref(IndeterminateLoadingCircle)
|
||||
} else if (isGiftBadgeExpired(state.giftBadge)) {
|
||||
forExpiredGiftBadge(
|
||||
giftBadge = state.giftBadge,
|
||||
onMakeAMonthlyDonation = {
|
||||
requireActivity().startActivity(AppSettingsActivity.subscriptions(requireContext()))
|
||||
requireActivity().finish()
|
||||
},
|
||||
onNotNow = {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
if (state.giftBadge.redemptionState == GiftBadge.RedemptionState.STARTED) {
|
||||
progressDialog.show()
|
||||
} else {
|
||||
progressDialog.hide()
|
||||
}
|
||||
|
||||
if (state.recipient != null && !isGiftBadgeRedeemed(state.giftBadge)) {
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
charSequence = requireContext().getString(R.string.ViewReceivedGiftBottomSheet__s_sent_you_a_gift, state.recipient.getShortDisplayName(requireContext())),
|
||||
DSLSettingsText.CenterModifier, DSLSettingsText.Title2BoldModifier
|
||||
)
|
||||
)
|
||||
|
||||
space(DimensionUnit.DP.toPixels(12f).toInt())
|
||||
presentSubheading(state.recipient)
|
||||
|
||||
space(DimensionUnit.DP.toPixels(37f).toInt())
|
||||
}
|
||||
|
||||
if (state.badge != null && state.controlState != null) {
|
||||
presentForUnexpiredGiftBadge(state, state.giftBadge, state.controlState, state.badge)
|
||||
space(DimensionUnit.DP.toPixels(16f).toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.presentSubheading(recipient: Recipient) {
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
charSequence = requireContext().getString(R.string.ViewReceivedGiftBottomSheet__youve_received_a_gift_badge, recipient.getDisplayName(requireContext())),
|
||||
DSLSettingsText.CenterModifier
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.presentForUnexpiredGiftBadge(
|
||||
state: ViewReceivedGiftState,
|
||||
giftBadge: GiftBadge,
|
||||
controlState: ViewReceivedGiftState.ControlState,
|
||||
badge: Badge
|
||||
) {
|
||||
when (giftBadge.redemptionState) {
|
||||
GiftBadge.RedemptionState.REDEEMED -> {
|
||||
customPref(
|
||||
BadgeDisplay160.Model(
|
||||
badge = badge
|
||||
)
|
||||
)
|
||||
|
||||
state.recipient?.run {
|
||||
presentSubheading(this)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
customPref(
|
||||
BadgeDisplay112.Model(
|
||||
badge = badge
|
||||
)
|
||||
)
|
||||
|
||||
customPref(
|
||||
OutlinedSwitch.Model(
|
||||
text = DSLSettingsText.from(
|
||||
when (controlState) {
|
||||
ViewReceivedGiftState.ControlState.DISPLAY -> R.string.SubscribeThanksForYourSupportBottomSheetDialogFragment__display_on_profile
|
||||
ViewReceivedGiftState.ControlState.FEATURE -> R.string.SubscribeThanksForYourSupportBottomSheetDialogFragment__make_featured_badge
|
||||
}
|
||||
),
|
||||
isEnabled = giftBadge.redemptionState != GiftBadge.RedemptionState.STARTED,
|
||||
isChecked = state.getControlChecked(),
|
||||
onClick = {
|
||||
viewModel.setChecked(!it.isChecked)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
if (state.hasOtherBadges && state.displayingOtherBadges) {
|
||||
noPadTextPref(DSLSettingsText.from(R.string.ThanksForYourSupportBottomSheetFragment__when_you_have_more))
|
||||
}
|
||||
|
||||
space(DimensionUnit.DP.toPixels(36f).toInt())
|
||||
|
||||
primaryButton(
|
||||
text = DSLSettingsText.from(R.string.ViewReceivedGiftSheet__redeem),
|
||||
isEnabled = giftBadge.redemptionState != GiftBadge.RedemptionState.STARTED,
|
||||
onClick = {
|
||||
lifecycleDisposable += viewModel.redeem().subscribeBy(
|
||||
onComplete = {
|
||||
dismissAllowingStateLoss()
|
||||
},
|
||||
onError = {
|
||||
onRedemptionError(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
secondaryButtonNoOutline(
|
||||
text = DSLSettingsText.from(R.string.ViewReceivedGiftSheet__not_now),
|
||||
isEnabled = giftBadge.redemptionState != GiftBadge.RedemptionState.STARTED,
|
||||
onClick = {
|
||||
setFragmentResult(
|
||||
REQUEST_KEY,
|
||||
Bundle().apply {
|
||||
putBoolean(RESULT_NOT_NOW, true)
|
||||
}
|
||||
)
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isGiftBadgeRedeemed(giftBadge: GiftBadge): Boolean {
|
||||
return giftBadge.redemptionState == GiftBadge.RedemptionState.REDEEMED
|
||||
}
|
||||
|
||||
private fun isGiftBadgeExpired(giftBadge: GiftBadge): Boolean {
|
||||
return try {
|
||||
val receiptCredentialPresentation = ReceiptCredentialPresentation(giftBadge.redemptionToken.toByteArray())
|
||||
|
||||
receiptCredentialPresentation.receiptExpirationTime <= TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())
|
||||
} catch (e: InvalidInputException) {
|
||||
Log.w(TAG, "Failed to check expiration of given badge.", e)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.thoughtcrime.securesms.badges.gifts.viewgift.received
|
||||
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
data class ViewReceivedGiftState(
|
||||
val recipient: Recipient? = null,
|
||||
val giftBadge: GiftBadge? = null,
|
||||
val badge: Badge? = null,
|
||||
val controlState: ControlState? = null,
|
||||
val hasOtherBadges: Boolean = false,
|
||||
val displayingOtherBadges: Boolean = false,
|
||||
val userCheckSelection: Boolean? = false,
|
||||
val redemptionState: RedemptionState = RedemptionState.NONE
|
||||
) {
|
||||
|
||||
fun getControlChecked(): Boolean {
|
||||
return when {
|
||||
userCheckSelection != null -> userCheckSelection
|
||||
controlState == ControlState.FEATURE -> false
|
||||
!displayingOtherBadges -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
enum class ControlState {
|
||||
DISPLAY,
|
||||
FEATURE
|
||||
}
|
||||
|
||||
enum class RedemptionState {
|
||||
NONE,
|
||||
IN_PROGRESS
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package org.thoughtcrime.securesms.badges.gifts.viewgift.received
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||
import org.thoughtcrime.securesms.badges.gifts.viewgift.ViewGiftRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.jobmanager.JobTracker
|
||||
import org.thoughtcrime.securesms.jobs.DonationReceiptRedemptionJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ViewReceivedGiftViewModel(
|
||||
sentFrom: RecipientId,
|
||||
private val messageId: Long,
|
||||
repository: ViewGiftRepository,
|
||||
val badgeRepository: BadgeRepository
|
||||
) : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ViewReceivedGiftViewModel::class.java)
|
||||
}
|
||||
|
||||
private val store = RxStore(ViewReceivedGiftState())
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
val state: Flowable<ViewReceivedGiftState> = store.stateFlowable
|
||||
|
||||
init {
|
||||
disposables += Recipient.observable(sentFrom).subscribe { recipient ->
|
||||
store.update { it.copy(recipient = recipient) }
|
||||
}
|
||||
|
||||
disposables += repository.getGiftBadge(messageId).subscribe { giftBadge ->
|
||||
store.update {
|
||||
it.copy(giftBadge = giftBadge)
|
||||
}
|
||||
}
|
||||
|
||||
disposables += repository
|
||||
.getGiftBadge(messageId)
|
||||
.firstOrError()
|
||||
.flatMap { repository.getBadge(it) }
|
||||
.subscribe { badge ->
|
||||
val otherBadges = Recipient.self().badges.filterNot { it.id == badge.id }
|
||||
val hasOtherBadges = otherBadges.isNotEmpty()
|
||||
val displayingBadges = SignalStore.donationsValues().getDisplayBadgesOnProfile()
|
||||
val displayingOtherBadges = hasOtherBadges && displayingBadges
|
||||
|
||||
store.update {
|
||||
it.copy(
|
||||
badge = badge,
|
||||
hasOtherBadges = hasOtherBadges,
|
||||
displayingOtherBadges = displayingOtherBadges,
|
||||
controlState = if (displayingBadges) ViewReceivedGiftState.ControlState.FEATURE else ViewReceivedGiftState.ControlState.DISPLAY
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.dispose()
|
||||
}
|
||||
|
||||
fun setChecked(isChecked: Boolean) {
|
||||
store.update { state ->
|
||||
state.copy(
|
||||
userCheckSelection = isChecked
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun redeem(): Completable {
|
||||
val snapshot = store.state
|
||||
|
||||
return if (snapshot.controlState != null && snapshot.badge != null) {
|
||||
if (snapshot.controlState == ViewReceivedGiftState.ControlState.DISPLAY) {
|
||||
badgeRepository.setVisibilityForAllBadges(snapshot.getControlChecked()).andThen(awaitRedemptionCompletion(false))
|
||||
} else if (snapshot.getControlChecked()) {
|
||||
awaitRedemptionCompletion(true)
|
||||
} else {
|
||||
awaitRedemptionCompletion(false)
|
||||
}
|
||||
} else {
|
||||
Completable.error(Exception("Cannot enqueue a redemption without a control state or badge."))
|
||||
}
|
||||
}
|
||||
|
||||
private fun awaitRedemptionCompletion(setAsPrimary: Boolean): Completable {
|
||||
return Completable.create {
|
||||
Log.i(TAG, "Enqueuing gift redemption and awaiting result...", true)
|
||||
|
||||
var finalJobState: JobTracker.JobState? = null
|
||||
val countDownLatch = CountDownLatch(1)
|
||||
|
||||
DonationReceiptRedemptionJob.createJobChainForGift(messageId, setAsPrimary).enqueue { _, state ->
|
||||
if (state.isComplete) {
|
||||
finalJobState = state
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (countDownLatch.await(10, TimeUnit.SECONDS)) {
|
||||
when (finalJobState) {
|
||||
JobTracker.JobState.SUCCESS -> {
|
||||
Log.d(TAG, "Gift redemption job chain succeeded.", true)
|
||||
it.onComplete()
|
||||
}
|
||||
JobTracker.JobState.FAILURE -> {
|
||||
Log.d(TAG, "Gift redemption job chain failed permanently.", true)
|
||||
it.onError(DonationError.genericBadgeRedemptionFailure(DonationErrorSource.GIFT_REDEMPTION))
|
||||
}
|
||||
else -> {
|
||||
Log.w(TAG, "Gift redemption job chain ignored due to in-progress jobs.", true)
|
||||
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Timeout awaiting for gift token redemption and profile refresh", true)
|
||||
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION))
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
Log.w(TAG, "Interrupted awaiting for gift token redemption and profile refresh", true)
|
||||
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val sentFrom: RecipientId,
|
||||
private val messageId: Long,
|
||||
private val repository: ViewGiftRepository,
|
||||
private val badgeRepository: BadgeRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(ViewReceivedGiftViewModel(sentFrom, messageId, repository, badgeRepository)) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package org.thoughtcrime.securesms.badges.gifts.viewgift.sent
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.LiveDataReactiveStreams
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.gifts.viewgift.ViewGiftRepository
|
||||
import org.thoughtcrime.securesms.badges.models.BadgeDisplay112
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
|
||||
/**
|
||||
* Handles all interactions for received gift badges.
|
||||
*/
|
||||
class ViewSentGiftBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
|
||||
companion object {
|
||||
private const val ARG_GIFT_BADGE = "arg.gift.badge"
|
||||
private const val ARG_SENT_TO = "arg.sent.to"
|
||||
|
||||
@JvmStatic
|
||||
fun show(fragmentManager: FragmentManager, messageRecord: MmsMessageRecord) {
|
||||
ViewSentGiftBottomSheet().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(ARG_SENT_TO, messageRecord.recipient.id)
|
||||
putByteArray(ARG_GIFT_BADGE, messageRecord.giftBadge!!.toByteArray())
|
||||
}
|
||||
show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val sentTo: RecipientId
|
||||
get() = requireArguments().getParcelable(ARG_SENT_TO)!!
|
||||
|
||||
private val giftBadge: GiftBadge
|
||||
get() = GiftBadge.parseFrom(requireArguments().getByteArray(ARG_GIFT_BADGE))
|
||||
|
||||
private val viewModel: ViewSentGiftViewModel by viewModels(
|
||||
factoryProducer = { ViewSentGiftViewModel.Factory(sentTo, giftBadge, ViewGiftRepository()) }
|
||||
)
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
BadgeDisplay112.register(adapter)
|
||||
|
||||
LiveDataReactiveStreams.fromPublisher(viewModel.state).observe(viewLifecycleOwner) { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: ViewSentGiftState): DSLConfiguration {
|
||||
return configure {
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
stringId = R.string.ViewSentGiftBottomSheet__thanks_for_your_support,
|
||||
DSLSettingsText.CenterModifier, DSLSettingsText.Title2BoldModifier
|
||||
)
|
||||
)
|
||||
|
||||
space(DimensionUnit.DP.toPixels(8f).toInt())
|
||||
|
||||
if (state.recipient != null) {
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
charSequence = getString(R.string.ViewSentGiftBottomSheet__youve_gifted_a_badge, state.recipient.getDisplayName(requireContext())),
|
||||
DSLSettingsText.CenterModifier
|
||||
)
|
||||
)
|
||||
|
||||
space(DimensionUnit.DP.toPixels(30f).toInt())
|
||||
}
|
||||
|
||||
if (state.badge != null) {
|
||||
customPref(
|
||||
BadgeDisplay112.Model(
|
||||
badge = state.badge
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.thoughtcrime.securesms.badges.gifts.viewgift.sent
|
||||
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
data class ViewSentGiftState(
|
||||
val recipient: Recipient? = null,
|
||||
val badge: Badge? = null
|
||||
)
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.thoughtcrime.securesms.badges.gifts.viewgift.sent
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import org.thoughtcrime.securesms.badges.gifts.viewgift.ViewGiftRepository
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
|
||||
class ViewSentGiftViewModel(
|
||||
sentFrom: RecipientId,
|
||||
giftBadge: GiftBadge,
|
||||
repository: ViewGiftRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val store = RxStore(ViewSentGiftState())
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
val state: Flowable<ViewSentGiftState> = store.stateFlowable
|
||||
|
||||
init {
|
||||
disposables += Recipient.observable(sentFrom).subscribe { recipient ->
|
||||
store.update { it.copy(recipient = recipient) }
|
||||
}
|
||||
|
||||
disposables += repository.getBadge(giftBadge).subscribe { badge ->
|
||||
store.update {
|
||||
it.copy(
|
||||
badge = badge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.dispose()
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val sentFrom: RecipientId,
|
||||
private val giftBadge: GiftBadge,
|
||||
private val repository: ViewGiftRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(ViewSentGiftViewModel(sentFrom, giftBadge, repository)) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,6 +162,7 @@ data class Badge(
|
||||
|
||||
companion object {
|
||||
const val BOOST_BADGE_ID = "BOOST"
|
||||
const val GIFT_BADGE_ID = "GIFT"
|
||||
|
||||
private val SELECTION_CHANGED = Any()
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.thoughtcrime.securesms.badges.models
|
||||
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
/**
|
||||
* Displays a 112dp badge.
|
||||
*/
|
||||
object BadgeDisplay112 {
|
||||
fun register(mappingAdapter: MappingAdapter) {
|
||||
mappingAdapter.registerFactory(Model::class.java, LayoutFactory(::ViewHolder, R.layout.badge_display_112))
|
||||
mappingAdapter.registerFactory(GiftModel::class.java, LayoutFactory(::GiftViewHolder, R.layout.badge_display_112))
|
||||
}
|
||||
|
||||
class Model(val badge: Badge, val withDisplayText: Boolean = true) : MappingModel<Model> {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean = badge.id == newItem.badge.id
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean = badge == newItem.badge && withDisplayText == newItem.withDisplayText
|
||||
}
|
||||
|
||||
class GiftModel(val giftBadge: GiftBadge) : MappingModel<GiftModel> {
|
||||
override fun areItemsTheSame(newItem: GiftModel): Boolean = giftBadge.redemptionToken == newItem.giftBadge.redemptionToken
|
||||
override fun areContentsTheSame(newItem: GiftModel): Boolean = giftBadge == newItem.giftBadge
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||
private val badgeImageView: BadgeImageView = itemView.findViewById(R.id.badge)
|
||||
private val titleView: TextView = itemView.findViewById(R.id.name)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
titleView.text = model.badge.name
|
||||
titleView.visible = model.withDisplayText
|
||||
badgeImageView.setBadge(model.badge)
|
||||
}
|
||||
}
|
||||
|
||||
class GiftViewHolder(itemView: View) : MappingViewHolder<GiftModel>(itemView) {
|
||||
private val badgeImageView: BadgeImageView = itemView.findViewById(R.id.badge)
|
||||
private val titleView: TextView = itemView.findViewById(R.id.name)
|
||||
|
||||
override fun bind(model: GiftModel) {
|
||||
titleView.visible = false
|
||||
badgeImageView.setGiftBadge(model.giftBadge, GlideApp.with(badgeImageView))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.thoughtcrime.securesms.badges.models
|
||||
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
|
||||
/**
|
||||
* Displays a 160dp badge.
|
||||
*/
|
||||
object BadgeDisplay160 {
|
||||
fun register(mappingAdapter: MappingAdapter) {
|
||||
mappingAdapter.registerFactory(Model::class.java, LayoutFactory(::ViewHolder, R.layout.badge_display_160))
|
||||
}
|
||||
|
||||
class Model(val badge: Badge) : MappingModel<Model> {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean = badge.id == newItem.badge.id
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean = badge == newItem.badge
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||
private val badgeImageView: BadgeImageView = itemView.findViewById(R.id.badge)
|
||||
private val titleView: TextView = itemView.findViewById(R.id.name)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
titleView.text = model.badge.name
|
||||
badgeImageView.setBadge(model.badge)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,48 +4,40 @@ import android.view.View
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
|
||||
object BadgePreview {
|
||||
|
||||
fun register(mappingAdapter: MappingAdapter) {
|
||||
mappingAdapter.registerFactory(Model::class.java, LayoutFactory({ ViewHolder(it) }, R.layout.featured_badge_preview_preference))
|
||||
mappingAdapter.registerFactory(SubscriptionModel::class.java, LayoutFactory({ ViewHolder(it) }, R.layout.subscription_flow_badge_preview_preference))
|
||||
mappingAdapter.registerFactory(BadgeModel.FeaturedModel::class.java, LayoutFactory({ ViewHolder(it) }, R.layout.featured_badge_preview_preference))
|
||||
mappingAdapter.registerFactory(BadgeModel.SubscriptionModel::class.java, LayoutFactory({ ViewHolder(it) }, R.layout.subscription_flow_badge_preview_preference))
|
||||
mappingAdapter.registerFactory(BadgeModel.GiftedBadgeModel::class.java, LayoutFactory({ ViewHolder(it) }, R.layout.gift_badge_preview_preference))
|
||||
}
|
||||
|
||||
abstract class BadgeModel<T : BadgeModel<T>> : PreferenceModel<T>() {
|
||||
sealed class BadgeModel<T : BadgeModel<T>> : MappingModel<T> {
|
||||
abstract val badge: Badge?
|
||||
}
|
||||
abstract val recipient: Recipient
|
||||
|
||||
data class Model(override val badge: Badge?) : BadgeModel<Model>() {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return true
|
||||
data class FeaturedModel(override val badge: Badge?) : BadgeModel<FeaturedModel>() {
|
||||
override val recipient: Recipient = Recipient.self()
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||
return super.areContentsTheSame(newItem) && badge == newItem.badge
|
||||
data class SubscriptionModel(override val badge: Badge?) : BadgeModel<SubscriptionModel>() {
|
||||
override val recipient: Recipient = Recipient.self()
|
||||
}
|
||||
|
||||
override fun getChangePayload(newItem: Model): Any? {
|
||||
return Unit
|
||||
}
|
||||
}
|
||||
data class GiftedBadgeModel(override val badge: Badge?, override val recipient: Recipient) : BadgeModel<GiftedBadgeModel>()
|
||||
|
||||
data class SubscriptionModel(override val badge: Badge?) : BadgeModel<SubscriptionModel>() {
|
||||
override fun areItemsTheSame(newItem: SubscriptionModel): Boolean {
|
||||
return true
|
||||
override fun areItemsTheSame(newItem: T): Boolean {
|
||||
return badge?.id == newItem.badge?.id && recipient.id == newItem.recipient.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: SubscriptionModel): Boolean {
|
||||
return super.areContentsTheSame(newItem) && badge == newItem.badge
|
||||
}
|
||||
|
||||
override fun getChangePayload(newItem: SubscriptionModel): Any? {
|
||||
return Unit
|
||||
override fun areContentsTheSame(newItem: T): Boolean {
|
||||
return badge == newItem.badge && recipient.hasSameContent(newItem.recipient)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +48,7 @@ object BadgePreview {
|
||||
|
||||
override fun bind(model: T) {
|
||||
if (payload.isEmpty()) {
|
||||
avatar.setRecipient(Recipient.self())
|
||||
avatar.setRecipient(model.recipient)
|
||||
avatar.disableQuickContact()
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ class CantProcessSubscriptionPaymentBottomSheetDialogFragment : DSLSettingsBotto
|
||||
text = DSLSettingsText.from(R.string.CantProcessSubscriptionPaymentBottomSheetDialogFragment__dont_show_this_again)
|
||||
) {
|
||||
SignalStore.donationsValues().showCantProcessDialog = false
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,9 +41,9 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
|
||||
sectionHeaderPref(
|
||||
DSLSettingsText.from(
|
||||
if (badge.isBoost()) {
|
||||
R.string.ExpiredBadgeBottomSheetDialogFragment__your_badge_has_expired
|
||||
R.string.ExpiredBadgeBottomSheetDialogFragment__boost_badge_expired
|
||||
} else {
|
||||
R.string.ExpiredBadgeBottomSheetDialogFragment__subscription_cancelled
|
||||
R.string.ExpiredBadgeBottomSheetDialogFragment__monthly_donation_cancelled
|
||||
},
|
||||
DSLSettingsText.CenterModifier
|
||||
)
|
||||
@@ -54,11 +54,11 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
|
||||
noPadTextPref(
|
||||
DSLSettingsText.from(
|
||||
if (badge.isBoost()) {
|
||||
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_boost_badge_has_expired)
|
||||
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_boost_badge_has_expired_and)
|
||||
} else if (inactive) {
|
||||
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_sustainer_subscription_was_automatically, badge.name)
|
||||
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_recurring_monthly_donation_was_automatically, badge.name)
|
||||
} else {
|
||||
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_sustainer_subscription_was_canceled)
|
||||
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_recurring_monthly_donation_was_canceled)
|
||||
},
|
||||
DSLSettingsText.CenterModifier
|
||||
)
|
||||
@@ -72,7 +72,7 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
|
||||
if (isLikelyASustainer) {
|
||||
R.string.ExpiredBadgeBottomSheetDialogFragment__you_can_reactivate
|
||||
} else {
|
||||
R.string.ExpiredBadgeBottomSheetDialogFragment__to_continue_supporting_technology
|
||||
R.string.ExpiredBadgeBottomSheetDialogFragment__you_can_keep
|
||||
}
|
||||
} else {
|
||||
R.string.ExpiredBadgeBottomSheetDialogFragment__you_can
|
||||
|
||||
@@ -58,7 +58,7 @@ class SelectFeaturedBadgeFragment : DSLSettingsFragment(
|
||||
}
|
||||
|
||||
val previewView: View = requireView().findViewById(R.id.preview)
|
||||
val previewViewHolder = BadgePreview.ViewHolder<BadgePreview.Model>(previewView)
|
||||
val previewViewHolder = BadgePreview.ViewHolder<BadgePreview.BadgeModel.FeaturedModel>(previewView)
|
||||
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner.lifecycle)
|
||||
lifecycleDisposable += viewModel.events.subscribe { event: SelectFeaturedBadgeEvent ->
|
||||
@@ -79,7 +79,7 @@ class SelectFeaturedBadgeFragment : DSLSettingsFragment(
|
||||
hasBoundPreview = true
|
||||
}
|
||||
|
||||
previewViewHolder.bind(BadgePreview.Model(state.selectedBadge))
|
||||
previewViewHolder.bind(BadgePreview.BadgeModel.FeaturedModel(state.selectedBadge))
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ class BecomeASustainerFragment : DSLSettingsBottomSheetFragment() {
|
||||
|
||||
private fun getConfiguration(state: BecomeASustainerState): DSLConfiguration {
|
||||
return configure {
|
||||
customPref(BadgePreview.Model(badge = state.badge))
|
||||
customPref(BadgePreview.BadgeModel.FeaturedModel(badge = state.badge))
|
||||
|
||||
sectionHeaderPref(
|
||||
title = DSLSettingsText.from(
|
||||
|
||||