Compare commits

...

169 Commits

Author SHA1 Message Date
Greyson Parrelli
940cee0f30 Bump version to 7.0.2 2024-02-23 16:26:12 -05:00
Greyson Parrelli
cdb6c16473 Update translations and other static files. 2024-02-23 16:25:05 -05:00
Greyson Parrelli
c4842ae7c5 Attempt to prevent message retry loops. 2024-02-23 15:36:23 -05:00
Greyson Parrelli
dc32e51ac2 Make a specific crash more clear to improve debuggability. 2024-02-23 15:36:23 -05:00
Greyson Parrelli
43caaf7efc Update a specific recipient case to merge rather than just steal PNI. 2024-02-23 15:36:23 -05:00
Greyson Parrelli
dcd0d433b0 Fix potential charset crash on some devices. 2024-02-23 15:36:23 -05:00
Cody Henthorne
763e891dfd Show username in group invite flow. 2024-02-23 15:36:23 -05:00
Cody Henthorne
c04f761f5a Show rate limit specific error message on username reservation. 2024-02-23 15:36:23 -05:00
Cody Henthorne
b147882e4f Fix text input selection handle colors. 2024-02-23 15:36:23 -05:00
Cody Henthorne
c9f5f91aad Fix black bars on username scan crosshair. 2024-02-23 15:36:23 -05:00
Cody Henthorne
0a3de42729 Fix username QR image generation for multiline usernames. 2024-02-23 15:36:23 -05:00
Greyson Parrelli
64fc0209f4 Remove unused endpoint. 2024-02-23 15:36:23 -05:00
Cody Henthorne
418ad51e77 Fix share your username popup icon tint. 2024-02-23 15:36:23 -05:00
Cody Henthorne
16faf41a84 Fix profile name not updating correctly. 2024-02-23 15:36:23 -05:00
Cody Henthorne
d5cf8d36b3 Fix username recovery UX bugs. 2024-02-23 15:36:23 -05:00
Greyson Parrelli
755fafb0b6 Always use US locale when logging rounded numbers. 2024-02-21 13:08:43 -05:00
Greyson Parrelli
8fc9893ecd Improve logging around retries archiving sessions. 2024-02-21 13:04:42 -05:00
Greyson Parrelli
9071fd0024 Bump version to 7.0.1 2024-02-20 21:45:57 -05:00
Greyson Parrelli
9a3233bb28 Update translations and other static files. 2024-02-20 21:45:30 -05:00
Greyson Parrelli
e77bc9170a Fix RTL rendering of username edit screen. 2024-02-20 21:36:24 -05:00
Greyson Parrelli
23d6a71a3b Update username validation to use libsignal. 2024-02-20 21:36:24 -05:00
Greyson Parrelli
67c3f41dff Fix crash when attempting to start a call via username. 2024-02-20 16:02:57 -05:00
Greyson Parrelli
e22fa499c2 Reduce username debounce rate to 500ms. 2024-02-20 15:25:27 -05:00
Cody Henthorne
fdef13ae92 Apply LQA feedback. 2024-02-20 15:02:39 -05:00
Greyson Parrelli
c0a6f2316c Do not acknowledge retry receipts sent to PNI. 2024-02-20 12:24:32 -05:00
Greyson Parrelli
7d6a87c825 Attempt to fix missing session crash for resends. 2024-02-20 12:13:54 -05:00
Greyson Parrelli
6c863fe99c Clean up capability logging. 2024-02-18 18:34:18 -05:00
Greyson Parrelli
5cf8242ea0 Bump version to 7.0.0 2024-02-16 16:44:19 -05:00
Greyson Parrelli
a804e8a27c Update translations and other static files. 2024-02-16 16:42:53 -05:00
Greyson Parrelli
3b598e2f07 Update string for username creation. 2024-02-16 15:53:05 -05:00
Greyson Parrelli
03c5a254e8 Fix issue with receiving server delivery receipts at PNI. 2024-02-15 22:05:57 -05:00
Greyson Parrelli
bdb34e16c6 Update UI and strings for the duplicate name review screen. 2024-02-15 21:43:36 -05:00
Cody Henthorne
e7c018283a Perform directory refresh after a PNI invite accept. 2024-02-15 21:43:36 -05:00
Cody Henthorne
ebd8d85a3d Implement UX feedback in new conversation start flows. 2024-02-15 21:43:36 -05:00
Greyson Parrelli
3f8a9e1be2 Reduce max discriminator length to 9. 2024-02-15 21:43:36 -05:00
Greyson Parrelli
0d5961baf9 Update string on find by username screen. 2024-02-15 21:43:36 -05:00
Greyson Parrelli
872ee805d1 Assume PNP capability is true. 2024-02-15 21:43:36 -05:00
Nicholas Tinsley
b19bcd88b9 Null check VideoPlayer during view binding. 2024-02-15 21:43:36 -05:00
Greyson Parrelli
5c9d65386b Fix username deletion sync issue. 2024-02-15 21:43:36 -05:00
Greyson Parrelli
a86a0938ce Fix avatar fallback photo for self when useSelfProfileAvatar=true. 2024-02-15 21:43:36 -05:00
Greyson Parrelli
a886e5f9a0 Directly show about sheet when you show a recipient sheet for yourself. 2024-02-15 21:43:36 -05:00
Greyson Parrelli
83c1bd61cb Add bottom sheet handle to RecipientBottomSheet. 2024-02-15 21:43:36 -05:00
Greyson Parrelli
4ce1789110 Do not show the discriminator field until one is chosen. 2024-02-15 21:43:36 -05:00
Greyson Parrelli
f484fdbbac Remove unused 'registration' variant of username screen. 2024-02-15 21:43:36 -05:00
Greyson Parrelli
57ac7cb328 Show some more info in the about sheet. 2024-02-15 21:43:36 -05:00
Greyson Parrelli
47cdc50a81 Add confirmation dialog when changing username would reset link. 2024-02-15 21:43:36 -05:00
Greyson Parrelli
555ddb5b20 Show dialog for successfully resetting your username link. 2024-02-15 21:43:36 -05:00
Greyson Parrelli
8e8ba23da7 Do not show the QR code shortuct if you have no username. 2024-02-15 21:43:36 -05:00
Greyson Parrelli
54a1b97167 Update username description string in edit profile screen. 2024-02-15 21:43:36 -05:00
Greyson Parrelli
7530d44d28 Refactor username link share screen to enable previews. 2024-02-15 21:43:36 -05:00
Greyson Parrelli
252aa3714e Sync the 'hasCompletedUsernameOnboarding' flag. 2024-02-15 21:43:36 -05:00
Greyson Parrelli
ce09e9a217 Update UI for PNP launch megaphone. 2024-02-15 21:43:35 -05:00
Greyson Parrelli
8797236b5a Add migration for ensuring we set the latest pnp settings. 2024-02-15 21:43:35 -05:00
Greyson Parrelli
6097e6c305 Default discoverability to 'off' until registration is complete. 2024-02-15 21:43:35 -05:00
Greyson Parrelli
879fca0e11 Interpret unknown phone number sharing setting as 'off'. 2024-02-15 21:43:35 -05:00
Greyson Parrelli
c359ddf3c8 Inline the pnp feature flag. 2024-02-15 21:43:35 -05:00
Greyson Parrelli
8ad77ac7aa Inline the username flag. 2024-02-15 21:43:35 -05:00
Cody Henthorne
bd3b779282 Bump version to 6.47.4 2024-02-15 21:43:16 -05:00
Cody Henthorne
42b805eb91 Updated baseline profile. 2024-02-15 21:37:08 -05:00
Cody Henthorne
107f2cd3b1 Update translations and other static files. 2024-02-15 21:31:52 -05:00
Nicholas Tinsley
c713ccf76c Don't mark outgoing media as upload only. 2024-02-15 21:26:30 -05:00
Cody Henthorne
dd9c65012b Fix NPE when find by phone row not supported. 2024-02-15 19:36:26 -05:00
Cody Henthorne
31e872a34e Bump version to 6.47.3 2024-02-14 20:05:57 -05:00
Cody Henthorne
81579dc9bf Update translations and other static files. 2024-02-14 20:00:20 -05:00
Greyson Parrelli
3c6c03cd75 Fix bug when syncing username deletions. 2024-02-14 11:59:15 -05:00
Greyson Parrelli
ba41df19bb Fix thread merges where one thread is inactive. 2024-02-14 11:15:49 -05:00
Cody Henthorne
0cc7178cdc Bump version to 6.47.2 2024-02-13 15:49:18 -05:00
Cody Henthorne
239e4a7e66 Updated baseline profile. 2024-02-13 15:46:30 -05:00
Cody Henthorne
0b031d35e3 Update translations and other static files. 2024-02-13 15:43:43 -05:00
Nicholas Tinsley
32e81049f5 Cycle audio remux feature flag. 2024-02-13 15:39:04 -05:00
Nicholas Tinsley
6a65a1c149 Default to 1x camera in video recording.
Addresses #11955, #12482, #13017

Shout out to @tedgravlin for #13411.
2024-02-13 15:39:04 -05:00
Cody Henthorne
36ea2a7f5d Fix active call manager.
For real this time.
2024-02-13 15:39:04 -05:00
Cody Henthorne
cc40c9d09f Bump version to 6.47.1 2024-02-12 16:32:01 -05:00
Cody Henthorne
fb9e731e00 Updated baseline profile. 2024-02-12 16:27:07 -05:00
Cody Henthorne
264607e0f3 Update translations and other static files. 2024-02-12 16:21:55 -05:00
Greyson Parrelli
cdfc42cc38 Fix possible crash in story replies. 2024-02-12 15:01:58 -05:00
Cody Henthorne
19cfae1da5 Remove duplicate future code. 2024-02-12 15:00:27 -05:00
Jim Gustafson
577b11a349 Update to RingRTC v2.37.1 2024-02-12 09:15:55 -05:00
Clark Chen
671dfceac3 Bump version to 6.47.0 2024-02-09 19:43:41 -05:00
Clark Chen
5626fb74ae Update translations and other static files. 2024-02-09 19:28:27 -05:00
Greyson Parrelli
88d6c91517 Fix some bugs related to how the conversation header renders. 2024-02-09 18:41:26 -05:00
Cody Henthorne
aa76cefb1c Update spam UX and reporting flows. 2024-02-09 18:41:26 -05:00
Cody Henthorne
a4fde60c1c Fix crash in ActiveCallManager for lower Android versions. 2024-02-09 15:19:25 -05:00
Nicholas Tinsley
8e86612fc2 Derive PIN keyboard type if none set.
Addresses #13338.
2024-02-09 15:11:38 -05:00
Clark
f6ded23383 Ignore message latency when latency is before device boot time. 2024-02-09 14:55:58 -05:00
Nicholas Tinsley
2ab689c59b Add plural string for chat deletion. 2024-02-09 13:32:05 -05:00
Greyson Parrelli
155f6a88f8 Ensure one-time kyber prekeys are generated during change number. 2024-02-09 13:32:05 -05:00
Greyson Parrelli
3d84fc9c98 Use 'deleted account' as name for certain unregisterd user cases. 2024-02-09 13:32:05 -05:00
Greyson Parrelli
976e146248 Only count active threads when determining possible communication. 2024-02-09 13:32:05 -05:00
Greyson Parrelli
d9b0723194 Make message footer clickable when there's an error. 2024-02-09 13:32:05 -05:00
Greyson Parrelli
0630a6910a Update strings and previews for pnp settings. 2024-02-09 13:32:05 -05:00
Greyson Parrelli
bc930345b9 Remove blockSSE feature flag. 2024-02-08 14:10:20 -05:00
Greyson Parrelli
d7d7963101 Defer to the latest revision for backup inclusion. 2024-02-08 14:10:20 -05:00
Greyson Parrelli
fa2551dfcf Always show the new 'find by phone number' item. 2024-02-08 14:10:20 -05:00
Greyson Parrelli
b260a47b49 Fix issue where media sent transcripts didn't trigger thread updates. 2024-02-08 14:10:20 -05:00
Greyson Parrelli
47e55fc621 Do not show add to contacts if there's no e164. 2024-02-08 14:10:20 -05:00
Alex Hart
700fe5e463 Add Find By Username and Find By Phone Number interstitials.
Co-authored-by: Greyson Parrelli <greyson@signal.org>
2024-02-08 14:10:20 -05:00
Greyson Parrelli
ca3d239ce2 Bump version to 6.46.7 2024-02-08 12:54:26 -05:00
Greyson Parrelli
12385b9c5a Update stores to use a constant accountId for PNIs. 2024-02-08 12:53:22 -05:00
Clark Chen
65b26adb3d Bump version to 6.46.6 2024-02-06 15:26:38 -05:00
Clark Chen
31b8927291 Update translations and other static files. 2024-02-06 15:16:55 -05:00
Cody Henthorne
94ffbb3e8e Fix NPE crash when scanning safety numbers. 2024-02-06 10:32:20 -05:00
Greyson Parrelli
1b7616b4db Fix issue with PNIs in contact sync. 2024-02-06 09:47:12 -05:00
Clark Chen
71850e1e35 Bump version to 6.46.5 2024-02-05 18:56:05 -05:00
Clark Chen
380fb60791 Update translations and other static files. 2024-02-05 18:36:27 -05:00
Greyson Parrelli
2cf9fa0524 Fix crash when opening group story replies. 2024-02-05 16:30:54 -05:00
Nicholas Tinsley
c8c4fdc65e Bump version to 6.46.4 2024-02-05 10:59:36 -05:00
Nicholas Tinsley
9fb16be74f Updated baseline profile. 2024-02-05 10:56:19 -05:00
Nicholas Tinsley
2d4d4aab66 Update translations and other static files. 2024-02-05 10:53:17 -05:00
Nicholas Tinsley
f18070b78c Revert "Don't recreate attachment InputStream if we don't have to."
This reverts commit 467dae8132.
2024-02-05 10:49:05 -05:00
Nicholas Tinsley
adf1d8a43a Bump version to 6.46.3 2024-02-03 14:08:31 -05:00
Nicholas Tinsley
f360934ddd Updated baseline profile. 2024-02-03 14:04:58 -05:00
Nicholas Tinsley
b986c4d54c Update translations and other static files. 2024-02-03 14:02:12 -05:00
Nicholas Tinsley
4545e70384 Remix audio remuxing into its own feature flag. 2024-02-03 13:57:49 -05:00
Nicholas Tinsley
3c5cbc3114 Bump version to 6.46.2 2024-02-03 12:45:27 -05:00
Nicholas Tinsley
879b0ad95d Updated baseline profile. 2024-02-03 12:41:31 -05:00
Nicholas Tinsley
9982810edb Update translations and other static files. 2024-02-03 12:38:24 -05:00
Greyson Parrelli
699b788187 Fix possible stack overflow in backup export. 2024-02-03 12:26:38 -05:00
Nicholas Tinsley
bd61044643 Bump version to 6.46.1 2024-02-02 15:24:32 -05:00
Nicholas Tinsley
6804d58323 Updated baseline profile. 2024-02-02 15:17:04 -05:00
Nicholas Tinsley
bfe57b4b8f Update translations and other static files. 2024-02-02 15:12:26 -05:00
Greyson Parrelli
e3fe852a34 Do not backup past revision if latest revision is soon-to-expire. 2024-02-02 15:07:30 -05:00
Cody Henthorne
673fe2625c Show iDEAL 0,01 temporary charge warning dialog for monthly subscription. 2024-02-02 15:07:30 -05:00
Cody Henthorne
e798feb1d7 Use paymentId from create PayPal intent instead of confirm. 2024-02-02 15:07:30 -05:00
Nicholas Tinsley
fa937f9f43 Bump version to 6.46.0 2024-01-31 22:42:48 -05:00
Nicholas Tinsley
ec5f3fc333 Updated baseline profile. 2024-01-31 22:37:58 -05:00
Nicholas Tinsley
d85c150a8c Update translations and other static files. 2024-01-31 22:35:13 -05:00
Nicholas Tinsley
a5faf0e098 Match output video file size limit in video editor. 2024-01-31 22:13:46 -05:00
Alex Hart
1234c63836 Add scaffolding for backupsV2. 2024-01-31 22:13:46 -05:00
Cody Henthorne
91920319c7 Cycle calling without a service remote config. 2024-01-31 22:13:46 -05:00
Nicholas Tinsley
950d9d5a4c Validate image preview as URI.
Resolves #13392.
2024-01-31 22:13:46 -05:00
Nicholas Tinsley
467dae8132 Don't recreate attachment InputStream if we don't have to. 2024-01-31 22:13:46 -05:00
Nicholas Tinsley
d1ef9d5dcf Align video compression limit with attachment limit. 2024-01-31 22:13:46 -05:00
Nicholas Tinsley
a60419a442 Add remote config for GIF keyboard page. 2024-01-31 22:13:46 -05:00
Nicholas Tinsley
f97a034c34 Null check multi share attachment quality guard. 2024-01-31 22:13:46 -05:00
Greyson Parrelli
4d0fbe2343 Add a dark theme for spinner. 2024-01-31 22:13:46 -05:00
Jameson Williams
1d5e108cd4 QA target can pass on ARM MacBook
The Conscrypt library does not have a native library suitable for
running on the ARM MacBooks. As a result, unit tests that rely on
Conscrypt also don't work on this hardware.

Fortunately, though, the tests will pass without Conscrypt anyway. As a
workaround, we can avoid using Conscrypt only when running the unit test
suite on these machines.

See also: https://github.com/google/conscrypt/issues/1034

Resolves #13382.
2024-01-31 22:13:46 -05:00
Greyson Parrelli
716afc98ac Sync PNI verification status to storage service. 2024-01-31 22:13:46 -05:00
Greyson Parrelli
459607adae Clear local state after requesting to ignore battery optimizations. 2024-01-31 22:13:46 -05:00
Cody Henthorne
784b705265 Fix spoiler in notification with emoji prefix. 2024-01-31 22:13:46 -05:00
Alex Hart
326b95bd10 Upgrade Compose and AndroidX libraries. 2024-01-31 22:13:46 -05:00
Greyson Parrelli
466acaf504 Fix possible exception when rendering SSE event.
Fixes #13390
2024-01-31 22:13:46 -05:00
Maksym Moroz
838165c3e6 Register gradle tasks instead of creating eagerly.
Signed-off-by: Maksym Moroz <maksymmoroz@duck.com>

Resolves #13391
2024-01-31 22:13:46 -05:00
Jim Gustafson
e1d7ad7d03 Update to RingRTC v2.37.0 2024-01-31 22:13:46 -05:00
Greyson Parrelli
3776e86b83 Update the types of messages we backup. 2024-01-31 22:13:46 -05:00
Greyson Parrelli
ea60858a07 Improve logging around 'contact joined' messages. 2024-01-31 22:13:46 -05:00
Greyson Parrelli
d4488c72fb Simplify contact splitting when reading from storage service. 2024-01-31 22:13:46 -05:00
Greyson Parrelli
9146f2fb30 Remove some unused methods.
Fixes ##13384
2024-01-31 22:12:17 -05:00
Greyson Parrelli
92993f967e Clear MSL after SN change. 2024-01-31 22:12:17 -05:00
Greyson Parrelli
2c2735af6d Do not create new threads to show error messages. 2024-01-31 22:12:17 -05:00
Alex Hart
80c0e19692 Improve sizing of shareable QR image. 2024-01-31 22:12:17 -05:00
Alex Hart
c9e2162afc Remove old username education fragment. 2024-01-31 22:12:17 -05:00
Nicholas Tinsley
9a52f4e3ff Remux audio if possible when transcoding.
Addresses #11712, #12674, #12945, #13084, #13346.
2024-01-31 22:12:17 -05:00
Greyson Parrelli
c0235d4cc2 Ensure contacts are split after profile 404. 2024-01-31 22:12:17 -05:00
Cody Henthorne
f1704fbb57 Fix file attachment dedup logic. 2024-01-31 22:12:17 -05:00
Alex Hart
38d5d3ad1b Add polish to usernames UX. 2024-01-31 22:12:17 -05:00
Jameson Williams
ec96b4e3aa Update Glide to use ksp, drop kapt.
Resolves #13381
2024-01-31 22:12:17 -05:00
Cody Henthorne
aa33fd44b8 Remove SMS export. 2024-01-31 22:12:17 -05:00
Clark
98865d61dd Convert gv2 update messages to backup distinct protos. 2024-01-31 22:12:17 -05:00
Cody Henthorne
0036b8e2d6 Migrate legacy png and webp to signal symbols. 2024-01-31 22:12:17 -05:00
Alex Hart
c021d26103 Add polish to PNP registration settings. 2024-01-31 22:12:17 -05:00
Alex Hart
8bca5b4901 Update PnP description text for everyone listing. 2024-01-31 22:12:17 -05:00
Greyson Parrelli
d4db6c8912 Keep some recipient logs longer. 2024-01-31 22:12:17 -05:00
Jameson Williams
c93b4909f4 Remove Jetifier from build.
materialish-progress and subsampling-scale-image-view were bringing in
Android Support libraries as transitive dependencies. This required
Jetifier to be run as part of the build. More recent versions of these
dependencies have been released, which now use AndroidX directly. By
upgrading these dependencies, Jetifier is no longer needed to build
Signal.

Resolves #13378
2024-01-31 22:12:17 -05:00
Maksym Moroz
67d3c8e777 Clean up app module build.gradle.kts
Closes #13379

Signed-off-by: Maksym Moroz <maksymmoroz@duck.com>
2024-01-31 22:12:17 -05:00
Nicholas Tinsley
8d44222097 Enable logcat in the video sample app. 2024-01-31 22:12:17 -05:00
Nicholas Tinsley
9cb2024334 Option to persist task in video sample app. 2024-01-31 22:12:17 -05:00
Alex Hart
e71bb33b23 Fix conversation bubbles becoming too long.
Co-authored-by: Cody Henthorne <cody@signal.org>
2024-01-31 22:12:17 -05:00
Alex Hart
4ada7c9be9 Implement several polish items for Call Links. 2024-01-31 22:12:16 -05:00
968 changed files with 25229 additions and 25488 deletions

View File

@@ -21,8 +21,8 @@ plugins {
apply(from = "static-ips.gradle.kts")
val canonicalVersionCode = 1380
val canonicalVersionName = "6.45.2"
val canonicalVersionCode = 1396
val canonicalVersionName = "7.0.2"
val postFixSize = 100
val abiPostFix: Map<String, Int> = mapOf(
@@ -98,7 +98,6 @@ android {
kotlinOptions {
jvmTarget = signalKotlinJvmTarget
freeCompilerArgs = listOf("-Xallow-result-return-type")
}
keystores["debug"]?.let { properties ->
@@ -164,8 +163,8 @@ android {
versionCode = canonicalVersionCode * postFixSize
versionName = canonicalVersionName
minSdkVersion(signalMinSdkVersion)
targetSdkVersion(signalTargetSdkVersion)
minSdk = signalMinSdkVersion
targetSdk = signalTargetSdkVersion
multiDexEnabled = true
@@ -415,9 +414,7 @@ android {
}
applicationVariants.all {
val variant = this
variant.outputs
outputs
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
.forEach { output ->
if (output.baseName.contains("nightly")) {
@@ -430,10 +427,10 @@ android {
output.versionNameOverride = tag
output.outputFileName = output.outputFileName.replace(".apk", "-${output.versionNameOverride}.apk")
} else {
output.outputFileName = output.outputFileName.replace(".apk", "-${variant.versionName}.apk")
output.outputFileName = output.outputFileName.replace(".apk", "-$versionName.apk")
}
} else {
output.outputFileName = output.outputFileName.replace(".apk", "-${variant.versionName}.apk")
output.outputFileName = output.outputFileName.replace(".apk", "-$versionName.apk")
val abiName: String = output.getFilter("ABI") ?: "universal"
val postFix: Int = abiPostFix[abiName]!!
@@ -447,25 +444,20 @@ android {
}
}
android.variantFilter {
val distribution: String = flavors[0].name
val environment: String = flavors[1].name
val buildType: String = buildType.name
val fullName: String = distribution + environment.capitalize() + buildType.capitalize()
if (!selectableVariants.contains(fullName)) {
ignore = true
androidComponents {
beforeVariants { variant ->
variant.enable = variant.name in selectableVariants
}
}
android.buildTypes.forEach {
val path: String = if (it.name == "release") {
"$projectDir/src/release/java"
} else {
"$projectDir/src/debug/java"
}
val releaseDir = "$projectDir/src/release/java"
val debugDir = "$projectDir/src/debug/java"
sourceSets.findByName(it.name)!!.java.srcDir(path)
android.buildTypes.configureEach {
val path = if (name == "release") releaseDir else debugDir
sourceSets.named(name) {
java.srcDir(path)
}
}
}
@@ -484,7 +476,6 @@ dependencies {
implementation(project(":donations"))
implementation(project(":contacts"))
implementation(project(":qr"))
implementation(project(":sms-exporter"))
implementation(project(":sticky-header-grid"))
implementation(project(":photoview"))
implementation(project(":core-ui"))
@@ -506,16 +497,19 @@ dependencies {
implementation(libs.androidx.exifinterface)
implementation(libs.androidx.compose.rxjava3)
implementation(libs.androidx.compose.runtime.livedata)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.multidex)
implementation(libs.androidx.navigation.fragment.ktx)
implementation(libs.androidx.navigation.ui.ktx)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.lifecycle.livedata.ktx)
implementation(libs.androidx.lifecycle.process)
implementation(libs.androidx.lifecycle.viewmodel.savedstate)
implementation(libs.androidx.lifecycle.common.java8)
implementation(libs.androidx.lifecycle.reactivestreams.ktx)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle)

View File

@@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.conversation.v2.items
import android.net.Uri
import android.view.View
import androidx.lifecycle.Observer
import com.bumptech.glide.RequestManager
import io.mockk.mockk
import org.junit.Assert.assertEquals
import org.junit.Rule
@@ -29,7 +30,6 @@ import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange
import org.thoughtcrime.securesms.linkpreview.LinkPreview
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.stickers.StickerLocator
@@ -209,7 +209,7 @@ class V2ConversationItemShapeTest {
override val selectedItems: Set<MultiselectPart> = emptySet()
override val isMessageRequestAccepted: Boolean = true
override val searchQuery: String? = null
override val glideRequests: GlideRequests = mockk()
override val requestManager: RequestManager = mockk()
override val isParentInScroll: Boolean = false
override fun getChatColorsData(): ChatColorsDrawable.ChatColorsData = ChatColorsDrawable.ChatColorsData(null, null)
@@ -322,5 +322,11 @@ class V2ConversationItemShapeTest {
override fun onItemClick(item: MultiselectPart?) = Unit
override fun onItemLongClick(itemView: View?, item: MultiselectPart?) = Unit
override fun onShowSafetyTips(forGroup: Boolean) = Unit
override fun onReportSpamLearnMoreClicked() = Unit
override fun onMessageRequestAcceptOptionsClicked() = Unit
}
}

View File

@@ -11,8 +11,6 @@ import org.signal.core.util.CursorUtil
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.FeatureFlagsAccessor
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import java.util.UUID
@@ -167,8 +165,6 @@ class RecipientTableTest {
@Test
fun givenARecipientWithPniAndAci_whenIMarkItUnregistered_thenIExpectItToBeSplit() {
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
val mainId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
SignalDatabase.recipients.markUnregistered(mainId)
@@ -185,12 +181,10 @@ class RecipientTableTest {
@Test
fun givenARecipientWithPniAndAci_whenISplitItForStorageSync_thenIExpectItToBeSplit() {
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
val mainId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
val mainRecord = SignalDatabase.recipients.getRecord(mainId)
SignalDatabase.recipients.splitForStorageSync(mainRecord.storageId!!)
SignalDatabase.recipients.splitForStorageSyncIfNecessary(mainRecord.aci!!)
val byAci: RecipientId = SignalDatabase.recipients.getByAci(ACI_A).get()

View File

@@ -18,6 +18,7 @@ import org.signal.core.util.Base64
import org.signal.core.util.SqlUtil
import org.signal.core.util.exists
import org.signal.core.util.orNull
import org.signal.core.util.readToSingleBoolean
import org.signal.core.util.requireLong
import org.signal.core.util.requireNonNullString
import org.signal.core.util.select
@@ -40,8 +41,6 @@ import org.thoughtcrime.securesms.mms.IncomingMessage
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.FeatureFlagsAccessor
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
@@ -57,7 +56,6 @@ class RecipientTableTest_getAndPossiblyMerge {
SignalStore.account().setE164(E164_SELF)
SignalStore.account().setAci(ACI_SELF)
SignalStore.account().setPni(PNI_SELF)
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
}
@Test
@@ -109,6 +107,18 @@ class RecipientTableTest_getAndPossiblyMerge {
val record = SignalDatabase.recipients.getRecord(id)
assertEquals(RecipientTable.RegisteredState.REGISTERED, record.registered)
}
test("e164+pni+aci insert, pni verified") {
val id = process(E164_A, PNI_A, ACI_A, pniVerified = true)
expect(E164_A, PNI_A, ACI_A)
expectPniVerified()
val record = SignalDatabase.recipients.getRecord(id)
assertEquals(RecipientTable.RegisteredState.REGISTERED, record.registered)
process(E164_A, PNI_A, ACI_A, pniVerified = false)
expectPniVerified()
}
}
@Test
@@ -164,6 +174,7 @@ class RecipientTableTest_getAndPossiblyMerge {
expect(E164_A, PNI_A, ACI_A)
expectNoSessionSwitchoverEvent()
expectPniVerified()
}
test("no match, all fields") {
@@ -225,6 +236,8 @@ class RecipientTableTest_getAndPossiblyMerge {
given(E164_A, PNI_A, null, pniSession = true)
process(E164_A, PNI_A, ACI_A, pniVerified = true)
expect(E164_A, PNI_A, ACI_A)
expectPniVerified()
}
test("e164 and aci matches, all provided, new pni") {
@@ -694,6 +707,8 @@ class RecipientTableTest_getAndPossiblyMerge {
expectDeleted()
expect(E164_A, PNI_A, ACI_A)
expectPniVerified()
}
test("merge, e164+pni & aci, pni session, pni verified") {
@@ -706,6 +721,7 @@ class RecipientTableTest_getAndPossiblyMerge {
expect(E164_A, PNI_A, ACI_A)
expectThreadMergeEvent(E164_A)
expectPniVerified()
}
test("merge, e164+pni & e164+pni+aci, change number") {
@@ -772,6 +788,17 @@ class RecipientTableTest_getAndPossiblyMerge {
expectChangeNumberEvent()
}
test("merge, e164 follows pni+aci") {
given(E164_A, PNI_A, null)
given(null, null, ACI_A)
process(null, PNI_A, ACI_A, pniVerified = true)
expect(E164_A, PNI_A, ACI_A)
expectThreadMergeEvent(E164_A)
expectPniVerified()
}
test("local user, local e164 and aci provided, changeSelf=false, leave e164 alone") {
given(E164_SELF, null, ACI_SELF)
given(null, null, ACI_A)
@@ -874,8 +901,8 @@ class RecipientTableTest_getAndPossiblyMerge {
// Thread validation
assertEquals(threadIdAci, retrievedThreadId)
Assert.assertNull(SignalDatabase.threads.getThreadIdFor(recipientIdE164))
Assert.assertNull(SignalDatabase.threads.getThreadRecord(threadIdE164))
assertNull(SignalDatabase.threads.getThreadIdFor(recipientIdE164))
assertNull(SignalDatabase.threads.getThreadRecord(threadIdE164))
// SMS validation
val sms1: MessageRecord = SignalDatabase.messages.getMessageRecord(smsId1)!!
@@ -919,10 +946,10 @@ class RecipientTableTest_getAndPossiblyMerge {
// Identity validation
assertEquals(identityKeyAci, SignalDatabase.identities.getIdentityStoreRecord(ACI_A.toString())!!.identityKey)
Assert.assertNull(SignalDatabase.identities.getIdentityStoreRecord(E164_A))
assertNull(SignalDatabase.identities.getIdentityStoreRecord(E164_A))
// Session validation
Assert.assertNotNull(SignalDatabase.sessions.load(ACI_SELF, SignalProtocolAddress(ACI_A.toString(), 1)))
assertNotNull(SignalDatabase.sessions.load(ACI_SELF, SignalProtocolAddress(ACI_A.toString(), 1)))
// Reaction validation
val reactionsSms: List<ReactionRecord> = SignalDatabase.reactions.getReactions(MessageId(smsId1))
@@ -1037,6 +1064,10 @@ class RecipientTableTest_getAndPossiblyMerge {
if (!test.sessionSwitchoverExpected) {
test.expectNoSessionSwitchoverEvent()
}
if (!test.pniVerifiedExpected) {
test.expectPniNotVerified()
}
} catch (e: Throwable) {
if (e.javaClass != exception) {
val error = java.lang.AssertionError("[$name] ${e.message}")
@@ -1056,6 +1087,7 @@ class RecipientTableTest_getAndPossiblyMerge {
var changeNumberExpected = false
var threadMergeExpected = false
var sessionSwitchoverExpected = false
var pniVerifiedExpected = false
init {
// Need to delete these first to prevent foreign key crash
@@ -1207,6 +1239,24 @@ class RecipientTableTest_getAndPossiblyMerge {
assertNull("Unexpected thread merge event!", getLatestThreadMergeEvent(outputRecipientId))
}
fun expectPniVerified() {
assertTrue("Expected PNI to be verified!", isPniVerified(outputRecipientId))
pniVerifiedExpected = true
}
fun expectPniNotVerified() {
assertFalse("Expected PNI to be not be verified!", isPniVerified(outputRecipientId))
}
private fun isPniVerified(recipientId: RecipientId): Boolean {
return SignalDatabase.rawDatabase
.select(RecipientTable.PNI_SIGNATURE_VERIFIED)
.from(RecipientTable.TABLE_NAME)
.where("${RecipientTable.ID} = ?", recipientId)
.run()
.readToSingleBoolean(false)
}
private fun insert(e164: String?, pni: PNI?, aci: ACI?): RecipientId {
val id: Long = SignalDatabase.rawDatabase.insert(
RecipientTable.TABLE_NAME,

View File

@@ -290,7 +290,8 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
from = sender,
timestamp = wallClock,
groupId = groupId,
groupContext = groupContext
groupContext = groupContext,
serverGuid = null
)
}

View File

@@ -57,23 +57,6 @@ class UsernameEditFragmentTest {
InstrumentationApplicationDependencyProvider.clearHandlers()
}
@Test
fun testUsernameCreationInRegistration() {
val scenario = createScenario(UsernameEditMode.REGISTRATION)
scenario.moveToState(Lifecycle.State.RESUMED)
onView(withId(R.id.toolbar)).check { view, noViewFoundException ->
noViewFoundException.assertIsNull()
val toolbar = view as Toolbar
toolbar.navigationIcon.assertIsNull()
}
onView(withText(R.string.UsernameEditFragment__add_a_username)).check(matches(isDisplayed()))
onView(withContentDescription(R.string.load_more_header__loading)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
}
@Ignore("Flakey espresso test.")
@Test
fun testUsernameCreationOutsideOfRegistration() {
@@ -108,7 +91,7 @@ class UsernameEditFragmentTest {
}
)
val scenario = createScenario(UsernameEditMode.REGISTRATION)
val scenario = createScenario(UsernameEditMode.NORMAL)
scenario.moveToState(Lifecycle.State.RESUMED)
onView(withId(R.id.username_text)).perform(typeText(nickname))

View File

@@ -12,8 +12,6 @@ import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.FeatureFlagsAccessor
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import org.whispersystems.signalservice.api.storage.SignalContactRecord
@@ -29,11 +27,10 @@ class ContactRecordProcessorTest {
SignalStore.account().setE164(E164_SELF)
SignalStore.account().setAci(ACI_SELF)
SignalStore.account().setPni(PNI_SELF)
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
}
@Test
fun process_splitContact_normalSplit() {
fun process_splitContact_normalSplit_twoRecords() {
// GIVEN
val originalId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
setStorageId(originalId, STORAGE_ID_A)
@@ -69,6 +66,35 @@ class ContactRecordProcessorTest {
assertNotEquals(byAci, byE164)
}
@Test
fun process_splitContact_normalSplit_oneRecord() {
// GIVEN
val originalId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
setStorageId(originalId, STORAGE_ID_A)
val remote = buildRecord(
STORAGE_ID_B,
ContactRecord(
aci = ACI_A.toString(),
unregisteredAtTimestamp = 100
)
)
// WHEN
val subject = ContactRecordProcessor()
subject.process(listOf(remote), StorageSyncHelper.KEY_GENERATOR)
// THEN
val byAci: RecipientId = SignalDatabase.recipients.getByAci(ACI_A).get()
val byE164: RecipientId = SignalDatabase.recipients.getByE164(E164_A).get()
val byPni: RecipientId = SignalDatabase.recipients.getByPni(PNI_A).get()
assertEquals(originalId, byAci)
assertEquals(byE164, byPni)
assertNotEquals(byAci, byE164)
}
@Test
fun process_splitContact_doNotSplitIfAciRecordIsRegistered() {
// GIVEN

View File

@@ -40,7 +40,7 @@ class AliceClient(val serviceId: ServiceId, val e164: String, val trustRoot: ECK
ApplicationDependencies.getIncomingMessageObserver()
.processEnvelope(bufferedStore, envelope, serverDeliveredTimestamp)
?.mapNotNull { it.run() }
?.forEach { ApplicationDependencies.getJobManager().add(it) }
?.forEach { it.enqueue() }
bufferedStore.flushToDisk()
val end = System.currentTimeMillis()

View File

@@ -6,6 +6,6 @@ package org.thoughtcrime.securesms.util;
public final class FeatureFlagsAccessor {
public static void forceValue(String key, Object value) {
FeatureFlags.FORCED_VALUES.put(FeatureFlags.PHONE_NUMBER_PRIVACY, true);
FeatureFlags.FORCED_VALUES.put(key, value);
}
}

View File

@@ -118,7 +118,8 @@ class ConversationElementGenerator {
null,
null,
0,
false
false,
null
)
val conversationMessage = ConversationMessageFactory.createWithUnresolvedData(

View File

@@ -13,6 +13,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.navigation.navGraphViewModels
import com.bumptech.glide.Glide
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.concurrent.LifecycleDisposable
@@ -42,7 +43,6 @@ import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange
import org.thoughtcrime.securesms.linkpreview.LinkPreview
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.stickers.StickerLocator
@@ -62,7 +62,7 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val adapter = ConversationAdapterV2(
lifecycleOwner = viewLifecycleOwner,
glideRequests = GlideApp.with(this),
requestManager = Glide.with(this),
clickListener = ClickListener(),
hasWallpaper = springboardViewModel.hasWallpaper.value,
colorizer = Colorizer(),
@@ -298,5 +298,17 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
override fun onItemLongClick(itemView: View?, item: MultiselectPart?) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onShowSafetyTips(forGroup: Boolean) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onReportSpamLearnMoreClicked() {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onMessageRequestAcceptOptionsClicked() {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
}
}

View File

@@ -28,11 +28,6 @@
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
<uses-permission android:name="android.permission.RECEIVE_MMS"/>
<uses-permission android:name="android.permission.READ_SMS"/>
<uses-permission android:name="android.permission.SEND_SMS"/>
<uses-permission android:name="android.permission.WRITE_SMS"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
@@ -160,12 +155,6 @@
android:value=".MainActivity" />
</activity>
<activity android:name=".PromptMmsActivity"
android:label="Configure MMS Settings"
android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".DeviceProvisioningActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="true">
@@ -693,7 +682,12 @@
<activity android:name=".NewConversationActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateAlwaysVisible"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".recipients.ui.findby.FindByActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="adjustResize"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
@@ -768,6 +762,13 @@
android:windowSoftInputMode="stateAlwaysHidden"
android:exported="false"/>
<activity
android:name=".backup.v2.ui.MessageBackupsFlowActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"
android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".stories.settings.StorySettingsActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
@@ -975,11 +976,6 @@
android:windowSoftInputMode="stateVisible|adjustResize"
android:exported="false"/>
<activity android:name=".profiles.username.AddAUsernameActivity"
android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateVisible|adjustResize"
android:exported="false"/>
<activity android:name=".profiles.manage.EditProfileActivity"
android:theme="@style/TextSecure.LightTheme"
android:windowSoftInputMode="stateVisible|adjustResize"
@@ -1072,13 +1068,6 @@
android:launchMode="singleTask"
android:exported="false"/>
<activity android:name=".megaphone.SmsExportMegaphoneActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:screenOrientation="portrait"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
android:launchMode="singleTask"
android:exported="false"/>
<activity android:name=".ratelimit.RecaptchaProofActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
@@ -1100,24 +1089,11 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".exporter.flow.SmsExportActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:launchMode="singleTask"
android:screenOrientation="portrait"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".components.settings.app.subscription.donate.DonateToSignalActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<service
android:enabled="true"
android:name=".exporter.SignalSmsExportService"
android:foregroundServiceType="dataSync"
android:exported="false"/>
<service
android:enabled="true"
android:name=".service.webrtc.WebRtcCallService"

File diff suppressed because it is too large Load Diff

View File

@@ -24,6 +24,7 @@ import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import androidx.multidex.MultiDexApplication;
import com.bumptech.glide.Glide;
import com.google.android.gms.security.ProviderInstaller;
import org.conscrypt.ConscryptSignal;
@@ -74,7 +75,6 @@ import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
import org.thoughtcrime.securesms.logging.PersistentLogger;
import org.thoughtcrime.securesms.messageprocessingalarm.RoutineMessageFetchReceiver;
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.SignalGlideComponents;
import org.thoughtcrime.securesms.mms.SignalGlideModule;
import org.thoughtcrime.securesms.providers.BlobProvider;
@@ -178,7 +178,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addBlocking("ring-rtc", this::initializeRingRtc)
.addBlocking("glide", () -> SignalGlideModule.setRegisterGlideComponents(new SignalGlideComponents()))
.addNonBlocking(() -> RegistrationUtil.maybeMarkRegistrationComplete())
.addNonBlocking(() -> GlideApp.get(this))
.addNonBlocking(() -> Glide.get(this))
.addNonBlocking(this::cleanAvatarStorage)
.addNonBlocking(this::initializeRevealableMessageManager)
.addNonBlocking(this::initializePendingRetryReceiptManager)

View File

@@ -18,6 +18,7 @@ import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityOptionsCompat;
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.engine.GlideException;
@@ -32,7 +33,6 @@ import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FullscreenHelper;
@@ -96,7 +96,7 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
Resources resources = this.getResources();
GlideApp.with(this)
Glide.with(this)
.asBitmap()
.load(contactPhoto)
.fallback(fallbackPhoto.asCallCard(this))

View File

@@ -8,6 +8,8 @@ import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.Observer;
import com.bumptech.glide.RequestManager;
import org.signal.ringrtc.CallLinkRootKey;
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
import org.thoughtcrime.securesms.contactshare.Contact;
@@ -26,7 +28,6 @@ import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.stickers.StickerLocator;
@@ -41,7 +42,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
@NonNull ConversationMessage messageRecord,
@NonNull Optional<MessageRecord> previousMessageRecord,
@NonNull Optional<MessageRecord> nextMessageRecord,
@NonNull GlideRequests glideRequests,
@NonNull RequestManager requestManager,
@NonNull Locale locale,
@NonNull Set<MultiselectPart> batchSelected,
@NonNull Recipient recipients,
@@ -122,5 +123,8 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void onEditedIndicatorClicked(@NonNull MessageRecord messageRecord);
void onShowGroupDescriptionClicked(@NonNull String groupName, @NonNull String description, boolean shouldLinkifyWebLinks);
void onJoinCallLink(@NonNull CallLinkRootKey callLinkRootKey);
void onShowSafetyTips(boolean forGroup);
void onReportSpamLearnMoreClicked();
void onMessageRequestAcceptOptionsClicked();
}
}

View File

@@ -3,9 +3,10 @@ package org.thoughtcrime.securesms;
import androidx.annotation.NonNull;
import androidx.lifecycle.LifecycleOwner;
import com.bumptech.glide.RequestManager;
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.mms.GlideRequests;
import java.util.Locale;
import java.util.Set;
@@ -14,7 +15,7 @@ public interface BindableConversationListItem extends Unbindable {
void bind(@NonNull LifecycleOwner lifecycleOwner,
@NonNull ThreadRecord thread,
@NonNull GlideRequests glideRequests, @NonNull Locale locale,
@NonNull RequestManager requestManager, @NonNull Locale locale,
@NonNull Set<Long> typingThreads,
@NonNull ConversationSet selectedConversations);

View File

@@ -11,16 +11,36 @@ import androidx.lifecycle.Lifecycle;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.signal.core.util.concurrent.SimpleTask;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.GroupRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import org.whispersystems.signalservice.api.push.ServiceId;
import java.util.List;
import java.util.Optional;
import okio.ByteString;
/**
* This should be used whenever we want to prompt the user to block/unblock a recipient.
*/
public final class BlockUnblockDialog {
private BlockUnblockDialog() { }
private BlockUnblockDialog() {}
public static void showReportSpamFor(@NonNull Context context,
@NonNull Lifecycle lifecycle,
@NonNull Recipient recipient,
@NonNull Runnable onReportSpam,
@Nullable Runnable onBlockAndReportSpam)
{
SimpleTask.run(lifecycle,
() -> buildReportSpamFor(context, recipient, onReportSpam, onBlockAndReportSpam),
AlertDialog.Builder::show);
}
public static void showBlockFor(@NonNull Context context,
@NonNull Lifecycle lifecycle,
@@ -137,4 +157,37 @@ public final class BlockUnblockDialog {
return builder;
}
@WorkerThread
private static AlertDialog.Builder buildReportSpamFor(@NonNull Context context,
@NonNull Recipient recipient,
@NonNull Runnable onReportSpam,
@Nullable Runnable onBlockAndReportSpam)
{
recipient = recipient.resolve();
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(context)
.setTitle(R.string.BlockUnblockDialog_report_spam_title)
.setPositiveButton(R.string.BlockUnblockDialog_report_spam, (d, w) -> onReportSpam.run());
if (onBlockAndReportSpam != null) {
builder.setNeutralButton(android.R.string.cancel, null)
.setNegativeButton(R.string.BlockUnblockDialog_report_spam_and_block, (d, w) -> onBlockAndReportSpam.run());
} else {
builder.setNegativeButton(android.R.string.cancel, null);
}
if (recipient.isGroup()) {
Recipient adder = SignalDatabase.groups().getGroupInviter(recipient.requireGroupId());
if (adder != null) {
builder.setMessage(context.getString(R.string.BlockUnblockDialog_report_spam_group_named_adder, adder.getDisplayName(context)));
} else {
builder.setMessage(R.string.BlockUnblockDialog_report_spam_group_unknown_adder);
}
} else {
builder.setMessage(R.string.BlockUnblockDialog_report_spam_description);
}
return builder;
}
}

View File

@@ -24,14 +24,17 @@ import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.signal.core.util.DimensionUnit;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.ContactFilterView;
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DisplayMetricsUtil;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.Util;
@@ -99,6 +102,10 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
private void initializeContactFilterView() {
this.contactFilterView = findViewById(R.id.contact_filter_edit_text);
if (getResources().getDisplayMetrics().heightPixels >= DimensionUnit.DP.toPixels(600)) {
this.contactFilterView.focusAndShowKeyboard();
}
}
private void initializeToolbar() {

View File

@@ -27,6 +27,8 @@ class ContactSelectionListAdapter(
registerFactory(RefreshContactsModel::class.java, LayoutFactory({ RefreshContactsViewHolder(it, onClickCallbacks::onRefreshContactsClicked) }, R.layout.contact_selection_refresh_action_item))
registerFactory(MoreHeaderModel::class.java, LayoutFactory({ MoreHeaderViewHolder(it) }, R.layout.contact_search_section_header))
registerFactory(EmptyModel::class.java, LayoutFactory({ EmptyViewHolder(it) }, R.layout.contact_selection_empty_state))
registerFactory(FindByUsernameModel::class.java, LayoutFactory({ FindByUsernameViewHolder(it, onClickCallbacks::onFindByUsernameClicked) }, R.layout.contact_selection_find_by_username_item))
registerFactory(FindByPhoneNumberModel::class.java, LayoutFactory({ FindByPhoneNumberViewHolder(it, onClickCallbacks::onFindByPhoneNumberClicked) }, R.layout.contact_selection_find_by_phone_number_item))
}
class NewGroupModel : MappingModel<NewGroupModel> {
@@ -44,6 +46,16 @@ class ContactSelectionListAdapter(
override fun areContentsTheSame(newItem: RefreshContactsModel): Boolean = true
}
class FindByUsernameModel : MappingModel<FindByUsernameModel> {
override fun areItemsTheSame(newItem: FindByUsernameModel): Boolean = true
override fun areContentsTheSame(newItem: FindByUsernameModel): Boolean = true
}
class FindByPhoneNumberModel : MappingModel<FindByPhoneNumberModel> {
override fun areItemsTheSame(newItem: FindByPhoneNumberModel): Boolean = true
override fun areContentsTheSame(newItem: FindByPhoneNumberModel): Boolean = true
}
class MoreHeaderModel : MappingModel<MoreHeaderModel> {
override fun areItemsTheSame(newItem: MoreHeaderModel): Boolean = true
@@ -92,13 +104,33 @@ class ContactSelectionListAdapter(
}
}
private class FindByPhoneNumberViewHolder(itemView: View, onClickListener: () -> Unit) : MappingViewHolder<FindByPhoneNumberModel>(itemView) {
init {
itemView.setOnClickListener { onClickListener() }
}
override fun bind(model: FindByPhoneNumberModel) = Unit
}
private class FindByUsernameViewHolder(itemView: View, onClickListener: () -> Unit) : MappingViewHolder<FindByUsernameModel>(itemView) {
init {
itemView.setOnClickListener { onClickListener() }
}
override fun bind(model: FindByUsernameModel) = Unit
}
class ArbitraryRepository : org.thoughtcrime.securesms.contacts.paged.ArbitraryRepository {
enum class ArbitraryRow(val code: String) {
NEW_GROUP("new-group"),
INVITE_TO_SIGNAL("invite-to-signal"),
MORE_HEADING("more-heading"),
REFRESH_CONTACTS("refresh-contacts");
REFRESH_CONTACTS("refresh-contacts"),
FIND_BY_USERNAME("find-by-username"),
FIND_BY_PHONE_NUMBER("find-by-phone-number");
companion object {
fun fromCode(code: String) = values().first { it.code == code }
@@ -120,6 +152,8 @@ class ContactSelectionListAdapter(
ArbitraryRow.INVITE_TO_SIGNAL -> InviteToSignalModel()
ArbitraryRow.MORE_HEADING -> MoreHeaderModel()
ArbitraryRow.REFRESH_CONTACTS -> RefreshContactsModel()
ArbitraryRow.FIND_BY_PHONE_NUMBER -> FindByPhoneNumberModel()
ArbitraryRow.FIND_BY_USERNAME -> FindByUsernameModel()
}
}
}
@@ -128,5 +162,7 @@ class ContactSelectionListAdapter(
fun onNewGroupClicked()
fun onInviteToSignalClicked()
fun onRefreshContactsClicked()
fun onFindByPhoneNumberClicked()
fun onFindByUsernameClicked()
}
}

View File

@@ -76,6 +76,7 @@ import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.UsernameAci
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.UsernameUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
@@ -141,6 +142,7 @@ public final class ContactSelectionListFragment extends LoggingFragment {
private ContactSearchMediator contactSearchMediator;
@Nullable private NewConversationCallback newConversationCallback;
@Nullable private FindByCallback findByCallback;
@Nullable private NewCallCallback newCallCallback;
@Nullable private ScrollCallback scrollCallback;
@Nullable private OnItemLongClickListener onItemLongClickListener;
@@ -161,6 +163,10 @@ public final class ContactSelectionListFragment extends LoggingFragment {
newConversationCallback = (NewConversationCallback) context;
}
if (context instanceof FindByCallback) {
findByCallback = (FindByCallback) context;
}
if (context instanceof NewCallCallback) {
newCallCallback = (NewCallCallback) context;
}
@@ -379,6 +385,16 @@ public final class ContactSelectionListFragment extends LoggingFragment {
newConversationCallback.onNewGroup(false);
}
@Override
public void onFindByPhoneNumberClicked() {
findByCallback.onFindByPhoneNumber();
}
@Override
public void onFindByUsernameClicked() {
findByCallback.onFindByUsername();
}
@Override
public void onInviteToSignalClicked() {
if (newConversationCallback != null) {
@@ -660,6 +676,10 @@ public final class ContactSelectionListFragment extends LoggingFragment {
}
}
public void addRecipientToSelectionIfAble(@NonNull RecipientId recipientId) {
listClickListener.onItemClick(new ContactSearchKey.RecipientSearchKey(recipientId, false));
}
private class ListClickListener {
public void onItemClick(ContactSearchKey contact) {
boolean isUnknown = contact instanceof ContactSearchKey.UnknownRecipientKey;
@@ -874,6 +894,11 @@ public final class ContactSelectionListFragment extends LoggingFragment {
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.NEW_GROUP.getCode());
}
if (findByCallback != null) {
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.FIND_BY_USERNAME.getCode());
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.FIND_BY_PHONE_NUMBER.getCode());
}
if (transportType != null) {
if (!hasQuery && includeRecents) {
builder.addSection(new ContactSearchConfiguration.Section.Recents(
@@ -891,7 +916,7 @@ public final class ContactSelectionListFragment extends LoggingFragment {
builder.addSection(new ContactSearchConfiguration.Section.Individuals(
includeSelf,
transportType,
newCallCallback == null,
newCallCallback == null && findByCallback == null,
null,
!hideLetterHeaders()
));
@@ -1011,6 +1036,12 @@ public final class ContactSelectionListFragment extends LoggingFragment {
void onNewGroup(boolean forceV1);
}
public interface FindByCallback {
void onFindByUsername();
void onFindByPhoneNumber();
}
public interface NewCallCallback {
void onInvite();
}

View File

@@ -38,7 +38,6 @@ public class DeviceProvisioningActivity extends PassphraseRequiredActivity {
.setOnDismissListener(dialog13 -> finish())
.create();
dialog.setIcon(getResources().getDrawable(R.drawable.icon_dialog));
dialog.show();
}
}

View File

@@ -54,7 +54,6 @@ public final class GroupMembersDialog {
}
private void contactClick(@NonNull Recipient recipient) {
RecipientBottomSheetDialogFragment.create(recipient.getId(), groupRecipient.requireGroupId())
.show(fragmentActivity.getSupportFragmentManager(), "BOTTOM");
RecipientBottomSheetDialogFragment.show(fragmentActivity.getSupportFragmentManager(), recipient.getId(), groupRecipient.requireGroupId());
}
}

View File

@@ -17,17 +17,16 @@ import android.widget.Toast;
import androidx.annotation.AnimRes;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.concurrent.ListenableFuture.Listener;
import org.thoughtcrime.securesms.components.ContactFilterView;
import org.thoughtcrime.securesms.components.ContactFilterView.OnFilterChangedListener;
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
import org.thoughtcrime.securesms.contacts.SelectedContact;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.mms.OutgoingMessage;
@@ -38,7 +37,6 @@ import org.thoughtcrime.securesms.util.DynamicNoActionBarInviteTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.Util;
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;

View File

@@ -50,6 +50,8 @@ import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.ui.findby.FindByActivity;
import org.thoughtcrime.securesms.recipients.ui.findby.FindByMode;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
@@ -70,14 +72,16 @@ import io.reactivex.rxjava3.disposables.Disposable;
* @author Moxie Marlinspike
*/
public class NewConversationActivity extends ContactSelectionActivity
implements ContactSelectionListFragment.NewConversationCallback, ContactSelectionListFragment.OnItemLongClickListener
implements ContactSelectionListFragment.NewConversationCallback, ContactSelectionListFragment.OnItemLongClickListener, ContactSelectionListFragment.FindByCallback
{
@SuppressWarnings("unused")
private static final String TAG = Log.tag(NewConversationActivity.class);
private ContactsManagementViewModel viewModel;
private ActivityResultLauncher<Intent> contactLauncher;
private ContactsManagementViewModel viewModel;
private ActivityResultLauncher<Intent> contactLauncher;
private ActivityResultLauncher<Intent> createGroupLauncher;
private ActivityResultLauncher<FindByMode> findByLauncher;
private final LifecycleDisposable disposables = new LifecycleDisposable();
@@ -99,6 +103,18 @@ public class NewConversationActivity extends ContactSelectionActivity
}
});
findByLauncher = registerForActivityResult(new FindByActivity.Contract(), result -> {
if (result != null) {
launch(result);
}
});
createGroupLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
if (result.getResultCode() == RESULT_OK) {
finish();
}
});
viewModel = new ViewModelProvider(this, factory).get(ContactsManagementViewModel.class);
}
@@ -163,7 +179,12 @@ public class NewConversationActivity extends ContactSelectionActivity
}
private void launch(Recipient recipient) {
Disposable disposable = ConversationIntents.createBuilder(this, recipient.getId(), -1L)
launch(recipient.getId());
}
private void launch(RecipientId recipientId) {
Disposable disposable = ConversationIntents.createBuilder(this, recipientId, -1L)
.map(builder -> builder
.withDraftText(getIntent().getStringExtra(Intent.EXTRA_TEXT))
.withDataUri(getIntent().getData())
@@ -206,7 +227,7 @@ public class NewConversationActivity extends ContactSelectionActivity
}
private void handleCreateGroup() {
startActivity(CreateGroupActivity.newIntent(this));
createGroupLauncher.launch(CreateGroupActivity.newIntent(this));
}
private void handleInvite() {
@@ -231,7 +252,17 @@ public class NewConversationActivity extends ContactSelectionActivity
@Override
public void onNewGroup(boolean forceV1) {
handleCreateGroup();
finish();
// finish();
}
@Override
public void onFindByUsername() {
findByLauncher.launch(FindByMode.USERNAME);
}
@Override
public void onFindByPhoneNumber() {
findByLauncher.launch(FindByMode.PHONE_NUMBER);
}
@Override

View File

@@ -374,7 +374,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
Log.i(TAG, "onAuthenticationSucceeded");
fingerprintPrompt.setImageResource(R.drawable.ic_check_white_48dp);
fingerprintPrompt.setImageResource(R.drawable.symbol_check_white_48);
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.green_500), PorterDuff.Mode.SRC_IN);
fingerprintPrompt.animate().setInterpolator(new BounceInterpolator()).scaleX(1.1f).scaleY(1.1f).setDuration(500).setListener(new AnimationCompleteListener() {
@Override
@@ -388,7 +388,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
public void onAuthenticationFailed() {
Log.w(TAG, "onAuthenticationFailed()");
fingerprintPrompt.setImageResource(R.drawable.ic_close_white_48dp);
fingerprintPrompt.setImageResource(R.drawable.symbol_x_white_48);
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.red_500), PorterDuff.Mode.SRC_IN);
TranslateAnimation shake = new TranslateAnimation(0, 30, 0, 0);

View File

@@ -1,31 +0,0 @@
package org.thoughtcrime.securesms;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Button;
import org.thoughtcrime.securesms.preferences.MmsPreferencesActivity;
public class PromptMmsActivity extends PassphraseRequiredActivity {
@Override
protected void onCreate(Bundle bundle, boolean ready) {
setContentView(R.layout.prompt_apn_activity);
initializeResources();
}
private void initializeResources() {
Button okButton = findViewById(R.id.ok_button);
Button cancelButton = findViewById(R.id.cancel_button);
okButton.setOnClickListener(v -> {
Intent intent = new Intent(PromptMmsActivity.this, MmsPreferencesActivity.class);
intent.putExtras(PromptMmsActivity.this.getIntent().getExtras());
startActivity(intent);
finish();
});
cancelButton.setOnClickListener(v -> finish());
}
}

View File

@@ -765,7 +765,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
if (isFinishing()) return; // XXX Stuart added this check above, not sure why, so I'm repeating in ignorance. - moxie
new MaterialAlertDialogBuilder(this)
.setTitle(R.string.RedPhone_number_not_registered)
.setIcon(R.drawable.ic_warning)
.setIcon(R.drawable.symbol_error_triangle_fill_24)
.setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice)
.setCancelable(true)
.setPositiveButton(R.string.RedPhone_got_it, (d, w) -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))

View File

@@ -8,12 +8,12 @@ import android.widget.TextView
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.setPadding
import com.airbnb.lottie.SimpleColorFilter
import com.bumptech.glide.Glide
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.avatar.Avatar
import org.thoughtcrime.securesms.avatar.AvatarRenderer
import org.thoughtcrime.securesms.avatar.Avatars
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader
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
@@ -132,12 +132,12 @@ object AvatarPickerItem {
}
is Avatar.Photo -> {
textView.visible = false
GlideApp.with(imageView).load(DecryptableStreamUriLoader.DecryptableUri(model.avatar.uri)).into(imageView)
Glide.with(imageView).load(DecryptableStreamUriLoader.DecryptableUri(model.avatar.uri)).into(imageView)
}
is Avatar.Resource -> {
imageView.setPadding((imageView.width * 0.2).toInt())
textView.visible = false
GlideApp.with(imageView).clear(imageView)
Glide.with(imageView).clear(imageView)
imageView.setImageResource(model.avatar.resourceId)
imageView.colorFilter = SimpleColorFilter(model.avatar.color.foregroundColor)
imageView.background.colorFilter = SimpleColorFilter(model.avatar.color.backgroundColor)

View File

@@ -5,10 +5,10 @@ import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import androidx.core.content.res.use
import com.bumptech.glide.RequestManager
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.AvatarImageView
import org.thoughtcrime.securesms.database.model.StoryViewState
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.util.visible
@@ -76,7 +76,7 @@ class AvatarView @JvmOverloads constructor(
/**
* Displays Note-to-Self
*/
fun displayChatAvatar(requestManager: GlideRequests, recipient: Recipient, isQuickContactEnabled: Boolean) {
fun displayChatAvatar(requestManager: RequestManager, recipient: Recipient, isQuickContactEnabled: Boolean) {
avatar.setAvatar(requestManager, recipient, isQuickContactEnabled)
}

View File

@@ -160,7 +160,7 @@ public class FullBackupExporter extends FullBackupBase {
for (String table : tables) {
throwIfCanceled(cancellationSignal);
if (table.equals(MessageTable.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isNonExpiringMessage(cursor), null, count, estimatedCount, cancellationSignal);
count = exportTable(table, input, outputStream, cursor -> isNonExpiringMessage(input, cursor), null, count, estimatedCount, cancellationSignal);
} else if (table.equals(ReactionTable.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, CursorUtil.requireLong(cursor, ReactionTable.MESSAGE_ID)), null, count, estimatedCount, cancellationSignal);
} else if (table.equals(MentionTable.TABLE_NAME)) {
@@ -579,25 +579,34 @@ public class FullBackupExporter extends FullBackupBase {
return count;
}
private static boolean isNonExpiringMessage(@NonNull Cursor cursor) {
long expiresIn = CursorUtil.requireLong(cursor, MessageTable.EXPIRES_IN);
boolean viewOnce = CursorUtil.requireBoolean(cursor, MessageTable.VIEW_ONCE);
private static boolean isNonExpiringMessage(@NonNull SQLiteDatabase db, @NonNull Cursor cursor) {
long id = CursorUtil.requireLong(cursor, MessageTable.ID);
long expireStarted = CursorUtil.requireLong(cursor, MessageTable.EXPIRE_STARTED);
long expiresIn = CursorUtil.requireLong(cursor, MessageTable.EXPIRES_IN);
long latestRevisionId = CursorUtil.requireLong(cursor, MessageTable.LATEST_REVISION_ID);
if (expiresIn == 0 && !viewOnce) {
return true;
long expiresAt = expireStarted + expiresIn;
long timeRemaining = expiresAt - System.currentTimeMillis();
if (latestRevisionId > 0 && latestRevisionId != id ) {
return isForNonExpiringMessage(db, latestRevisionId);
}
return expiresIn > EXPIRATION_BACKUP_THRESHOLD;
if (expireStarted > 0 && timeRemaining <= EXPIRATION_BACKUP_THRESHOLD) {
return false;
}
return true;
}
private static boolean isForNonExpiringMessage(@NonNull SQLiteDatabase db, long messageId) {
String[] columns = new String[] { MessageTable.EXPIRES_IN, MessageTable.VIEW_ONCE };
String[] columns = new String[] { MessageTable.ID, MessageTable.EXPIRE_STARTED, MessageTable.EXPIRES_IN, MessageTable.LATEST_REVISION_ID };
String where = MessageTable.ID + " = ?";
String[] args = SqlUtil.buildArgs(messageId);
try (Cursor mmsCursor = db.query(MessageTable.TABLE_NAME, columns, where, args, null, null, null)) {
if (mmsCursor != null && mmsCursor.moveToFirst()) {
return isNonExpiringMessage(mmsCursor);
return isNonExpiringMessage(db, mmsCursor);
}
}

View File

@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase.Companion.recipients
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
@@ -56,7 +57,7 @@ object AccountDataProcessor {
readReceipts = TextSecurePreferences.isReadReceiptsEnabled(context),
sealedSenderIndicators = TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context),
linkPreviews = SignalStore.settings().isLinkPreviewsEnabled,
notDiscoverableByPhoneNumber = SignalStore.phoneNumberPrivacy().phoneNumberDiscoverabilityMode.isUndiscoverable,
notDiscoverableByPhoneNumber = SignalStore.phoneNumberPrivacy().phoneNumberDiscoverabilityMode == PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE,
phoneNumberSharingMode = SignalStore.phoneNumberPrivacy().phoneNumberSharingMode.toBackupPhoneNumberSharingMode(),
preferContactAvatars = SignalStore.settings().isPreferSystemContactPhotos,
universalExpireTimer = SignalStore.settings().universalExpireTimer,
@@ -86,7 +87,7 @@ object AccountDataProcessor {
TextSecurePreferences.setTypingIndicatorsEnabled(context, settings.typingIndicators)
TextSecurePreferences.setShowUnidentifiedDeliveryIndicatorsEnabled(context, settings.sealedSenderIndicators)
SignalStore.settings().isLinkPreviewsEnabled = settings.linkPreviews
SignalStore.phoneNumberPrivacy().phoneNumberDiscoverabilityMode = if (settings.notDiscoverableByPhoneNumber) PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE else PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode.DISCOVERABLE
SignalStore.phoneNumberPrivacy().phoneNumberDiscoverabilityMode = if (settings.notDiscoverableByPhoneNumber) PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE else PhoneNumberDiscoverabilityMode.DISCOVERABLE
SignalStore.phoneNumberPrivacy().phoneNumberSharingMode = settings.phoneNumberSharingMode.toLocalPhoneNumberMode()
SignalStore.settings().isPreferSystemContactPhotos = settings.preferContactAvatars
SignalStore.settings().universalExpireTimer = settings.universalExpireTimer

View File

@@ -0,0 +1,261 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.updateLayoutParams
import kotlinx.collections.immutable.persistentListOf
import org.signal.core.ui.BottomSheets
import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
import org.thoughtcrime.securesms.databinding.PaypalButtonBinding
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import java.math.BigDecimal
import java.util.Currency
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MessageBackupsCheckoutSheet(
messageBackupsType: MessageBackupsType,
availablePaymentGateways: List<GatewayResponse.Gateway>,
onDismissRequest: () -> Unit,
onPaymentGatewaySelected: (GatewayResponse.Gateway) -> Unit
) {
ModalBottomSheet(
onDismissRequest = onDismissRequest,
dragHandle = { BottomSheets.Handle() },
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
) {
SheetContent(
messageBackupsType = messageBackupsType,
availablePaymentGateways = availablePaymentGateways,
onPaymentGatewaySelected = onPaymentGatewaySelected
)
}
}
@Composable
private fun SheetContent(
messageBackupsType: MessageBackupsType,
availablePaymentGateways: List<GatewayResponse.Gateway>,
onPaymentGatewaySelected: (GatewayResponse.Gateway) -> Unit
) {
val resources = LocalContext.current.resources
val formattedPrice = remember(messageBackupsType.pricePerMonth) {
FiatMoneyUtil.format(resources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
}
Text(
text = "Pay $formattedPrice/month to Signal", // TODO [message-backups] Finalized copy
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(top = 48.dp)
)
Text(
text = "You'll get:", // TODO [message-backups] Finalized copy
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 5.dp)
)
MessageBackupsTypeBlock(
messageBackupsType = messageBackupsType,
isSelected = false,
onSelected = {},
enabled = false,
modifier = Modifier.padding(top = 24.dp)
)
Column(
verticalArrangement = spacedBy(12.dp),
modifier = Modifier.padding(top = 48.dp, bottom = 24.dp)
) {
availablePaymentGateways.forEach {
when (it) {
GatewayResponse.Gateway.GOOGLE_PAY -> GooglePayButton {
onPaymentGatewaySelected(GatewayResponse.Gateway.GOOGLE_PAY)
}
GatewayResponse.Gateway.PAYPAL -> PayPalButton {
onPaymentGatewaySelected(GatewayResponse.Gateway.PAYPAL)
}
GatewayResponse.Gateway.CREDIT_CARD -> CreditOrDebitCardButton {
onPaymentGatewaySelected(GatewayResponse.Gateway.CREDIT_CARD)
}
GatewayResponse.Gateway.SEPA_DEBIT -> SepaButton {
onPaymentGatewaySelected(GatewayResponse.Gateway.SEPA_DEBIT)
}
GatewayResponse.Gateway.IDEAL -> IdealButton {
onPaymentGatewaySelected(GatewayResponse.Gateway.IDEAL)
}
}
}
}
}
@Composable
private fun PayPalButton(
onClick: () -> Unit
) {
AndroidView(factory = {
val view = LayoutInflater.from(it).inflate(R.layout.paypal_button, null)
view.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
view
}) {
val binding = PaypalButtonBinding.bind(it)
binding.paypalButton.updateLayoutParams<ViewGroup.MarginLayoutParams> {
marginStart = 0
marginEnd = 0
}
binding.paypalButton.setOnClickListener {
onClick()
}
}
}
@Composable
private fun GooglePayButton(
onClick: () -> Unit
) {
val model = GooglePayButton.Model(onClick, true)
AndroidView(factory = {
LayoutInflater.from(it).inflate(R.layout.google_pay_button_pref, null)
}) {
val holder = GooglePayButton.ViewHolder(it)
holder.bind(model)
}
}
@Composable
private fun SepaButton(
onClick: () -> Unit
) {
Buttons.LargeTonal(
onClick = onClick,
modifier = Modifier.fillMaxWidth()
) {
Icon(
painter = painterResource(id = R.drawable.bank_transfer),
contentDescription = null,
modifier = Modifier.padding(end = 8.dp)
)
Text(text = stringResource(id = R.string.GatewaySelectorBottomSheet__bank_transfer))
}
}
@Composable
private fun IdealButton(
onClick: () -> Unit
) {
Buttons.LargeTonal(
onClick = onClick,
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(id = R.drawable.logo_ideal),
contentDescription = null,
modifier = Modifier
.size(32.dp)
.padding(end = 8.dp)
)
Text(text = stringResource(id = R.string.GatewaySelectorBottomSheet__ideal))
}
}
@Composable
private fun CreditOrDebitCardButton(
onClick: () -> Unit
) {
Buttons.LargePrimary(
onClick = onClick,
modifier = Modifier.fillMaxWidth()
) {
Icon(
painter = painterResource(id = R.drawable.credit_card),
contentDescription = null,
modifier = Modifier.padding(end = 8.dp)
)
Text(
text = stringResource(id = R.string.GatewaySelectorBottomSheet__credit_or_debit_card)
)
}
}
@Preview
@Composable
private fun MessageBackupsCheckoutSheetPreview() {
val paidTier = MessageBackupsType(
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("USD")),
title = "Text + All your media",
features = persistentListOf(
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "Full text message backup"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_album_compact_bold_16,
label = "Full media backup"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "1TB of storage (~250K photos)"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_heart_compact_bold_16,
label = "Thanks for supporting Signal!"
)
)
)
val availablePaymentGateways = GatewayResponse.Gateway.values().toList()
Previews.Preview {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
) {
SheetContent(
messageBackupsType = paidTier,
availablePaymentGateways = availablePaymentGateways,
onPaymentGatewaySelected = {}
)
}
}
}

View File

@@ -0,0 +1,187 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.signal.core.ui.Scaffolds
import org.signal.core.ui.theme.SignalTheme
import org.thoughtcrime.securesms.R
/**
* Educational content which allows user to proceed to set up automatic backups
* or navigate to a support page to learn more.
*/
@Composable
fun MessageBackupsEducationScreen(
onNavigationClick: () -> Unit,
onEnableBackups: () -> Unit,
onLearnMore: () -> Unit
) {
Scaffolds.Settings(
onNavigationClick = onNavigationClick,
navigationIconPainter = painterResource(id = R.drawable.symbol_x_24),
title = "Chat backups" // TODO [message-backups] Finalized copy
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(it)
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
) {
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
item {
Image(
painter = painterResource(id = R.drawable.ic_signal_logo_large), // TODO [message-backups] Final image asset
contentDescription = null,
modifier = Modifier
.padding(top = 48.dp)
.size(88.dp)
)
}
item {
Text(
text = "Chat Backups", // TODO [message-backups] Finalized copy
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(top = 15.dp)
)
}
item {
Text(
text = "Back up your messages and media and using Signals secure, end-to-end encrypted storage service. Never lose a message when you get a new phone or reinstall Signal.", // TODO [message-backups] Finalized copy
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 12.dp)
)
}
item {
Column(
modifier = Modifier.padding(top = 32.dp),
verticalArrangement = Arrangement.spacedBy(20.dp)
) {
NotableFeatureRow(
painter = painterResource(id = R.drawable.symbol_lock_compact_20),
text = "End-to-end Encrypted" // TODO [message-backups] Finalized copy
)
NotableFeatureRow(
painter = painterResource(id = R.drawable.symbol_check_square_compact_20),
text = "Optional, always" // TODO [message-backups] Finalized copy
)
NotableFeatureRow(
painter = painterResource(id = R.drawable.symbol_trash_compact_20),
text = "Delete your backup anytime" // TODO [message-backups] Finalized copy
)
}
}
}
Buttons.LargePrimary(
onClick = onEnableBackups,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "Enable backups" // TODO [message-backups] Finalized copy
)
}
TextButton(
onClick = onLearnMore,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
) {
Text(
text = "Learn more" // TODO [message-backups] Finalized copy
)
}
}
}
}
@Preview
@Composable
private fun MessageBackupsEducationSheetPreview() {
Previews.Preview {
MessageBackupsEducationScreen(
onNavigationClick = {},
onEnableBackups = {},
onLearnMore = {}
)
}
}
@Preview
@Composable
private fun NotableFeatureRowPreview() {
Previews.Preview {
NotableFeatureRow(
painter = painterResource(id = R.drawable.symbol_lock_compact_20),
text = "Notable feature information"
)
}
}
@Composable
private fun NotableFeatureRow(
painter: Painter,
text: String
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painter,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.padding(end = 8.dp)
.size(32.dp)
.background(color = SignalTheme.colors.colorSurface2, shape = CircleShape)
.padding(6.dp)
)
Text(
text = text,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.dialog
import androidx.navigation.compose.rememberNavController
import org.signal.core.ui.theme.SignalTheme
import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.util.viewModel
class MessageBackupsFlowActivity : PassphraseRequiredActivity() {
private val viewModel: MessageBackupsFlowViewModel by viewModel { MessageBackupsFlowViewModel() }
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
setContent {
SignalTheme {
val state by viewModel.state
val navController = rememberNavController()
fun MessageBackupsScreen.next() {
val nextScreen = viewModel.goToNextScreen(this)
if (nextScreen != this) {
navController.navigate(nextScreen.name)
}
}
fun NavController.popOrFinish() {
if (popBackStack()) {
return
}
finishAfterTransition()
}
LaunchedEffect(Unit) {
navController.setLifecycleOwner(this@MessageBackupsFlowActivity)
navController.setOnBackPressedDispatcher(this@MessageBackupsFlowActivity.onBackPressedDispatcher)
navController.enableOnBackPressed(true)
}
NavHost(
navController = navController,
startDestination = MessageBackupsScreen.EDUCATION.name,
enterTransition = { slideInHorizontally(initialOffsetX = { it }) },
exitTransition = { slideOutHorizontally(targetOffsetX = { -it }) },
popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }) },
popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) }
) {
composable(route = MessageBackupsScreen.EDUCATION.name) {
MessageBackupsEducationScreen(
onNavigationClick = navController::popOrFinish,
onEnableBackups = { MessageBackupsScreen.EDUCATION.next() },
onLearnMore = {}
)
}
composable(route = MessageBackupsScreen.PIN_EDUCATION.name) {
MessageBackupsPinEducationScreen(
onNavigationClick = navController::popOrFinish,
onGeneratePinClick = {},
onUseCurrentPinClick = { MessageBackupsScreen.PIN_EDUCATION.next() },
recommendedPinSize = 16 // TODO [message-backups] This value should come from some kind of config
)
}
composable(route = MessageBackupsScreen.PIN_CONFIRMATION.name) {
MessageBackupsPinConfirmationScreen(
pin = state.pin,
onPinChanged = viewModel::onPinEntryUpdated,
pinKeyboardType = state.pinKeyboardType,
onPinKeyboardTypeSelected = viewModel::onPinKeyboardTypeUpdated,
onNextClick = { MessageBackupsScreen.PIN_CONFIRMATION.next() }
)
}
composable(route = MessageBackupsScreen.TYPE_SELECTION.name) {
MessageBackupsTypeSelectionScreen(
selectedBackupsType = state.selectedMessageBackupsType,
availableBackupsTypes = state.availableBackupsTypes,
onMessageBackupsTypeSelected = viewModel::onMessageBackupsTypeUpdated,
onNavigationClick = navController::popOrFinish,
onReadMoreClicked = {},
onNextClicked = { MessageBackupsScreen.TYPE_SELECTION.next() }
)
}
dialog(route = MessageBackupsScreen.CHECKOUT_SHEET.name) {
MessageBackupsCheckoutSheet(
messageBackupsType = state.selectedMessageBackupsType!!,
availablePaymentGateways = state.availablePaymentGateways,
onDismissRequest = navController::popOrFinish,
onPaymentGatewaySelected = {
viewModel.onPaymentGatewayUpdated(it)
MessageBackupsScreen.CHECKOUT_SHEET.next()
}
)
}
}
}
}
}
}

View File

@@ -0,0 +1,8 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
class MessageBackupsFlowRepository

View File

@@ -0,0 +1,19 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
data class MessageBackupsFlowState(
val selectedMessageBackupsType: MessageBackupsType? = null,
val availableBackupsTypes: List<MessageBackupsType> = emptyList(),
val selectedPaymentGateway: GatewayResponse.Gateway? = null,
val availablePaymentGateways: List<GatewayResponse.Gateway> = emptyList(),
val pin: String = "",
val pinKeyboardType: PinKeyboardType = SignalStore.pinValues().keyboardType
)

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
class MessageBackupsFlowViewModel : ViewModel() {
private val internalState = mutableStateOf(MessageBackupsFlowState())
val state: State<MessageBackupsFlowState> = internalState
fun goToNextScreen(currentScreen: MessageBackupsScreen): MessageBackupsScreen {
return when (currentScreen) {
MessageBackupsScreen.EDUCATION -> MessageBackupsScreen.PIN_EDUCATION
MessageBackupsScreen.PIN_EDUCATION -> MessageBackupsScreen.PIN_CONFIRMATION
MessageBackupsScreen.PIN_CONFIRMATION -> validatePinAndUpdateState()
MessageBackupsScreen.TYPE_SELECTION -> validateTypeAndUpdateState()
MessageBackupsScreen.CHECKOUT_SHEET -> validateGatewayAndUpdateState()
MessageBackupsScreen.PROCESS_PAYMENT -> MessageBackupsScreen.COMPLETED
MessageBackupsScreen.COMPLETED -> error("Unsupported state transition from terminal state COMPLETED")
}
}
fun onPinEntryUpdated(pin: String) {
internalState.value = state.value.copy(pin = pin)
}
fun onPinKeyboardTypeUpdated(pinKeyboardType: PinKeyboardType) {
internalState.value = state.value.copy(pinKeyboardType = pinKeyboardType)
}
fun onPaymentGatewayUpdated(gateway: GatewayResponse.Gateway) {
internalState.value = state.value.copy(selectedPaymentGateway = gateway)
}
fun onMessageBackupsTypeUpdated(messageBackupsType: MessageBackupsType) {
internalState.value = state.value.copy(selectedMessageBackupsType = messageBackupsType)
}
private fun validatePinAndUpdateState(): MessageBackupsScreen {
return MessageBackupsScreen.TYPE_SELECTION
}
private fun validateTypeAndUpdateState(): MessageBackupsScreen {
return MessageBackupsScreen.CHECKOUT_SHEET
}
private fun validateGatewayAndUpdateState(): MessageBackupsScreen {
return MessageBackupsScreen.PROCESS_PAYMENT
}
}

View File

@@ -0,0 +1,198 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
/**
* Screen which requires the user to enter their pin before enabling backups.
*/
@Composable
fun MessageBackupsPinConfirmationScreen(
pin: String,
onPinChanged: (String) -> Unit,
pinKeyboardType: PinKeyboardType,
onPinKeyboardTypeSelected: (PinKeyboardType) -> Unit,
onNextClick: () -> Unit
) {
val focusRequester = remember { FocusRequester() }
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
) {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
item {
Text(
text = "Enter your PIN", // TODO [message-backups] Finalized copy
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(top = 40.dp)
)
}
item {
Text(
text = "Enter your Signal PIN to enable backups", // TODO [message-backups] Finalized copy
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 16.dp)
)
}
item {
// TODO [message-backups] Confirm default focus state
val keyboardType = remember(pinKeyboardType) {
when (pinKeyboardType) {
PinKeyboardType.NUMERIC -> KeyboardType.NumberPassword
PinKeyboardType.ALPHA_NUMERIC -> KeyboardType.Password
}
}
TextField(
value = pin,
onValueChange = onPinChanged,
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
keyboardActions = KeyboardActions(
onDone = { onNextClick() }
),
keyboardOptions = KeyboardOptions(
keyboardType = keyboardType
),
modifier = Modifier
.padding(top = 72.dp)
.fillMaxWidth()
.focusRequester(focusRequester)
)
}
item {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxWidth()
.padding(top = 48.dp)
) {
PinKeyboardTypeToggle(
pinKeyboardType = pinKeyboardType,
onPinKeyboardTypeSelected = onPinKeyboardTypeSelected
)
}
}
}
Box(
contentAlignment = Alignment.BottomEnd,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
) {
Buttons.LargeTonal(
onClick = onNextClick
) {
Text(
text = "Next" // TODO [message-backups] Finalized copy
)
}
}
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
}
@Preview
@Composable
private fun MessageBackupsPinConfirmationScreenPreview() {
Previews.Preview {
MessageBackupsPinConfirmationScreen(
pin = "",
onPinChanged = {},
pinKeyboardType = PinKeyboardType.ALPHA_NUMERIC,
onPinKeyboardTypeSelected = {},
onNextClick = {}
)
}
}
@Preview
@Composable
private fun PinKeyboardTypeTogglePreview() {
Previews.Preview {
var type by remember { mutableStateOf(PinKeyboardType.ALPHA_NUMERIC) }
PinKeyboardTypeToggle(
pinKeyboardType = type,
onPinKeyboardTypeSelected = { type = it }
)
}
}
@Composable
private fun PinKeyboardTypeToggle(
pinKeyboardType: PinKeyboardType,
onPinKeyboardTypeSelected: (PinKeyboardType) -> Unit
) {
val callback = remember(pinKeyboardType) {
{ onPinKeyboardTypeSelected(pinKeyboardType.other) }
}
val iconRes = remember(pinKeyboardType) {
when (pinKeyboardType) {
PinKeyboardType.NUMERIC -> R.drawable.symbol_keyboard_24
PinKeyboardType.ALPHA_NUMERIC -> R.drawable.symbol_number_pad_24
}
}
TextButton(onClick = callback) {
Icon(
painter = painterResource(id = iconRes),
tint = MaterialTheme.colorScheme.primary,
contentDescription = null,
modifier = Modifier.padding(end = 8.dp)
)
Text(
text = "Switch keyboard" // TODO [message-backups] Finalized copy
)
}
}

View File

@@ -0,0 +1,132 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.signal.core.ui.Scaffolds
import org.thoughtcrime.securesms.R
/**
* Explanation screen that details how the user's pin is utilized with backups,
* and how long they should make their pin.
*/
@Composable
fun MessageBackupsPinEducationScreen(
onNavigationClick: () -> Unit,
onGeneratePinClick: () -> Unit,
onUseCurrentPinClick: () -> Unit,
recommendedPinSize: Int
) {
Scaffolds.Settings(
title = "Backup type", // TODO [message-backups] Finalized copy
onNavigationClick = onNavigationClick,
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_left_24)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(it)
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
) {
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
item {
Image(
painter = painterResource(id = R.drawable.ic_signal_logo_large), // TODO [message-backups] Finalized image
contentDescription = null,
modifier = Modifier
.padding(top = 48.dp)
.size(88.dp)
)
}
item {
Text(
text = "PINs protect your backup", // TODO [message-backups] Finalized copy
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(top = 16.dp)
)
}
item {
Text(
text = "Your Signal PIN lets you restore your backup when you re-install Signal. For increased security, we recommend updating to a new $recommendedPinSize-digit PIN.", // TODO [message-backups] Finalized copy
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(top = 16.dp)
)
}
item {
Text(
text = "If you forget your PIN, you will not be able to restore your backup. You can change your PIN at any time in settings.", // TODO [message-backups] Finalized copy
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(top = 16.dp)
)
}
}
Buttons.LargePrimary(
onClick = onGeneratePinClick,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "Generate a new $recommendedPinSize-digit PIN" // TODO [message-backups] Finalized copy
)
}
TextButton(
onClick = onUseCurrentPinClick,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
) {
Text(
text = "Use current Signal PIN" // TODO [message-backups] Finalized copy
)
}
}
}
}
@Preview
@Composable
private fun MessageBackupsPinScreenPreview() {
Previews.Preview {
MessageBackupsPinEducationScreen(
onNavigationClick = {},
onGeneratePinClick = {},
onUseCurrentPinClick = {},
recommendedPinSize = 16
)
}
}

View File

@@ -0,0 +1,16 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
enum class MessageBackupsScreen {
EDUCATION,
PIN_EDUCATION,
PIN_CONFIRMATION,
TYPE_SELECTION,
CHECKOUT_SHEET,
PROCESS_PAYMENT,
COMPLETED
}

View File

@@ -0,0 +1,302 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withAnnotation
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.signal.core.ui.Scaffolds
import org.signal.core.ui.theme.SignalTheme
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import java.math.BigDecimal
import java.util.Currency
/**
* Screen which allows the user to select their preferred backup type.
*/
@OptIn(ExperimentalTextApi::class)
@Composable
fun MessageBackupsTypeSelectionScreen(
selectedBackupsType: MessageBackupsType?,
availableBackupsTypes: List<MessageBackupsType>,
onMessageBackupsTypeSelected: (MessageBackupsType) -> Unit,
onNavigationClick: () -> Unit,
onReadMoreClicked: () -> Unit,
onNextClicked: () -> Unit
) {
Scaffolds.Settings(
title = "",
onNavigationClick = onNavigationClick,
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_left_24)
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
.fillMaxSize()
) {
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
item {
Image(
painter = painterResource(id = R.drawable.ic_signal_logo_large), // TODO [message-backups] Finalized art asset
contentDescription = null,
modifier = Modifier.size(88.dp)
)
}
item {
Text(
text = "Choose your backup type", // TODO [message-backups] Finalized copy
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(top = 12.dp)
)
}
item {
// TODO [message-backups] Finalized copy
val primaryColor = MaterialTheme.colorScheme.primary
val readMoreString = buildAnnotatedString {
append("All backups are end-to-end encrypted. Signal is a non-profit—paying for backups helps support our mission. ")
withAnnotation(tag = "URL", annotation = "read-more") {
withStyle(
style = SpanStyle(
color = primaryColor
)
) {
append("Read more")
}
}
}
ClickableText(
text = readMoreString,
style = MaterialTheme.typography.bodyLarge.copy(textAlign = TextAlign.Center),
onClick = { offset ->
readMoreString
.getStringAnnotations(tag = "URL", start = offset, end = offset)
.firstOrNull()?.let { onReadMoreClicked() }
},
modifier = Modifier.padding(top = 8.dp)
)
}
itemsIndexed(
availableBackupsTypes,
{ _, item -> item.title }
) { index, item ->
MessageBackupsTypeBlock(
messageBackupsType = item,
isSelected = item == selectedBackupsType,
onSelected = { onMessageBackupsTypeSelected(item) },
modifier = Modifier.padding(top = if (index == 0) 20.dp else 18.dp)
)
}
}
Buttons.LargePrimary(
onClick = onNextClicked,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
) {
Text(
text = "Next" // TODO [message-backups] Finalized copy
)
}
}
}
}
@Preview
@Composable
private fun MessageBackupsTypeSelectionScreenPreview() {
val freeTier = MessageBackupsType(
pricePerMonth = FiatMoney(BigDecimal.ZERO, Currency.getInstance("USD")),
title = "Text + 30 days of media",
features = persistentListOf(
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "Full text message backup"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_album_compact_bold_16,
label = "Last 30 days of media"
)
)
)
val paidTier = MessageBackupsType(
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("USD")),
title = "Text + All your media",
features = persistentListOf(
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "Full text message backup"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_album_compact_bold_16,
label = "Full media backup"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "1TB of storage (~250K photos)"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_heart_compact_bold_16,
label = "Thanks for supporting Signal!"
)
)
)
var selectedBackupsType by remember { mutableStateOf(freeTier) }
Previews.Preview {
MessageBackupsTypeSelectionScreen(
selectedBackupsType = selectedBackupsType,
availableBackupsTypes = listOf(freeTier, paidTier),
onMessageBackupsTypeSelected = { selectedBackupsType = it },
onNavigationClick = {},
onReadMoreClicked = {},
onNextClicked = {}
)
}
}
@Composable
fun MessageBackupsTypeBlock(
messageBackupsType: MessageBackupsType,
isSelected: Boolean,
onSelected: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true
) {
val borderColor = if (isSelected) {
MaterialTheme.colorScheme.primary
} else {
Color.Transparent
}
val background = if (isSelected) {
MaterialTheme.colorScheme.secondaryContainer
} else {
SignalTheme.colors.colorSurface2
}
Column(
modifier = modifier
.fillMaxWidth()
.background(color = background, shape = RoundedCornerShape(18.dp))
.border(width = 2.dp, color = borderColor, shape = RoundedCornerShape(18.dp))
.clip(shape = RoundedCornerShape(18.dp))
.clickable(onClick = onSelected, enabled = enabled)
.padding(vertical = 16.dp, horizontal = 20.dp)
) {
Text(
text = formatCostPerMonth(messageBackupsType.pricePerMonth),
style = MaterialTheme.typography.titleSmall
)
Text(
text = messageBackupsType.title,
style = MaterialTheme.typography.titleMedium
)
Column(
verticalArrangement = spacedBy(4.dp),
modifier = Modifier
.padding(top = 8.dp)
.padding(horizontal = 16.dp)
) {
messageBackupsType.features.forEach {
MessageBackupsTypeFeatureRow(messageBackupsTypeFeature = it)
}
}
}
}
@Composable
private fun formatCostPerMonth(pricePerMonth: FiatMoney): String {
return if (pricePerMonth.amount == BigDecimal.ZERO) {
"Free"
} else {
"${FiatMoneyUtil.format(LocalContext.current.resources, pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())}/month"
}
}
@Composable
private fun MessageBackupsTypeFeatureRow(messageBackupsTypeFeature: MessageBackupsTypeFeature) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Icon(
painter = painterResource(id = messageBackupsTypeFeature.iconResourceId),
contentDescription = null,
modifier = Modifier.padding(end = 8.dp)
)
Text(
text = messageBackupsTypeFeature.label,
style = MaterialTheme.typography.bodyLarge
)
}
}
data class MessageBackupsType(
val pricePerMonth: FiatMoney,
val title: String,
val features: ImmutableList<MessageBackupsTypeFeature>
)
data class MessageBackupsTypeFeature(
val iconResourceId: Int,
val label: String
)

View File

@@ -4,6 +4,8 @@ import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.content.res.use
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestManager
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.glide.BadgeSpriteTransformation
@@ -11,8 +13,6 @@ import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.components.settings.app.subscription.BadgeImageSize
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
@@ -43,35 +43,35 @@ class BadgeImageView @JvmOverloads constructor(
}
fun setBadgeFromRecipient(recipient: Recipient?) {
getGlideRequests()?.let {
getGlideRequestManager()?.let {
setBadgeFromRecipient(recipient, it)
} ?: clearDrawable()
}
fun setBadgeFromRecipient(recipient: Recipient?, glideRequests: GlideRequests) {
fun setBadgeFromRecipient(recipient: Recipient?, requestManager: RequestManager) {
if (recipient == null || recipient.badges.isEmpty()) {
setBadge(null, glideRequests)
setBadge(null, requestManager)
} else if (recipient.isSelf) {
val badge = recipient.featuredBadge
if (badge == null || !badge.visible || badge.isExpired()) {
setBadge(null, glideRequests)
setBadge(null, requestManager)
} else {
setBadge(badge, glideRequests)
setBadge(badge, requestManager)
}
} else {
setBadge(recipient.featuredBadge, glideRequests)
setBadge(recipient.featuredBadge, requestManager)
}
}
fun setBadge(badge: Badge?) {
getGlideRequests()?.let {
getGlideRequestManager()?.let {
setBadge(badge, it)
} ?: clearDrawable()
}
fun setBadge(badge: Badge?, glideRequests: GlideRequests) {
fun setBadge(badge: Badge?, requestManager: RequestManager) {
if (badge != null) {
glideRequests
requestManager
.load(badge)
.downsample(DownsampleStrategy.NONE)
.transform(BadgeSpriteTransformation(BadgeSpriteTransformation.Size.fromInteger(badgeSize), badge.imageDensity, ThemeUtil.isDarkTheme(context)))
@@ -79,21 +79,21 @@ class BadgeImageView @JvmOverloads constructor(
isClickable = true
} else {
glideRequests
requestManager
.clear(this)
clearDrawable()
}
}
fun setGiftBadge(badge: GiftBadge?, glideRequests: GlideRequests) {
fun setGiftBadge(badge: GiftBadge?, requestManager: RequestManager) {
if (badge != null) {
glideRequests
requestManager
.load(GiftBadgeModel(badge))
.downsample(DownsampleStrategy.NONE)
.transform(BadgeSpriteTransformation(BadgeSpriteTransformation.Size.fromInteger(badgeSize), ScreenDensity.getBestDensityBucketForDevice(), ThemeUtil.isDarkTheme(context)))
.into(this)
} else {
glideRequests
requestManager
.clear(this)
clearDrawable()
}
@@ -106,9 +106,9 @@ class BadgeImageView @JvmOverloads constructor(
}
}
private fun getGlideRequests(): GlideRequests? {
private fun getGlideRequestManager(): RequestManager? {
return try {
GlideApp.with(this)
Glide.with(this)
} catch (e: IllegalArgumentException) {
// View not attached to an activity or activity destroyed
null

View File

@@ -9,13 +9,13 @@ import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.content.res.use
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import com.bumptech.glide.RequestManager
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.badges.gifts.Gifts.formatExpiry
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.recipients.Recipient
/**
@@ -50,7 +50,7 @@ class GiftMessageView @JvmOverloads constructor(
}
}
fun setGiftBadge(glideRequests: GlideRequests, giftBadge: GiftBadge, isOutgoing: Boolean, callback: Callback, fromRecipient: Recipient, toRecipient: Recipient) {
fun setGiftBadge(requestManager: RequestManager, giftBadge: GiftBadge, isOutgoing: Boolean, callback: Callback, fromRecipient: Recipient, toRecipient: Recipient) {
descriptionView.text = giftBadge.formatExpiry(context)
actionView.icon = null
actionView.setOnClickListener { callback.onViewGiftBadgeClicked() }
@@ -88,7 +88,7 @@ class GiftMessageView @JvmOverloads constructor(
)
}
badgeView.setGiftBadge(giftBadge, glideRequests)
badgeView.setGiftBadge(giftBadge, requestManager)
}
fun onGiftNotOpened() {

View File

@@ -7,6 +7,7 @@ import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.compose.runtime.Stable
import com.bumptech.glide.Glide
import com.bumptech.glide.load.Key
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
@@ -14,7 +15,6 @@ import kotlinx.parcelize.Parcelize
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.glide.BadgeSpriteTransformation
import org.thoughtcrime.securesms.components.settings.PreferenceModel
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.util.ThemeUtil
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
@@ -130,7 +130,7 @@ data class Badge(
badge.alpha = if (model.badge.isExpired() || model.isFaded) 0.5f else 1f
GlideApp.with(badge)
Glide.with(badge)
.load(model.badge)
.downsample(DownsampleStrategy.NONE)
.diskCacheStrategy(DiskCacheStrategy.NONE)

View File

@@ -2,10 +2,10 @@ package org.thoughtcrime.securesms.badges.models
import android.view.View
import android.widget.TextView
import com.bumptech.glide.Glide
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
@@ -49,7 +49,7 @@ object BadgeDisplay112 {
override fun bind(model: GiftModel) {
titleView.visible = false
badgeImageView.setGiftBadge(model.giftBadge, GlideApp.with(badgeImageView))
badgeImageView.setGiftBadge(model.giftBadge, Glide.with(badgeImageView))
}
}
}

View File

@@ -7,13 +7,13 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.core.widget.TextViewCompat
import com.bumptech.glide.Glide
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.database.MessageTypes
import org.thoughtcrime.securesms.databinding.CallLogAdapterItemBinding
import org.thoughtcrime.securesms.databinding.CallLogCreateCallLinkItemBinding
import org.thoughtcrime.securesms.databinding.ConversationListItemClearFilterBinding
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.SearchUtil
@@ -272,7 +272,7 @@ class CallLogAdapter(
}
private fun presentRecipientDetails(recipient: Recipient, searchQuery: String?) {
binding.callRecipientAvatar.setAvatar(GlideApp.with(binding.callRecipientAvatar), recipient, true)
binding.callRecipientAvatar.setAvatar(Glide.with(binding.callRecipientAvatar), recipient, true)
binding.callRecipientBadge.setBadgeFromRecipient(recipient)
binding.callRecipientName.text = if (searchQuery != null) {
SearchUtil.getHighlightedSpan(

View File

@@ -40,7 +40,9 @@ class NewCallActivity : ContactSelectionActivity(), ContactSelectionListFragment
override fun onSelectionChanged() = Unit
override fun onBeforeContactSelected(isFromUnknownSearchKey: Boolean, recipientId: Optional<RecipientId?>, number: String?, callback: Consumer<Boolean?>) {
if (isFromUnknownSearchKey) {
if (recipientId.isPresent) {
launch(Recipient.resolved(recipientId.get()))
} else {
Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.")
if (SignalStore.account().isRegistered) {
Log.i(TAG, "[onContactSelected] Doing contact refresh.")

View File

@@ -16,9 +16,10 @@ import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.RequestManager;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.transfercontrols.TransferControlView;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
@@ -65,7 +66,7 @@ public class AlbumThumbnailView extends FrameLayout {
transferControlsStub = new Stub<>(findViewById(R.id.album_transfer_controls_stub));
}
public void setSlides(@NonNull GlideRequests glideRequests, @NonNull List<Slide> slides, boolean showControls) {
public void setSlides(@NonNull RequestManager requestManager, @NonNull List<Slide> slides, boolean showControls) {
if (slides.size() < 2) {
throw new IllegalStateException("Provided less than two slides.");
}
@@ -98,7 +99,7 @@ public class AlbumThumbnailView extends FrameLayout {
currentSizeClass = sizeClass;
}
showSlides(glideRequests, slides);
showSlides(requestManager, slides);
applyCorners();
forceLayout();
}
@@ -261,21 +262,21 @@ public class AlbumThumbnailView extends FrameLayout {
applyCornersForSizeClass5();
}
private void showSlides(@NonNull GlideRequests glideRequests, @NonNull List<Slide> slides) {
private void showSlides(@NonNull RequestManager requestManager, @NonNull List<Slide> slides) {
boolean showControls = TransferControlView.containsPlayableSlides(slides);
setSlide(glideRequests, slides.get(0), R.id.album_cell_1, showControls);
setSlide(glideRequests, slides.get(1), R.id.album_cell_2, showControls);
setSlide(requestManager, slides.get(0), R.id.album_cell_1, showControls);
setSlide(requestManager, slides.get(1), R.id.album_cell_2, showControls);
if (slides.size() >= 3) {
setSlide(glideRequests, slides.get(2), R.id.album_cell_3, showControls);
setSlide(requestManager, slides.get(2), R.id.album_cell_3, showControls);
}
if (slides.size() >= 4) {
setSlide(glideRequests, slides.get(3), R.id.album_cell_4, showControls);
setSlide(requestManager, slides.get(3), R.id.album_cell_4, showControls);
}
if (slides.size() >= 5) {
setSlide(glideRequests, slides.get(4), R.id.album_cell_5, showControls && slides.size() == 5);
setSlide(requestManager, slides.get(4), R.id.album_cell_5, showControls && slides.size() == 5);
}
if (slides.size() > 5) {
@@ -284,7 +285,7 @@ public class AlbumThumbnailView extends FrameLayout {
}
}
private void setSlide(@NonNull GlideRequests glideRequests, @NonNull Slide slide, @IdRes int id, boolean showControls) {
private void setSlide(@NonNull RequestManager requestManager, @NonNull Slide slide, @IdRes int id, boolean showControls) {
ThumbnailView cell = findViewById(id);
cell.showSecondaryText(false);
cell.setThumbnailClickListener(defaultThumbnailClickListener);
@@ -294,7 +295,7 @@ public class AlbumThumbnailView extends FrameLayout {
cell.setPlayVideoClickListener(playVideoClickListener);
}
cell.setOnLongClickListener(defaultLongClickListener);
cell.setImageResource(glideRequests, slide, showControls, false);
cell.setImageResource(requestManager, slide, showControls, false);
}
private int sizeClass(int size) {

View File

@@ -15,6 +15,9 @@ import androidx.annotation.Px;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.fragment.app.FragmentActivity;
import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.RequestManager;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.MultiTransformation;
import com.bumptech.glide.load.Transformation;
@@ -31,15 +34,13 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequest;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
import org.thoughtcrime.securesms.util.AvatarUtil;
@@ -129,10 +130,10 @@ public final class AvatarImageView extends AppCompatImageView {
*/
public void setRecipient(@NonNull Recipient recipient, boolean quickContactEnabled) {
if (recipient.isSelf()) {
setAvatar(GlideApp.with(this), null, quickContactEnabled);
setAvatar(Glide.with(this), null, quickContactEnabled);
AvatarUtil.loadIconIntoImageView(recipient, this);
} else {
setAvatar(GlideApp.with(this), recipient, quickContactEnabled);
setAvatar(Glide.with(this), recipient, quickContactEnabled);
}
}
@@ -144,21 +145,21 @@ public final class AvatarImageView extends AppCompatImageView {
* Shows self as the note to self icon.
*/
public void setAvatar(@Nullable Recipient recipient) {
setAvatar(GlideApp.with(this), recipient, false);
setAvatar(Glide.with(this), recipient, false);
}
/**
* Shows self as the profile avatar.
*/
public void setAvatarUsingProfile(@Nullable Recipient recipient) {
setAvatar(GlideApp.with(this), recipient, false, true);
setAvatar(Glide.with(this), recipient, false, true);
}
public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled) {
public void setAvatar(@NonNull RequestManager requestManager, @Nullable Recipient recipient, boolean quickContactEnabled) {
setAvatar(requestManager, recipient, quickContactEnabled, false);
}
public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled, boolean useSelfProfileAvatar) {
public void setAvatar(@NonNull RequestManager requestManager, @Nullable Recipient recipient, boolean quickContactEnabled, boolean useSelfProfileAvatar) {
setAvatar(requestManager, recipient, new AvatarOptions.Builder(this)
.withUseSelfProfileAvatar(useSelfProfileAvatar)
.withQuickContactEnabled(quickContactEnabled)
@@ -166,10 +167,10 @@ public final class AvatarImageView extends AppCompatImageView {
}
private void setAvatar(@Nullable Recipient recipient, @NonNull AvatarOptions avatarOptions) {
setAvatar(GlideApp.with(this), recipient, avatarOptions);
setAvatar(Glide.with(this), recipient, avatarOptions);
}
private void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, @NonNull AvatarOptions avatarOptions) {
private void setAvatar(@NonNull RequestManager requestManager, @Nullable Recipient recipient, @NonNull AvatarOptions avatarOptions) {
if (recipient != null) {
RecipientContactPhoto photo = (recipient.isSelf() && avatarOptions.useSelfProfileAvatar) ? new RecipientContactPhoto(recipient,
new ProfileContactPhoto(Recipient.self()))
@@ -183,8 +184,18 @@ public final class AvatarImageView extends AppCompatImageView {
this.chatColors = chatColors;
recipientContactPhoto = photo;
Drawable fallbackContactPhotoDrawable = size == SIZE_SMALL ? photo.recipient.getSmallFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider, ViewUtil.getWidth(this))
: photo.recipient.getFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider, ViewUtil.getWidth(this));
Recipient.FallbackPhotoProvider activeFallbackPhotoProvider = this.fallbackPhotoProvider;
if (recipient.isSelf() && avatarOptions.useSelfProfileAvatar) {
activeFallbackPhotoProvider = new Recipient.FallbackPhotoProvider() {
@Override
public @NonNull FallbackContactPhoto getPhotoForLocalNumber() {
return super.getPhotoForRecipientWithName(recipient.getDisplayName(getContext()), ViewUtil.getWidth(AvatarImageView.this));
}
};
}
Drawable fallbackContactPhotoDrawable = size == SIZE_SMALL ? photo.recipient.getSmallFallbackContactPhotoDrawable(getContext(), inverted, activeFallbackPhotoProvider, ViewUtil.getWidth(this))
: photo.recipient.getFallbackContactPhotoDrawable(getContext(), inverted, activeFallbackPhotoProvider, ViewUtil.getWidth(this));
if (fixedSizeTarget != null) {
requestManager.clear(fixedSizeTarget);
@@ -199,7 +210,7 @@ public final class AvatarImageView extends AppCompatImageView {
transforms.add(new CircleCrop());
blurred = shouldBlur;
GlideRequest<Drawable> request = requestManager.load(photo.contactPhoto)
RequestBuilder<Drawable> request = requestManager.load(photo.contactPhoto)
.dontAnimate()
.fallback(fallbackContactPhotoDrawable)
.error(fallbackContactPhotoDrawable)
@@ -244,8 +255,7 @@ public final class AvatarImageView extends AppCompatImageView {
ConversationSettingsActivity.createTransitionBundle(context, this));
} else {
if (context instanceof FragmentActivity) {
RecipientBottomSheetDialogFragment.create(recipient.getId(), null)
.show(((FragmentActivity) context).getSupportFragmentManager(), "BOTTOM");
RecipientBottomSheetDialogFragment.show(((FragmentActivity) context).getSupportFragmentManager(), recipient.getId(), null);
} else {
context.startActivity(ConversationSettingsActivity.forRecipient(context, recipient.getId()),
ConversationSettingsActivity.createTransitionBundle(context, this));
@@ -265,7 +275,7 @@ public final class AvatarImageView extends AppCompatImageView {
.getPhotoForGroup()
.asDrawable(getContext(), color);
GlideApp.with(this)
Glide.with(this)
.load(avatarBytes)
.dontAnimate()
.fallback(fallback)

View File

@@ -9,8 +9,9 @@ import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.RequestManager;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
@@ -52,15 +53,15 @@ public class BorderlessImageView extends FrameLayout {
image.setOnLongClickListener(l);
}
public void setSlide(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
public void setSlide(@NonNull RequestManager requestManager, @NonNull Slide slide) {
boolean showControls = slide.asAttachment().getUri() == null;
if (slide.hasSticker()) {
image.setScaleType(ImageView.ScaleType.FIT_CENTER);
image.setImageResource(glideRequests, slide, showControls, false);
image.setImageResource(requestManager, slide, showControls, false);
} else {
image.setScaleType(ImageView.ScaleType.CENTER_CROP);
image.setImageResource(glideRequests, slide, showControls, false, slide.asAttachment().width, slide.asAttachment().height);
image.setImageResource(requestManager, slide, showControls, false, slide.asAttachment().width, slide.asAttachment().height);
}
missingShade.setVisibility(showControls ? View.VISIBLE : View.GONE);

View File

@@ -114,23 +114,23 @@ public final class ContactFilterView extends FrameLayout {
int defStyle)
{
final TypedArray attributes = context.obtainStyledAttributes(attrs,
R.styleable.ContactFilterToolbar,
R.styleable.ContactFilterView,
defStyle,
0);
int styleResource = attributes.getResourceId(R.styleable.ContactFilterToolbar_searchTextStyle, -1);
int styleResource = attributes.getResourceId(R.styleable.ContactFilterView_searchTextStyle, -1);
if (styleResource != -1) {
TextViewCompat.setTextAppearance(searchText, styleResource);
}
if (!attributes.getBoolean(R.styleable.ContactFilterToolbar_showDialpad, true)) {
if (!attributes.getBoolean(R.styleable.ContactFilterView_showDialpad, true)) {
dialpadToggle.setVisibility(GONE);
}
if (attributes.getBoolean(R.styleable.ContactFilterToolbar_cfv_autoFocus, true)) {
if (attributes.getBoolean(R.styleable.ContactFilterView_cfv_autoFocus, true)) {
searchText.requestFocus();
}
int backgroundRes = attributes.getResourceId(R.styleable.ContactFilterToolbar_cfv_background, -1);
int backgroundRes = attributes.getResourceId(R.styleable.ContactFilterView_cfv_background, -1);
if (backgroundRes != -1) {
findViewById(R.id.background_holder).setBackgroundResource(backgroundRes);
}

View File

@@ -16,10 +16,10 @@ import androidx.annotation.ColorInt
import androidx.annotation.Px
import androidx.annotation.UiThread
import androidx.core.os.bundleOf
import com.bumptech.glide.RequestManager
import org.signal.core.util.dp
import org.signal.core.util.getParcelableCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.mms.SlideClickListener
import org.thoughtcrime.securesms.mms.SlidesClickedListener
@@ -192,7 +192,7 @@ class ConversationItemThumbnail @JvmOverloads constructor(
@UiThread
fun setImageResource(
glideRequests: GlideRequests,
requestManager: RequestManager,
slides: List<Slide>,
showControls: Boolean,
isPreview: Boolean
@@ -223,7 +223,7 @@ class ConversationItemThumbnail @JvmOverloads constructor(
val attachment = slides[0].asAttachment()
thumbnail.get().setImageResource(glideRequests, slides[0], showControls, isPreview, attachment.width, attachment.height)
thumbnail.get().setImageResource(requestManager, slides[0], showControls, isPreview, attachment.width, attachment.height)
touchDelegate = thumbnail.get().touchDelegate
} else {
state = state.copy(
@@ -232,7 +232,7 @@ class ConversationItemThumbnail @JvmOverloads constructor(
)
state.applyState(thumbnail, album)
album.get().setSlides(glideRequests, slides, showControls)
album.get().setSlides(requestManager, slides, showControls)
touchDelegate = album.get().touchDelegate
}
}

View File

@@ -11,9 +11,10 @@ import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.ContextCompat;
import com.bumptech.glide.RequestManager;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.badges.BadgeImageView;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import java.util.List;
@@ -49,7 +50,7 @@ public class ConversationTypingView extends ConstraintLayout {
indicator = findViewById(R.id.typing_indicator);
}
public void setTypists(@NonNull GlideRequests glideRequests, @NonNull List<Recipient> typists, boolean isGroupThread, boolean hasWallpaper) {
public void setTypists(@NonNull RequestManager requestManager, @NonNull List<Recipient> typists, boolean isGroupThread, boolean hasWallpaper) {
if (typists.isEmpty()) {
indicator.stopAnimation();
return;
@@ -64,7 +65,7 @@ public class ConversationTypingView extends ConstraintLayout {
typistCount.setVisibility(GONE);
if (isGroupThread) {
presentGroupThreadAvatars(glideRequests, typists);
presentGroupThreadAvatars(requestManager, typists);
}
if (hasWallpaper) {
@@ -84,23 +85,23 @@ public class ConversationTypingView extends ConstraintLayout {
return indicator.isActive();
}
private void presentGroupThreadAvatars(@NonNull GlideRequests glideRequests, @NonNull List<Recipient> typists) {
avatar1.setAvatar(glideRequests, typists.get(0), typists.size() == 1);
private void presentGroupThreadAvatars(@NonNull RequestManager requestManager, @NonNull List<Recipient> typists) {
avatar1.setAvatar(requestManager, typists.get(0), typists.size() == 1);
avatar1.setVisibility(VISIBLE);
badge1.setBadgeFromRecipient(typists.get(0), glideRequests);
badge1.setBadgeFromRecipient(typists.get(0), requestManager);
badge1.setVisibility(VISIBLE);
if (typists.size() > 1) {
avatar2.setAvatar(glideRequests, typists.get(1), false);
avatar2.setAvatar(requestManager, typists.get(1), false);
avatar2.setVisibility(VISIBLE);
badge2.setBadgeFromRecipient(typists.get(1), glideRequests);
badge2.setBadgeFromRecipient(typists.get(1), requestManager);
badge2.setVisibility(VISIBLE);
}
if (typists.size() == 3) {
avatar3.setAvatar(glideRequests, typists.get(2), false);
avatar3.setAvatar(requestManager, typists.get(2), false);
avatar3.setVisibility(VISIBLE);
badge3.setBadgeFromRecipient(typists.get(2), glideRequests);
badge3.setBadgeFromRecipient(typists.get(2), requestManager);
badge3.setVisibility(VISIBLE);
}

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
@@ -146,7 +147,7 @@ public class DeliveryStatusView extends AppCompatImageView {
setVisibility(View.VISIBLE);
ViewUtil.setPaddingStart(this, 0);
ViewUtil.setPaddingEnd(this, horizontalPadding);
setImageResource(R.drawable.ic_delivery_status_sending);
setImageResource(R.drawable.symbol_messagestatus_sending_24);
updateContentDescription();
}
@@ -156,7 +157,7 @@ public class DeliveryStatusView extends AppCompatImageView {
ViewUtil.setPaddingStart(this, horizontalPadding);
ViewUtil.setPaddingEnd(this, 0);
clearAnimation();
setImageResource(R.drawable.ic_delivery_status_sent);
setImageResource(R.drawable.symbol_messagestatus_sent_24);
updateContentDescription();
}
@@ -166,7 +167,7 @@ public class DeliveryStatusView extends AppCompatImageView {
ViewUtil.setPaddingStart(this, horizontalPadding);
ViewUtil.setPaddingEnd(this, 0);
clearAnimation();
setImageResource(R.drawable.ic_delivery_status_delivered);
setImageResource(R.drawable.symbol_messagestatus_delivered_24);
updateContentDescription();
}
@@ -176,12 +177,12 @@ public class DeliveryStatusView extends AppCompatImageView {
ViewUtil.setPaddingStart(this, horizontalPadding);
ViewUtil.setPaddingEnd(this, 0);
clearAnimation();
setImageResource(R.drawable.ic_delivery_status_read);
setImageResource(R.drawable.symbol_messagestatus_read_24);
updateContentDescription();
}
public void setTint(int color) {
setColorFilter(color);
setColorFilter(color, PorterDuff.Mode.SRC_IN);
}
private void updateContentDescription() {

View File

@@ -1,12 +1,11 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
import android.text.SpannableStringBuilder;
import android.util.AttributeSet;
import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
@@ -15,11 +14,10 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.SimpleEmojiTextView;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.ContextUtil;
import org.thoughtcrime.securesms.util.DrawableUtil;
import org.thoughtcrime.securesms.util.SpanUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Objects;
public class FromTextView extends SimpleEmojiTextView {
private static final String TAG = Log.tag(FromTextView.class);
@@ -71,17 +69,23 @@ public class FromTextView extends SimpleEmojiTextView {
setText(builder);
if (recipient.isBlocked()) setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_block_grey600_18dp, 0, 0, 0);
if (recipient.isBlocked()) setCompoundDrawablesRelativeWithIntrinsicBounds(getBlocked(), null, null, null);
else if (recipient.isMuted()) setCompoundDrawablesRelativeWithIntrinsicBounds(getMuted(), null, null, null);
else setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0);
}
private Drawable getBlocked() {
return getDrawable(R.drawable.symbol_block_16);
}
private Drawable getMuted() {
Drawable mutedDrawable = Objects.requireNonNull(ContextCompat.getDrawable(getContext(), R.drawable.ic_bell_disabled_16));
return getDrawable(R.drawable.ic_bell_disabled_16);
}
private Drawable getDrawable(@DrawableRes int drawable) {
Drawable mutedDrawable = ContextUtil.requireDrawable(getContext(), drawable);
mutedDrawable.setBounds(0, 0, ViewUtil.dpToPx(18), ViewUtil.dpToPx(18));
mutedDrawable.setColorFilter(new PorterDuffColorFilter(ContextCompat.getColor(getContext(), R.color.signal_icon_tint_secondary), PorterDuff.Mode.SRC_IN));
DrawableUtil.tint(mutedDrawable, ContextCompat.getColor(getContext(), R.color.signal_icon_tint_secondary));
return mutedDrawable;
}
}

View File

@@ -9,7 +9,7 @@ import androidx.annotation.Nullable;
import com.bumptech.glide.request.target.BitmapImageViewTarget;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.signal.core.util.concurrent.SettableFuture;
public class GlideBitmapListeningTarget extends BitmapImageViewTarget {

View File

@@ -8,8 +8,8 @@ import androidx.annotation.Nullable;
import com.bumptech.glide.request.target.DrawableImageViewTarget;
import org.signal.core.util.concurrent.SettableFuture;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
public class GlideDrawableListeningTarget extends DrawableImageViewTarget {

View File

@@ -32,9 +32,13 @@ import androidx.lifecycle.Observer;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestManager;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.concurrent.ListenableFuture;
import org.signal.core.util.concurrent.SettableFuture;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
@@ -49,9 +53,9 @@ import org.thoughtcrime.securesms.conversation.ConversationStickerSuggestionAdap
import org.thoughtcrime.securesms.conversation.MessageStyler;
import org.thoughtcrime.securesms.conversation.VoiceNoteDraftView;
import org.thoughtcrime.securesms.database.DraftTable;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.database.model.Quote;
import org.thoughtcrime.securesms.database.model.StickerRecord;
import org.thoughtcrime.securesms.keyboard.KeyboardPage;
@@ -59,8 +63,6 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.QuoteModel;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
@@ -69,8 +71,6 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.MessageRecordUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import java.util.Arrays;
import java.util.List;
@@ -184,7 +184,7 @@ public class InputPanel extends ConstraintLayout
}
});
stickerSuggestionAdapter = new ConversationStickerSuggestionAdapter(GlideApp.with(this), this);
stickerSuggestionAdapter = new ConversationStickerSuggestionAdapter(Glide.with(this), this);
stickerSuggestion.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false));
stickerSuggestion.setAdapter(stickerSuggestionAdapter);
@@ -212,14 +212,14 @@ public class InputPanel extends ConstraintLayout
composeText.setMediaListener(listener);
}
public void setQuote(@NonNull GlideRequests glideRequests,
public void setQuote(@NonNull RequestManager requestManager,
long id,
@NonNull Recipient author,
@Nullable CharSequence body,
@NonNull SlideDeck attachments,
@NonNull QuoteModel.Type quoteType)
{
this.quoteView.setQuote(glideRequests, id, author, body, false, attachments, null, quoteType);
this.quoteView.setQuote(requestManager, id, author, body, false, attachments, null, quoteType);
int originalHeight = this.quoteView.getVisibility() == VISIBLE ? this.quoteView.getMeasuredHeight()
: 0;
@@ -325,10 +325,10 @@ public class InputPanel extends ConstraintLayout
this.linkPreview.setNoPreview(customError);
}
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull Optional<LinkPreview> preview) {
public void setLinkPreview(@NonNull RequestManager requestManager, @NonNull Optional<LinkPreview> preview) {
if (preview.isPresent()) {
this.linkPreview.setVisibility(View.VISIBLE);
this.linkPreview.setLinkPreview(glideRequests, preview.get(), true);
this.linkPreview.setLinkPreview(requestManager, preview.get(), true);
} else {
this.linkPreview.setVisibility(View.GONE);
}
@@ -404,7 +404,7 @@ public class InputPanel extends ConstraintLayout
quoteView.setWallpaperEnabled(enabled);
}
public void enterEditMessageMode(@NonNull GlideRequests glideRequests, @NonNull ConversationMessage conversationMessageToEdit, boolean fromDraft) {
public void enterEditMessageMode(@NonNull RequestManager requestManager, @NonNull ConversationMessage conversationMessageToEdit, boolean fromDraft) {
SpannableString textToEdit = conversationMessageToEdit.getDisplayBody(getContext());
if (!fromDraft) {
MessageStyler.convertSpoilersToComposeMode(textToEdit);
@@ -415,14 +415,14 @@ public class InputPanel extends ConstraintLayout
if (quote == null) {
clearQuote();
} else {
setQuote(glideRequests, quote.getId(), Recipient.resolved(quote.getAuthor()), quote.getDisplayText(), quote.getAttachment(), quote.getQuoteType());
setQuote(requestManager, quote.getId(), Recipient.resolved(quote.getAuthor()), quote.getDisplayText(), quote.getAttachment(), quote.getQuoteType());
}
this.messageToEdit = conversationMessageToEdit.getMessageRecord();
updateEditModeThumbnail(glideRequests);
updateEditModeThumbnail(requestManager);
updateEditModeUi();
}
private void updateEditModeThumbnail(@NonNull GlideRequests glideRequests) {
private void updateEditModeThumbnail(@NonNull RequestManager requestManager) {
if (messageToEdit instanceof MmsMessageRecord) {
MmsMessageRecord mediaEditMessage = (MmsMessageRecord) messageToEdit;
SlideDeck slideDeck = mediaEditMessage.getSlideDeck();
@@ -430,7 +430,7 @@ public class InputPanel extends ConstraintLayout
if (imageVideoSlide != null && imageVideoSlide.getUri() != null) {
editMessageThumbnail.setVisibility(VISIBLE);
glideRequests.load(new DecryptableStreamUriLoader.DecryptableUri(imageVideoSlide.getUri()))
requestManager.load(new DecryptableStreamUriLoader.DecryptableUri(imageVideoSlide.getUri()))
.centerCrop()
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.into(editMessageThumbnail);

View File

@@ -16,13 +16,14 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import com.bumptech.glide.RequestManager;
import org.signal.ringrtc.CallLinkRootKey;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.calls.links.CallLinks;
import org.thoughtcrime.securesms.conversation.colors.AvatarColorHash;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -162,11 +163,11 @@ public class LinkPreviewView extends FrameLayout {
noPreview.setText(getLinkPreviewErrorString(customError));
}
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail) {
setLinkPreview(glideRequests, linkPreview, showThumbnail, true, false);
public void setLinkPreview(@NonNull RequestManager requestManager, @NonNull LinkPreview linkPreview, boolean showThumbnail) {
setLinkPreview(requestManager, linkPreview, showThumbnail, true, false);
}
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail, boolean showDescription, boolean scheduleMessageMode) {
public void setLinkPreview(@NonNull RequestManager requestManager, @NonNull LinkPreview linkPreview, boolean showThumbnail, boolean showDescription, boolean scheduleMessageMode) {
spinner.setVisibility(GONE);
noPreview.setVisibility(GONE);
@@ -216,13 +217,13 @@ public class LinkPreviewView extends FrameLayout {
if (showThumbnail && linkPreview.getThumbnail().isPresent()) {
thumbnail.setVisibility(VISIBLE);
thumbnailState.applyState(thumbnail);
thumbnail.get().setImageResource(glideRequests, new ImageSlide(linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION && !scheduleMessageMode, false);
thumbnail.get().setImageResource(requestManager, new ImageSlide(linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION && !scheduleMessageMode, false);
thumbnail.get().showSecondaryText(false);
} else if (callLinkRootKey != null) {
thumbnail.setVisibility(VISIBLE);
thumbnailState.applyState(thumbnail);
thumbnail.get().setImageDrawable(
glideRequests,
requestManager,
Recipient.DEFAULT_FALLBACK_PHOTO_PROVIDER
.getPhotoForCallLink()
.asDrawable(getContext(),

View File

@@ -13,16 +13,19 @@ import androidx.annotation.RequiresApi
import androidx.core.os.bundleOf
import androidx.fragment.app.FragmentManager
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.databinding.PromptBatterySaverBottomSheetBinding
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.LocalMetrics
import org.thoughtcrime.securesms.util.PowerManagerCompat
@RequiresApi(23)
class PromptBatterySaverDialogFragment : FixedRoundedCornerBottomSheetDialogFragment() {
companion object {
private val TAG = Log.tag(PromptBatterySaverDialogFragment::class.java)
@JvmStatic
fun show(fragmentManager: FragmentManager) {
@@ -51,8 +54,11 @@ class PromptBatterySaverDialogFragment : FixedRoundedCornerBottomSheetDialogFrag
binding.continueButton.setOnClickListener {
PowerManagerCompat.requestIgnoreBatteryOptimizations(requireContext())
Log.i(TAG, "Requested to ignore battery optimizations, clearing local metrics.")
LocalMetrics.clear()
}
binding.dismissButton.setOnClickListener {
Log.i(TAG, "User denied request to ignore battery optimizations.")
SignalStore.uiHints().markDismissedBatterySaverPrompt()
dismiss()
}

View File

@@ -15,6 +15,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.bumptech.glide.RequestManager;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.google.android.material.imageview.ShapeableImageView;
import com.google.android.material.shape.CornerFamily;
@@ -32,7 +33,6 @@ import org.thoughtcrime.securesms.conversation.MessageStyler;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.QuoteModel;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
@@ -193,7 +193,7 @@ public class QuoteView extends ConstraintLayout implements RecipientForeverObser
}
}
public void setQuote(GlideRequests glideRequests,
public void setQuote(RequestManager requestManager,
long id,
@NonNull Recipient author,
@Nullable CharSequence body,
@@ -213,7 +213,7 @@ public class QuoteView extends ConstraintLayout implements RecipientForeverObser
this.author.observeForever(this);
setQuoteAuthor(author);
setQuoteText(resolveBody(body, quoteType), attachments, originalMissing, storyReaction);
setQuoteAttachment(glideRequests, body, attachments, originalMissing);
setQuoteAttachment(requestManager, body, attachments, originalMissing);
setQuoteMissingFooter(originalMissing);
applyColorTheme();
}
@@ -347,7 +347,7 @@ public class QuoteView extends ConstraintLayout implements RecipientForeverObser
}
}
private void setQuoteAttachment(@NonNull GlideRequests glideRequests, @NonNull CharSequence body, @NonNull SlideDeck slideDeck, boolean originalMissing) {
private void setQuoteAttachment(@NonNull RequestManager requestManager, @NonNull CharSequence body, @NonNull SlideDeck slideDeck, boolean originalMissing) {
boolean outgoing = messageType != MessageType.INCOMING && messageType != MessageType.STORY_REPLY_INCOMING;
boolean preview = messageType == MessageType.PREVIEW || messageType == MessageType.STORY_REPLY_PREVIEW;
@@ -359,7 +359,7 @@ public class QuoteView extends ConstraintLayout implements RecipientForeverObser
attachmentVideoOVerlayStub.setVisibility(GONE);
attachmentNameViewStub.setVisibility(GONE);
thumbnailView.setVisibility(VISIBLE);
glideRequests.load(model)
requestManager.load(model)
.centerCrop()
.override(thumbWidth, thumbHeight)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
@@ -377,7 +377,7 @@ public class QuoteView extends ConstraintLayout implements RecipientForeverObser
attachmentVideoOVerlayStub.setVisibility(GONE);
attachmentNameViewStub.setVisibility(GONE);
thumbnailView.setVisibility(VISIBLE);
glideRequests.load(R.drawable.ic_gift_thumbnail)
requestManager.load(R.drawable.ic_gift_thumbnail)
.centerCrop()
.override(thumbWidth, thumbHeight)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
@@ -404,7 +404,7 @@ public class QuoteView extends ConstraintLayout implements RecipientForeverObser
if (imageVideoSlide.hasVideo() && !imageVideoSlide.isVideoGif()) {
attachmentVideoOVerlayStub.setVisibility(VISIBLE);
}
glideRequests.load(new DecryptableUri(imageVideoSlide.getUri()))
requestManager.load(new DecryptableUri(imageVideoSlide.getUri()))
.centerCrop()
.override(thumbWidth, thumbHeight)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)

View File

@@ -22,6 +22,7 @@ import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.Key;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
@@ -31,7 +32,6 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
import org.thoughtcrime.securesms.database.loaders.RecentPhotosLoader;
import org.thoughtcrime.securesms.mms.GlideApp;
public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.LoaderCallbacks<Cursor> {
@@ -119,7 +119,7 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
Key signature = new MediaStoreSignature(mimeType, dateModified, orientation);
GlideApp.with(getContext().getApplicationContext())
Glide.with(getContext().getApplicationContext())
.load(uri)
.signature(signature)
.diskCacheStrategy(DiskCacheStrategy.NONE)

View File

@@ -17,6 +17,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.annimon.stream.Stream;
import com.bumptech.glide.RequestManager;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.thoughtcrime.securesms.R;
@@ -24,7 +25,6 @@ import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.contactshare.ContactUtil;
import org.thoughtcrime.securesms.database.RecipientTable;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
@@ -45,13 +45,13 @@ public class SharedContactView extends LinearLayout implements RecipientForeverO
private TextView actionButtonView;
private ConversationItemFooter footer;
private Contact contact;
private Locale locale;
private GlideRequests glideRequests;
private EventListener eventListener;
private CornerMask cornerMask;
private int bigCornerRadius;
private int smallCornerRadius;
private Contact contact;
private Locale locale;
private RequestManager requestManager;
private EventListener eventListener;
private CornerMask cornerMask;
private int bigCornerRadius;
private int smallCornerRadius;
private final Map<RecipientId, LiveRecipient> activeRecipients = new HashMap<>();
@@ -111,10 +111,10 @@ public class SharedContactView extends LinearLayout implements RecipientForeverO
cornerMask.mask(canvas);
}
public void setContact(@NonNull Contact contact, @NonNull GlideRequests glideRequests, @NonNull Locale locale) {
this.glideRequests = glideRequests;
this.locale = locale;
this.contact = contact;
public void setContact(@NonNull Contact contact, @NonNull RequestManager requestManager, @NonNull Locale locale) {
this.requestManager = requestManager;
this.locale = locale;
this.contact = contact;
Stream.of(activeRecipients.values()).forEach(recipient -> recipient.removeForeverObserver(this));
this.activeRecipients.clear();
@@ -172,17 +172,17 @@ public class SharedContactView extends LinearLayout implements RecipientForeverO
private void presentAvatar(@Nullable Uri uri) {
if (uri != null) {
glideRequests.load(new DecryptableUri(uri))
.fallback(R.drawable.ic_contact_picture)
.circleCrop()
.diskCacheStrategy(DiskCacheStrategy.ALL)
.dontAnimate()
.into(avatarView);
requestManager.load(new DecryptableUri(uri))
.fallback(R.drawable.symbol_person_display_40)
.circleCrop()
.diskCacheStrategy(DiskCacheStrategy.ALL)
.dontAnimate()
.into(avatarView);
} else {
glideRequests.load(R.drawable.ic_contact_picture)
.circleCrop()
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(avatarView);
requestManager.load(R.drawable.symbol_person_display_40)
.circleCrop()
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(avatarView);
}
}

View File

@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.database.Cursor;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
@@ -15,11 +14,12 @@ import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.RequestManager;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.MediaTable;
import org.thoughtcrime.securesms.mediapreview.MediaPreviewCache;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.util.MediaUtil;
@@ -58,8 +58,8 @@ public class ThreadPhotoRailView extends FrameLayout {
}
}
public void setMediaRecords(@NonNull GlideRequests glideRequests, @NonNull List<MediaTable.MediaRecord> mediaRecords) {
this.recyclerView.setAdapter(new ThreadPhotoRailAdapter(getContext(), glideRequests, mediaRecords, this.listener));
public void setMediaRecords(@NonNull RequestManager requestManager, @NonNull List<MediaTable.MediaRecord> mediaRecords) {
this.recyclerView.setAdapter(new ThreadPhotoRailAdapter(getContext(), requestManager, mediaRecords, this.listener));
}
private static class ThreadPhotoRailAdapter extends RecyclerView.Adapter<ThreadPhotoRailAdapter.ThreadPhotoViewHolder> {
@@ -67,18 +67,18 @@ public class ThreadPhotoRailView extends FrameLayout {
@SuppressWarnings("unused")
private static final String TAG = Log.tag(ThreadPhotoRailAdapter.class);
@NonNull private final GlideRequests glideRequests;
@NonNull private final RequestManager requestManager;
@Nullable private OnItemClickedListener clickedListener;
private final List<MediaTable.MediaRecord> mediaRecords = new ArrayList<>();
private ThreadPhotoRailAdapter(@NonNull Context context,
@NonNull GlideRequests glideRequests,
@NonNull RequestManager requestManager,
@NonNull List<MediaTable.MediaRecord> mediaRecords,
@Nullable OnItemClickedListener listener)
{
this.glideRequests = glideRequests;
this.requestManager = requestManager;
this.clickedListener = listener;
this.mediaRecords.clear();
@@ -103,7 +103,7 @@ public class ThreadPhotoRailView extends FrameLayout {
MediaTable.MediaRecord mediaRecord = mediaRecords.get(position);
Slide slide = MediaUtil.getSlideForAttachment(mediaRecord.getAttachment());
viewHolder.imageView.setImageResource(glideRequests, slide, false, false);
viewHolder.imageView.setImageResource(requestManager, slide, false, false);
viewHolder.imageView.setOnClickListener(v -> {
MediaPreviewCache.INSTANCE.setDrawable(viewHolder.imageView.getImageDrawable());
if (clickedListener != null) clickedListener.onItemClicked(viewHolder.imageView, mediaRecord);

View File

@@ -27,11 +27,14 @@ import androidx.annotation.UiThread;
import androidx.appcompat.widget.AppCompatImageView;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.RequestManager;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.Request;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.RequestOptions;
import org.signal.core.util.concurrent.ListenableFuture;
import org.signal.core.util.concurrent.SettableFuture;
import org.signal.core.util.logging.Log;
import org.signal.glide.transforms.SignalDownsampleStrategy;
import org.thoughtcrime.securesms.R;
@@ -39,8 +42,6 @@ import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.components.transfercontrols.TransferControlView;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideRequest;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener;
@@ -49,8 +50,6 @@ import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.stories.StoryTextPostModel;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.thoughtcrime.securesms.util.views.Stub;
import java.util.Arrays;
@@ -314,23 +313,23 @@ public class ThumbnailView extends FrameLayout {
}
}
public void setImageDrawable(@NonNull GlideRequests glideRequests, @Nullable Drawable drawable) {
glideRequests.clear(image);
glideRequests.clear(blurHash);
public void setImageDrawable(@NonNull RequestManager requestManager, @Nullable Drawable drawable) {
requestManager.clear(image);
requestManager.clear(blurHash);
image.setImageDrawable(drawable);
blurHash.setImageDrawable(null);
}
@UiThread
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide,
public ListenableFuture<Boolean> setImageResource(@NonNull RequestManager requestManager, @NonNull Slide slide,
boolean showControls, boolean isPreview)
{
return setImageResource(glideRequests, slide, showControls, isPreview, 0, 0);
return setImageResource(requestManager, slide, showControls, isPreview, 0, 0);
}
@UiThread
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide,
public ListenableFuture<Boolean> setImageResource(@NonNull RequestManager requestManager, @NonNull Slide slide,
boolean showControls, boolean isPreview,
int naturalWidth, int naturalHeight)
{
@@ -340,10 +339,10 @@ public class ThumbnailView extends FrameLayout {
transferControlViewStub.setVisibility(View.GONE);
playOverlay.setVisibility(View.GONE);
glideRequests.clear(blurHash);
requestManager.clear(blurHash);
blurHash.setImageDrawable(null);
glideRequests.clear(image);
requestManager.clear(image);
image.setImageDrawable(null);
int errorImageResource;
@@ -414,10 +413,10 @@ public class ThumbnailView extends FrameLayout {
boolean resultHandled = false;
if (slide.hasPlaceholder() && (previousBlurHash == null || !Objects.equals(slide.getPlaceholderBlur(), previousBlurHash))) {
buildPlaceholderGlideRequest(glideRequests, slide).into(new GlideBitmapListeningTarget(blurHash, result));
buildPlaceholderRequestBuilder(requestManager, slide).into(new GlideBitmapListeningTarget(blurHash, result));
resultHandled = true;
} else if (!slide.hasPlaceholder()) {
glideRequests.clear(blurHash);
requestManager.clear(blurHash);
blurHash.setImageDrawable(null);
}
@@ -425,14 +424,14 @@ public class ThumbnailView extends FrameLayout {
if (!MediaUtil.isJpegType(slide.getContentType()) && !MediaUtil.isVideoType(slide.getContentType())) {
SettableFuture<Boolean> thumbnailFuture = new SettableFuture<>();
thumbnailFuture.deferTo(result);
thumbnailFuture.addListener(new BlurHashClearListener(glideRequests, blurHash));
thumbnailFuture.addListener(new BlurHashClearListener(requestManager, blurHash));
}
buildThumbnailGlideRequest(glideRequests, slide).into(new GlideDrawableListeningTarget(image, result));
buildThumbnailRequestBuilder(requestManager, slide).into(new GlideDrawableListeningTarget(image, result));
resultHandled = true;
} else {
glideRequests.clear(image);
requestManager.clear(image);
image.setImageDrawable(null);
}
@@ -443,20 +442,20 @@ public class ThumbnailView extends FrameLayout {
return result;
}
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Uri uri) {
return setImageResource(glideRequests, uri, 0, 0);
public ListenableFuture<Boolean> setImageResource(@NonNull RequestManager requestManager, @NonNull Uri uri) {
return setImageResource(requestManager, uri, 0, 0);
}
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Uri uri, int width, int height) {
return setImageResource(glideRequests, uri, width, height, true, null);
public ListenableFuture<Boolean> setImageResource(@NonNull RequestManager requestManager, @NonNull Uri uri, int width, int height) {
return setImageResource(requestManager, uri, width, height, true, null);
}
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Uri uri, int width, int height, boolean animate, @Nullable ThumbnailRequestListener listener) {
public ListenableFuture<Boolean> setImageResource(@NonNull RequestManager requestManager, @NonNull Uri uri, int width, int height, boolean animate, @Nullable ThumbnailRequestListener listener) {
SettableFuture<Boolean> future = new SettableFuture<>();
transferControlViewStub.setVisibility(View.GONE);
GlideRequest<Drawable> request = glideRequests.load(new DecryptableUri(uri))
RequestBuilder<Drawable> request = requestManager.load(new DecryptableUri(uri))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE)
.listener(listener);
@@ -483,12 +482,12 @@ public class ThumbnailView extends FrameLayout {
return future;
}
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull StoryTextPostModel model, int width, int height) {
public ListenableFuture<Boolean> setImageResource(@NonNull RequestManager requestManager, @NonNull StoryTextPostModel model, int width, int height) {
SettableFuture<Boolean> future = new SettableFuture<>();
transferControlViewStub.setVisibility(View.GONE);
GlideRequest<Drawable> request = glideRequests.load(model)
RequestBuilder<Drawable> request = requestManager.load(model)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.placeholder(model.getPlaceholder())
.downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE)
@@ -502,7 +501,7 @@ public class ThumbnailView extends FrameLayout {
return future;
}
private <T> GlideRequest<T> override(@NonNull GlideRequest<T> request, int width, int height) {
private <T> RequestBuilder<T> override(@NonNull RequestBuilder<T> request, int width, int height) {
if (width > 0 && height > 0) {
Log.d(TAG, "override: apply w" + width + "xh" + height);
return request.override(width, height);
@@ -542,8 +541,8 @@ public class ThumbnailView extends FrameLayout {
return false;
}
private GlideRequest<Drawable> buildThumbnailGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
GlideRequest<Drawable> request = applySizing(glideRequests.load(new DecryptableUri(Objects.requireNonNull(slide.getUri())))
private RequestBuilder<Drawable> buildThumbnailRequestBuilder(@NonNull RequestManager requestManager, @NonNull Slide slide) {
RequestBuilder<Drawable> requestBuilder = applySizing(requestManager.load(new DecryptableUri(Objects.requireNonNull(slide.getUri())))
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE)
.transition(withCrossFade()));
@@ -551,21 +550,21 @@ public class ThumbnailView extends FrameLayout {
boolean doNotShowMissingThumbnailImage = Build.VERSION.SDK_INT < 23;
if (slide.isInProgress() || doNotShowMissingThumbnailImage) {
return request;
return requestBuilder;
} else {
return request.apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture));
return requestBuilder.apply(RequestOptions.errorOf(R.drawable.missing_thumbnail));
}
}
public void clear(GlideRequests glideRequests) {
glideRequests.clear(image);
public void clear(RequestManager requestManager) {
requestManager.clear(image);
image.setImageDrawable(null);
if (transferControlViewStub.resolved()) {
transferControlViewStub.get().clear();
}
glideRequests.clear(blurHash);
requestManager.clear(blurHash);
blurHash.setImageDrawable(null);
slide = null;
@@ -594,9 +593,9 @@ public class ThumbnailView extends FrameLayout {
}
private RequestBuilder<Bitmap> buildPlaceholderGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
GlideRequest<Bitmap> bitmap = glideRequests.asBitmap();
BlurHash placeholderBlur = slide.getPlaceholderBlur();
private RequestBuilder<Bitmap> buildPlaceholderRequestBuilder(@NonNull RequestManager requestManager, @NonNull Slide slide) {
RequestBuilder<Bitmap> bitmap = requestManager.asBitmap();
BlurHash placeholderBlur = slide.getPlaceholderBlur();
if (placeholderBlur != null) {
bitmap = bitmap.load(placeholderBlur);
@@ -604,7 +603,7 @@ public class ThumbnailView extends FrameLayout {
bitmap = bitmap.load(slide.getPlaceholderRes(getContext().getTheme()));
}
final GlideRequest<Bitmap> resizedRequest = applySizing(bitmap.diskCacheStrategy(DiskCacheStrategy.NONE));
final RequestBuilder<Bitmap> resizedRequest = applySizing(bitmap.diskCacheStrategy(DiskCacheStrategy.NONE));
if (placeholderBlur != null) {
return resizedRequest.centerCrop();
} else {
@@ -612,7 +611,7 @@ public class ThumbnailView extends FrameLayout {
}
}
private <TranscodeType> GlideRequest<TranscodeType> applySizing(@NonNull GlideRequest<TranscodeType> request) {
private <TranscodeType> RequestBuilder<TranscodeType> applySizing(@NonNull RequestBuilder<TranscodeType> request) {
int[] size = new int[2];
fillTargetDimensions(size, dimens, bounds);
if (size[WIDTH] == 0 && size[HEIGHT] == 0) {
@@ -701,23 +700,23 @@ public class ThumbnailView extends FrameLayout {
private static class BlurHashClearListener implements ListenableFuture.Listener<Boolean> {
private final GlideRequests glideRequests;
private final ImageView blurHash;
private final RequestManager requestManager;
private final ImageView blurHash;
private BlurHashClearListener(@NonNull GlideRequests glideRequests, @NonNull ImageView blurHash) {
this.glideRequests = glideRequests;
this.blurHash = blurHash;
private BlurHashClearListener(@NonNull RequestManager requestManager, @NonNull ImageView blurHash) {
this.requestManager = requestManager;
this.blurHash = blurHash;
}
@Override
public void onSuccess(Boolean result) {
glideRequests.clear(blurHash);
requestManager.clear(blurHash);
blurHash.setImageDrawable(null);
}
@Override
public void onFailure(ExecutionException e) {
glideRequests.clear(blurHash);
requestManager.clear(blurHash);
blurHash.setImageDrawable(null);
}
}

View File

@@ -18,13 +18,13 @@ import androidx.annotation.Px;
import androidx.annotation.StringRes;
import androidx.core.content.ContextCompat;
import com.bumptech.glide.Glide;
import com.google.android.material.shape.MaterialShapeDrawable;
import com.google.android.material.shape.ShapeAppearanceModel;
import org.signal.core.util.DimensionUnit;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.GlideApp;
/**
* Class for creating simple tooltips to show throughout the app. Utilizes a popup window so you
@@ -101,7 +101,7 @@ public class TooltipPopup extends PopupWindow {
if (iconGlideModel != null) {
ImageView iconView = getContentView().findViewById(R.id.tooltip_icon);
iconView.setVisibility(View.VISIBLE);
GlideApp.with(anchor.getContext()).load(iconGlideModel).into(iconView);
Glide.with(anchor.getContext()).load(iconGlideModel).into(iconView);
}
setElevation(10);

View File

@@ -11,6 +11,7 @@ import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.exifinterface.media.ExifInterface;
import com.bumptech.glide.RequestManager;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.Target;
import com.davemorrissey.labs.subscaleview.ImageSource;
@@ -23,7 +24,6 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.subsampling.AttachmentBitmapDecoder;
import org.thoughtcrime.securesms.components.subsampling.AttachmentRegionDecoder;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.util.ActionRequestListener;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
@@ -81,7 +81,7 @@ public class ZoomingImageView extends FrameLayout {
}
@SuppressLint("StaticFieldLeak")
public void setImageUri(@NonNull GlideRequests glideRequests, @NonNull Uri uri, @NonNull String contentType, @NonNull Runnable onMediaReady)
public void setImageUri(@NonNull RequestManager requestManager, @NonNull Uri uri, @NonNull String contentType, @NonNull Runnable onMediaReady)
{
final Context context = getContext();
final int maxTextureSize = BitmapUtil.getMaxTextureSize();
@@ -103,7 +103,7 @@ public class ZoomingImageView extends FrameLayout {
if (dimensions == null || (dimensions.first <= maxTextureSize && dimensions.second <= maxTextureSize)) {
Log.i(TAG, "Loading in standard image view...");
setImageViewUri(glideRequests, uri, onMediaReady);
setImageViewUri(requestManager, uri, onMediaReady);
} else {
Log.i(TAG, "Loading in subsampling image view...");
setSubsamplingImageViewUri(uri);
@@ -112,11 +112,11 @@ public class ZoomingImageView extends FrameLayout {
});
}
private void setImageViewUri(@NonNull GlideRequests glideRequests, @NonNull Uri uri, @NonNull Runnable onMediaReady) {
private void setImageViewUri(@NonNull RequestManager requestManager, @NonNull Uri uri, @NonNull Runnable onMediaReady) {
photoView.setVisibility(View.VISIBLE);
subsamplingImageView.setVisibility(View.GONE);
glideRequests.load(new DecryptableUri(uri))
requestManager.load(new DecryptableUri(uri))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.dontTransform()
.override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)

View File

@@ -32,6 +32,7 @@ import androidx.core.view.GestureDetectorCompat;
import androidx.core.view.ViewKt;
import androidx.core.widget.TextViewCompat;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
@@ -73,6 +74,7 @@ public class EmojiTextView extends AppCompatTextView {
private boolean isJumbomoji;
private boolean forceJumboEmoji;
private boolean renderSpoilers;
private boolean shrinkWrap;
private MentionRendererDelegate mentionRendererDelegate;
private SpoilerRendererDelegate spoilerRendererDelegate;
@@ -96,6 +98,7 @@ public class EmojiTextView extends AppCompatTextView {
measureLastLine = a.getBoolean(R.styleable.EmojiTextView_measureLastLine, false);
forceJumboEmoji = a.getBoolean(R.styleable.EmojiTextView_emoji_forceJumbo, false);
renderSpoilers = a.getBoolean(R.styleable.EmojiTextView_emoji_renderSpoilers, false);
shrinkWrap = a.getBoolean(R.styleable.EmojiTextView_emoji_shrinkWrap, false);
a.recycle();
a = context.obtainStyledAttributes(attrs, new int[] { android.R.attr.textSize });
@@ -224,6 +227,25 @@ public class EmojiTextView extends AppCompatTextView {
widthMeasureSpec = applyWidthMeasureRoundingFix(widthMeasureSpec);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int mode = MeasureSpec.getMode(widthMeasureSpec);
if (shrinkWrap && getLayout() != null && mode == MeasureSpec.AT_MOST) {
Layout layout = getLayout();
float maxLineWidth = 0f;
for (int i = 0; i < layout.getLineCount(); i++) {
if (layout.getLineWidth(i) > maxLineWidth) {
maxLineWidth = layout.getLineWidth(i);
}
}
int desiredWidth = (int) maxLineWidth + getPaddingLeft() + getPaddingRight();
if (getMeasuredWidth() > desiredWidth) {
widthMeasureSpec = MeasureSpec.makeMeasureSpec(desiredWidth, mode);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
CharSequence text = getText();
if (getLayout() == null || !measureLastLine || text == null || text.length() == 0) {
lastLineWidth = -1;

View File

@@ -32,7 +32,7 @@ public class UntrustedSendDialog extends AlertDialog.Builder implements DialogIn
this.resendListener = resendListener;
setTitle(R.string.UntrustedSendDialog_send_message);
setIcon(R.drawable.ic_warning);
setIcon(R.drawable.symbol_error_triangle_fill_24);
setMessage(message);
setPositiveButton(R.string.UntrustedSendDialog_send, this);
setNegativeButton(android.R.string.cancel, null);

View File

@@ -31,7 +31,7 @@ public class UnverifiedSendDialog extends AlertDialog.Builder implements DialogI
this.resendListener = resendListener;
setTitle(R.string.UnverifiedSendDialog_send_message);
setIcon(R.drawable.ic_warning);
setIcon(R.drawable.symbol_error_triangle_fill_24);
setMessage(message);
setPositiveButton(R.string.UnverifiedSendDialog_send, this);
setNegativeButton(android.R.string.cancel, null);

View File

@@ -17,9 +17,9 @@ import com.google.android.gms.maps.MapView;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.MarkerOptions;
import org.signal.core.util.concurrent.ListenableFuture;
import org.signal.core.util.concurrent.SettableFuture;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import java.util.concurrent.ExecutionException;

View File

@@ -18,11 +18,11 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import org.signal.core.util.concurrent.ListenableFuture;
import org.signal.core.util.concurrent.SettableFuture;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.NumericKeyboardView;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
public class VerificationPinKeyboard extends FrameLayout {

View File

@@ -4,7 +4,6 @@ import android.content.Context
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.keyvalue.AccountValues.UsernameSyncState
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.FeatureFlags
/**
* Displays a reminder message when the local username gets out of sync with
@@ -42,14 +41,10 @@ class UsernameOutOfSyncReminder : Reminder(NO_RESOURCE) {
companion object {
@JvmStatic
fun isEligible(): Boolean {
return if (FeatureFlags.usernames()) {
when (SignalStore.account().usernameSyncState) {
UsernameSyncState.USERNAME_AND_LINK_CORRUPTED -> true
UsernameSyncState.LINK_CORRUPTED -> true
UsernameSyncState.IN_SYNC -> false
}
} else {
false
return when (SignalStore.account().usernameSyncState) {
UsernameSyncState.USERNAME_AND_LINK_CORRUPTED -> true
UsernameSyncState.LINK_CORRUPTED -> true
UsernameSyncState.IN_SYNC -> false
}
}
}

View File

@@ -120,8 +120,6 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
override fun onWillFinish() {
if (wasConfigurationUpdated) {
setResult(MainActivity.RESULT_CONFIG_CHANGED)
} else {
setResult(RESULT_OK)
}
}

View File

@@ -10,6 +10,7 @@ import androidx.navigation.fragment.findNavController
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.signal.core.util.isNotNullOrBlank
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.BadgeImageView
import org.thoughtcrime.securesms.components.AvatarImageView
@@ -27,7 +28,6 @@ import org.thoughtcrime.securesms.components.settings.PreferenceViewHolder
import org.thoughtcrime.securesms.components.settings.app.subscription.completed.TerminalDonationDelegate
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.events.ReminderUpdateEvent
import org.thoughtcrime.securesms.keyvalue.AccountValues
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
import org.thoughtcrime.securesms.recipients.Recipient
@@ -42,6 +42,7 @@ import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.views.Stub
import org.thoughtcrime.securesms.util.visible
class AppSettingsFragment : DSLSettingsFragment(
titleId = R.string.text_secure_normal__menu_settings,
@@ -219,7 +220,7 @@ class AppSettingsFragment : DSLSettingsFragment(
clickPref(
title = DSLSettingsText.from(R.string.preferences__privacy),
icon = DSLSettingsIcon.from(R.drawable.symbol_lock_24),
icon = DSLSettingsIcon.from(R.drawable.symbol_lock_white_48),
onClick = {
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_privacySettingsFragment)
},
@@ -340,6 +341,7 @@ class AppSettingsFragment : DSLSettingsFragment(
private val aboutView: EmojiTextView = itemView.findViewById(R.id.about)
private val badgeView: BadgeImageView = itemView.findViewById(R.id.badge)
private val qrButton: View = itemView.findViewById(R.id.qr_button)
private val usernameView: TextView = itemView.findViewById(R.id.username)
init {
aboutView.setOverflowText(" ")
@@ -352,6 +354,8 @@ class AppSettingsFragment : DSLSettingsFragment(
titleView.text = model.recipient.profileName.toString()
summaryView.text = PhoneNumberFormatter.prettyPrint(model.recipient.requireE164())
usernameView.text = model.recipient.username.orElse("")
usernameView.visible = model.recipient.username.isPresent
avatarView.setRecipient(Recipient.self())
badgeView.setBadgeFromRecipient(Recipient.self())
@@ -359,7 +363,7 @@ class AppSettingsFragment : DSLSettingsFragment(
summaryView.visibility = View.VISIBLE
avatarView.visibility = View.VISIBLE
if (FeatureFlags.usernames() && SignalStore.account().usernameSyncState == AccountValues.UsernameSyncState.IN_SYNC) {
if (SignalStore.account().username.isNotNullOrBlank()) {
qrButton.visibility = View.VISIBLE
qrButton.isClickable = true
qrButton.setOnClickListener { model.onQrButtonClicked() }

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.components.settings.app.changenumber
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Bundle
@@ -47,6 +48,7 @@ class ChangeNumberLockActivity : PassphraseRequiredActivity() {
dynamicTheme.onResume(this)
}
@SuppressLint("MissingSuperCall")
override fun onBackPressed() = Unit
private fun checkWhoAmI() {

View File

@@ -233,6 +233,7 @@ class ChangeNumberRepository(
SignalStore.account().setE164(e164)
SignalStore.account().setPni(pni)
ApplicationDependencies.resetProtocolStores()
ApplicationDependencies.getGroupsV2Authorization().clear()
@@ -250,6 +251,7 @@ class ChangeNumberRepository(
val pniIdentityKeyPair = IdentityKeyPair(metadata.pniIdentityKeyPair.toByteArray())
val pniRegistrationId = metadata.pniRegistrationId
val pniSignedPreyKeyId = metadata.pniSignedPreKeyId
val pniLastResortKyberPreKeyId = metadata.pniLastResortKyberPreKeyId
val pniProtocolStore = ApplicationDependencies.getProtocolStore().pni()
val pniMetadataStore = SignalStore.account().pniPreKeys
@@ -258,16 +260,22 @@ class ChangeNumberRepository(
SignalStore.account().setPniIdentityKeyAfterChangeNumber(pniIdentityKeyPair)
val signedPreKey = pniProtocolStore.loadSignedPreKey(pniSignedPreyKeyId)
val oneTimePreKeys = PreKeyUtil.generateAndStoreOneTimeEcPreKeys(pniProtocolStore, pniMetadataStore)
val oneTimeEcPreKeys = PreKeyUtil.generateAndStoreOneTimeEcPreKeys(pniProtocolStore, pniMetadataStore)
val lastResortKyberPreKey = pniProtocolStore.loadLastResortKyberPreKeys().firstOrNull { it.id == pniLastResortKyberPreKeyId }
val oneTimeKyberPreKeys = PreKeyUtil.generateAndStoreOneTimeKyberPreKeys(pniProtocolStore, pniMetadataStore)
if (lastResortKyberPreKey == null) {
Log.w(TAG, "Last-resort kyber prekey is missing!")
}
pniMetadataStore.activeSignedPreKeyId = signedPreKey.id
accountManager.setPreKeys(
PreKeyUpload(
serviceIdType = ServiceIdType.PNI,
signedPreKey = signedPreKey,
oneTimeEcPreKeys = oneTimePreKeys,
lastResortKyberPreKey = null,
oneTimeKyberPreKeys = null
oneTimeEcPreKeys = oneTimeEcPreKeys,
lastResortKyberPreKey = lastResortKyberPreKey,
oneTimeKyberPreKeys = oneTimeKyberPreKeys
)
)
pniMetadataStore.isSignedPreKeyRegistered = true
@@ -394,7 +402,8 @@ class ChangeNumberRepository(
previousPni = SignalStore.account().pni!!.toByteString(),
pniIdentityKeyPair = pniIdentity.serialize().toByteString(),
pniRegistrationId = pniRegistrationIds[primaryDeviceId]!!,
pniSignedPreKeyId = devicePniSignedPreKeys[primaryDeviceId]!!.keyId
pniSignedPreKeyId = devicePniSignedPreKeys[primaryDeviceId]!!.keyId,
pniLastResortKyberPreKeyId = devicePniLastResortKyberPreKeys[primaryDeviceId]!!.keyId
)
return ChangeNumberRequestData(request, metadata)

View File

@@ -1,26 +1,18 @@
package org.thoughtcrime.securesms.components.settings.app.chats
import android.app.Activity
import android.content.Intent
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.Navigation
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.chats.sms.SmsExportState
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.exporter.flow.SmsExportActivity
import org.thoughtcrime.securesms.exporter.flow.SmsExportDialogs
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate
class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__chats) {
private lateinit var viewModel: ChatsSettingsViewModel
private lateinit var smsExportLauncher: ActivityResultLauncher<Intent>
override fun onResume() {
super.onResume()
@@ -29,12 +21,6 @@ class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__ch
@Suppress("ReplaceGetOrSet")
override fun bindAdapter(adapter: MappingAdapter) {
smsExportLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
SmsExportDialogs.showSmsRemovalDialog(requireContext(), requireView())
}
}
viewModel = ViewModelProvider(this).get(ChatsSettingsViewModel::class.java)
viewModel.state.observe(viewLifecycleOwner) {
@@ -44,55 +30,6 @@ class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__ch
private fun getConfiguration(state: ChatsSettingsState): DSLConfiguration {
return configure {
if (!state.useAsDefaultSmsApp) {
when (state.smsExportState) {
SmsExportState.FETCHING -> Unit
SmsExportState.HAS_UNEXPORTED_MESSAGES -> {
clickPref(
title = DSLSettingsText.from(R.string.SmsSettingsFragment__export_sms_messages),
summary = DSLSettingsText.from(R.string.SmsSettingsFragment__you_can_export_your_sms_messages_to_your_phones_sms_database),
onClick = {
smsExportLauncher.launch(SmsExportActivity.createIntent(requireContext()))
}
)
dividerPref()
}
SmsExportState.ALL_MESSAGES_EXPORTED -> {
clickPref(
title = DSLSettingsText.from(R.string.SmsSettingsFragment__remove_sms_messages),
summary = DSLSettingsText.from(R.string.SmsSettingsFragment__remove_sms_messages_from_signal_to_clear_up_storage_space),
onClick = {
SmsExportDialogs.showSmsRemovalDialog(requireContext(), requireView())
}
)
clickPref(
title = DSLSettingsText.from(R.string.SmsSettingsFragment__export_sms_messages_again),
summary = DSLSettingsText.from(R.string.SmsSettingsFragment__exporting_again_can_result_in_duplicate_messages),
onClick = {
SmsExportDialogs.showSmsReExportDialog(requireContext()) {
smsExportLauncher.launch(SmsExportActivity.createIntent(requireContext(), isReExport = true))
}
}
)
dividerPref()
}
SmsExportState.NO_SMS_MESSAGES_IN_DATABASE -> Unit
SmsExportState.NOT_AVAILABLE -> Unit
}
} else {
clickPref(
title = DSLSettingsText.from(R.string.preferences__sms_mms),
onClick = {
Navigation.findNavController(requireView()).safeNavigate(R.id.action_chatsSettingsFragment_to_smsSettingsFragment)
}
)
dividerPref()
}
switchPref(
title = DSLSettingsText.from(R.string.preferences__generate_link_previews),
summary = DSLSettingsText.from(R.string.preferences__retrieve_link_previews_from_websites_for_messages),

View File

@@ -1,14 +1,10 @@
package org.thoughtcrime.securesms.components.settings.app.chats
import org.thoughtcrime.securesms.components.settings.app.chats.sms.SmsExportState
data class ChatsSettingsState(
val generateLinkPreviews: Boolean,
val useAddressBook: Boolean,
val keepMutedChatsArchived: Boolean,
val useSystemEmoji: Boolean,
val enterKeySends: Boolean,
val chatBackupsEnabled: Boolean,
val useAsDefaultSmsApp: Boolean,
val smsExportState: SmsExportState = SmsExportState.FETCHING
val chatBackupsEnabled: Boolean
)

View File

@@ -2,24 +2,18 @@ package org.thoughtcrime.securesms.components.settings.app.chats
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import org.thoughtcrime.securesms.components.settings.app.chats.sms.SmsSettingsRepository
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.BackupUtil
import org.thoughtcrime.securesms.util.ConversationUtil
import org.thoughtcrime.securesms.util.ThrottledDebouncer
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.livedata.Store
class ChatsSettingsViewModel @JvmOverloads constructor(
private val repository: ChatsSettingsRepository = ChatsSettingsRepository(),
smsSettingsRepository: SmsSettingsRepository = SmsSettingsRepository()
private val repository: ChatsSettingsRepository = ChatsSettingsRepository()
) : ViewModel() {
private val refreshDebouncer = ThrottledDebouncer(500L)
private val disposables = CompositeDisposable()
private val store: Store<ChatsSettingsState> = Store(
ChatsSettingsState(
@@ -28,23 +22,12 @@ class ChatsSettingsViewModel @JvmOverloads constructor(
keepMutedChatsArchived = SignalStore.settings().shouldKeepMutedChatsArchived(),
useSystemEmoji = SignalStore.settings().isPreferSystemEmoji,
enterKeySends = SignalStore.settings().isEnterKeySends,
chatBackupsEnabled = SignalStore.settings().isBackupEnabled && BackupUtil.canUserAccessBackupDirectory(ApplicationDependencies.getApplication()),
useAsDefaultSmsApp = Util.isDefaultSmsProvider(ApplicationDependencies.getApplication())
chatBackupsEnabled = SignalStore.settings().isBackupEnabled && BackupUtil.canUserAccessBackupDirectory(ApplicationDependencies.getApplication())
)
)
val state: LiveData<ChatsSettingsState> = store.stateLiveData
init {
disposables += smsSettingsRepository.getSmsExportState().subscribe { state ->
store.update { it.copy(smsExportState = state) }
}
}
override fun onCleared() {
disposables.clear()
}
fun setGenerateLinkPreviewsEnabled(enabled: Boolean) {
store.update { it.copy(generateLinkPreviews = enabled) }
SignalStore.settings().isLinkPreviewsEnabled = enabled

View File

@@ -1,9 +0,0 @@
package org.thoughtcrime.securesms.components.settings.app.chats.sms
enum class SmsExportState {
FETCHING,
HAS_UNEXPORTED_MESSAGES,
ALL_MESSAGES_EXPORTED,
NO_SMS_MESSAGES_IN_DATABASE,
NOT_AVAILABLE
}

View File

@@ -1,153 +0,0 @@
package org.thoughtcrime.securesms.components.settings.app.chats.sms
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.os.Build
import android.provider.Settings
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.components.settings.models.OutlinedLearnMore
import org.thoughtcrime.securesms.exporter.flow.SmsExportActivity
import org.thoughtcrime.securesms.exporter.flow.SmsExportDialogs
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
private const val SMS_REQUEST_CODE: Short = 1234
class SmsSettingsFragment : DSLSettingsFragment(R.string.preferences__sms_mms) {
private lateinit var viewModel: SmsSettingsViewModel
private lateinit var smsExportLauncher: ActivityResultLauncher<Intent>
override fun onResume() {
super.onResume()
viewModel.checkSmsEnabled()
}
override fun bindAdapter(adapter: MappingAdapter) {
OutlinedLearnMore.register(adapter)
smsExportLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
SmsExportDialogs.showSmsRemovalDialog(requireContext(), requireView())
}
}
viewModel = ViewModelProvider(this)[SmsSettingsViewModel::class.java]
viewModel.state.observe(viewLifecycleOwner) {
adapter.submitList(getConfiguration(it).toMappingModelList())
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (Util.isDefaultSmsProvider(requireContext())) {
SignalStore.settings().setDefaultSms(true)
} else {
SignalStore.settings().setDefaultSms(false)
findNavController().navigateUp()
}
}
private fun getConfiguration(state: SmsSettingsState): DSLConfiguration {
return configure {
if (state.useAsDefaultSmsApp) {
customPref(
OutlinedLearnMore.Model(
summary = DSLSettingsText.from(R.string.SmsSettingsFragment__sms_support_will_be_removed_soon_to_focus_on_encrypted_messaging),
learnMoreUrl = getString(R.string.sms_export_url)
)
)
}
when (state.smsExportState) {
SmsExportState.FETCHING -> Unit
SmsExportState.HAS_UNEXPORTED_MESSAGES -> {
clickPref(
title = DSLSettingsText.from(R.string.SmsSettingsFragment__export_sms_messages),
summary = DSLSettingsText.from(R.string.SmsSettingsFragment__you_can_export_your_sms_messages_to_your_phones_sms_database),
onClick = {
smsExportLauncher.launch(SmsExportActivity.createIntent(requireContext()))
}
)
dividerPref()
}
SmsExportState.ALL_MESSAGES_EXPORTED -> {
clickPref(
title = DSLSettingsText.from(R.string.SmsSettingsFragment__remove_sms_messages),
summary = DSLSettingsText.from(R.string.SmsSettingsFragment__remove_sms_messages_from_signal_to_clear_up_storage_space),
onClick = {
SmsExportDialogs.showSmsRemovalDialog(requireContext(), requireView())
}
)
clickPref(
title = DSLSettingsText.from(R.string.SmsSettingsFragment__export_sms_messages_again),
summary = DSLSettingsText.from(R.string.SmsSettingsFragment__exporting_again_can_result_in_duplicate_messages),
onClick = {
SmsExportDialogs.showSmsReExportDialog(requireContext()) {
smsExportLauncher.launch(SmsExportActivity.createIntent(requireContext(), isReExport = true))
}
}
)
dividerPref()
}
SmsExportState.NO_SMS_MESSAGES_IN_DATABASE -> Unit
SmsExportState.NOT_AVAILABLE -> Unit
}
if (state.useAsDefaultSmsApp) {
@Suppress("DEPRECATION")
clickPref(
title = DSLSettingsText.from(R.string.SmsSettingsFragment__use_as_default_sms_app),
summary = DSLSettingsText.from(R.string.arrays__enabled),
onClick = {
startDefaultAppSelectionIntent()
}
)
}
switchPref(
title = DSLSettingsText.from(R.string.preferences__sms_delivery_reports),
summary = DSLSettingsText.from(R.string.preferences__request_a_delivery_report_for_each_sms_message_you_send),
isChecked = state.smsDeliveryReportsEnabled,
onClick = {
viewModel.setSmsDeliveryReportsEnabled(!state.smsDeliveryReportsEnabled)
}
)
switchPref(
title = DSLSettingsText.from(R.string.preferences__support_wifi_calling),
summary = DSLSettingsText.from(R.string.preferences__enable_if_your_device_supports_sms_mms_delivery_over_wifi),
isChecked = state.wifiCallingCompatibilityEnabled,
onClick = {
viewModel.setWifiCallingCompatibilityEnabled(!state.wifiCallingCompatibilityEnabled)
}
)
}
}
// Linter isn't smart enough to figure out the else only happens if API >= 24
@SuppressLint("InlinedApi")
@Suppress("DEPRECATION")
private fun startDefaultAppSelectionIntent() {
val intent: Intent = when {
Build.VERSION.SDK_INT < 23 -> Intent(Settings.ACTION_WIRELESS_SETTINGS)
Build.VERSION.SDK_INT < 24 -> Intent(Settings.ACTION_SETTINGS)
else -> Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS)
}
startActivityForResult(intent, SMS_REQUEST_CODE.toInt())
}
}

View File

@@ -1,40 +0,0 @@
package org.thoughtcrime.securesms.components.settings.app.chats.sms
import androidx.annotation.WorkerThread
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.SignalDatabase
class SmsSettingsRepository(
private val smsDatabase: MessageTable = SignalDatabase.messages,
private val mmsDatabase: MessageTable = SignalDatabase.messages
) {
fun getSmsExportState(): Single<SmsExportState> {
return Single.fromCallable {
checkInsecureMessageCount() ?: checkUnexportedInsecureMessageCount()
}.subscribeOn(Schedulers.io())
}
@WorkerThread
private fun checkInsecureMessageCount(): SmsExportState? {
val totalSmsMmsCount = smsDatabase.getInsecureMessageCount() + mmsDatabase.getInsecureMessageCount()
return if (totalSmsMmsCount == 0) {
SmsExportState.NO_SMS_MESSAGES_IN_DATABASE
} else {
null
}
}
@WorkerThread
private fun checkUnexportedInsecureMessageCount(): SmsExportState {
val totalUnexportedCount = smsDatabase.getUnexportedInsecureMessagesCount() + mmsDatabase.getUnexportedInsecureMessagesCount()
return if (totalUnexportedCount > 0) {
SmsExportState.HAS_UNEXPORTED_MESSAGES
} else {
SmsExportState.ALL_MESSAGES_EXPORTED
}
}
}

View File

@@ -1,8 +0,0 @@
package org.thoughtcrime.securesms.components.settings.app.chats.sms
data class SmsSettingsState(
val useAsDefaultSmsApp: Boolean,
val smsDeliveryReportsEnabled: Boolean,
val wifiCallingCompatibilityEnabled: Boolean,
val smsExportState: SmsExportState = SmsExportState.FETCHING
)

View File

@@ -1,50 +0,0 @@
package org.thoughtcrime.securesms.components.settings.app.chats.sms
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.livedata.Store
class SmsSettingsViewModel : ViewModel() {
private val repository = SmsSettingsRepository()
private val disposables = CompositeDisposable()
private val store = Store(
SmsSettingsState(
useAsDefaultSmsApp = Util.isDefaultSmsProvider(ApplicationDependencies.getApplication()),
smsDeliveryReportsEnabled = SignalStore.settings().isSmsDeliveryReportsEnabled,
wifiCallingCompatibilityEnabled = SignalStore.settings().isWifiCallingCompatibilityModeEnabled
)
)
val state: LiveData<SmsSettingsState> = store.stateLiveData
init {
disposables += repository.getSmsExportState().subscribe { state ->
store.update { it.copy(smsExportState = state) }
}
}
override fun onCleared() {
disposables.clear()
}
fun setSmsDeliveryReportsEnabled(enabled: Boolean) {
store.update { it.copy(smsDeliveryReportsEnabled = enabled) }
SignalStore.settings().isSmsDeliveryReportsEnabled = enabled
}
fun setWifiCallingCompatibilityEnabled(enabled: Boolean) {
store.update { it.copy(wifiCallingCompatibilityEnabled = enabled) }
SignalStore.settings().isWifiCallingCompatibilityModeEnabled = enabled
}
fun checkSmsEnabled() {
store.update { it.copy(useAsDefaultSmsApp = Util.isDefaultSmsProvider(ApplicationDependencies.getApplication())) }
}
}

View File

@@ -186,6 +186,48 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
}
)
clickPref(
title = DSLSettingsText.from("Log dump PreKey ServiceId-KeyIds"),
onClick = {
logPreKeyIds()
}
)
clickPref(
title = DSLSettingsText.from("Retry all jobs now"),
summary = DSLSettingsText.from("Clear backoff intervals, app will restart"),
onClick = {
SimpleTask.run({
JobDatabase.getInstance(ApplicationDependencies.getApplication()).debugResetBackoffInterval()
}) {
AppUtil.restart(requireContext())
}
}
)
clickPref(
title = DSLSettingsText.from("Delete all prekeys"),
summary = DSLSettingsText.from("Deletes all signed/last-resort/one-time prekeys for both ACI and PNI accounts. WILL cause problems."),
onClick = {
MaterialAlertDialogBuilder(requireContext())
.setTitle("Delete all prekeys?")
.setMessage("Are you sure? This will delete all prekeys for both ACI and PNI accounts. This WILL cause problems.")
.setPositiveButton(android.R.string.ok) { _, _ ->
SignalDatabase.signedPreKeys.debugDeleteAll()
SignalDatabase.oneTimePreKeys.debugDeleteAll()
SignalDatabase.kyberPreKeys.debugDeleteAll()
Toast.makeText(requireContext(), "All prekeys deleted!", Toast.LENGTH_SHORT).show()
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
)
dividerPref()
sectionHeaderPref(DSLSettingsText.from("Logging"))
clickPref(
title = DSLSettingsText.from("Clear all logs"),
onClick = {
@@ -227,21 +269,10 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
)
clickPref(
title = DSLSettingsText.from("Log dump PreKey ServiceId-KeyIds"),
title = DSLSettingsText.from("Clear local metrics"),
summary = DSLSettingsText.from("Click to clear all local metrics state."),
onClick = {
logPreKeyIds()
}
)
clickPref(
title = DSLSettingsText.from("Retry all jobs now"),
summary = DSLSettingsText.from("Clear backoff intervals, app will restart"),
onClick = {
SimpleTask.run({
JobDatabase.getInstance(ApplicationDependencies.getApplication()).debugResetBackoffInterval()
}) {
AppUtil.restart(requireContext())
}
clearAllLocalMetricsState()
}
)
@@ -436,18 +467,6 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
dividerPref()
sectionHeaderPref(DSLSettingsText.from("Local Metrics"))
clickPref(
title = DSLSettingsText.from("Clear local metrics"),
summary = DSLSettingsText.from("Click to clear all local metrics state."),
onClick = {
clearAllLocalMetricsState()
}
)
dividerPref()
sectionHeaderPref(DSLSettingsText.from("Group call server"))
radioPref(
@@ -716,13 +735,6 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
}
)
clickPref(
title = DSLSettingsText.from("Clear Username education ui hint"),
onClick = {
SignalStore.uiHints().clearHasSeenUsernameEducation()
}
)
clickPref(
title = DSLSettingsText.from("Corrupt username"),
summary = DSLSettingsText.from("Changes our local username without telling the server so it falls out of sync. Refresh profile afterwards to trigger corruption."),

View File

@@ -95,8 +95,7 @@ fun ResultItem(result: InternalSearchResult, modifier: Modifier = Modifier) {
.clickable {
if (activity != null) {
RecipientBottomSheetDialogFragment
.create(result.id, result.groupId)
.show(activity.supportFragmentManager, "TAG")
.show(activity.supportFragmentManager, result.id, result.groupId)
}
}
.padding(8.dp)

View File

@@ -40,7 +40,6 @@ import org.thoughtcrime.securesms.service.KeyCachingService
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.ConversationUtil
import org.thoughtcrime.securesms.util.ExpirationUtil
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.ServiceUtil
import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
@@ -119,18 +118,16 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
private fun getConfiguration(state: PrivacySettingsState): DSLConfiguration {
return configure {
if (FeatureFlags.phoneNumberPrivacy()) {
clickPref(
title = DSLSettingsText.from(R.string.preferences_app_protection__phone_number),
summary = DSLSettingsText.from(R.string.preferences_app_protection__choose_who_can_see),
onClick = {
Navigation.findNavController(requireView())
.safeNavigate(R.id.action_privacySettingsFragment_to_phoneNumberPrivacySettingsFragment)
}
)
clickPref(
title = DSLSettingsText.from(R.string.preferences_app_protection__phone_number),
summary = DSLSettingsText.from(R.string.preferences_app_protection__choose_who_can_see),
onClick = {
Navigation.findNavController(requireView())
.safeNavigate(R.id.action_privacySettingsFragment_to_phoneNumberPrivacySettingsFragment)
}
)
dividerPref()
}
dividerPref()
clickPref(
title = DSLSettingsText.from(R.string.PrivacySettingsFragment__blocked),
@@ -193,7 +190,7 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
MaterialAlertDialogBuilder(requireContext()).apply {
setTitle(R.string.ApplicationPreferencesActivity_disable_passphrase)
setMessage(R.string.ApplicationPreferencesActivity_this_will_permanently_unlock_signal_and_message_notifications)
setIcon(R.drawable.ic_warning)
setIcon(R.drawable.symbol_error_triangle_fill_24)
setPositiveButton(R.string.ApplicationPreferencesActivity_disable) { _, _ ->
MasterSecretUtil.changeMasterSecretPassphrase(
activity,

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.components.settings.app.privacy.pnp
import android.content.res.Configuration
import android.os.Bundle
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
@@ -18,6 +19,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
@@ -27,6 +29,7 @@ import org.signal.core.ui.Dividers
import org.signal.core.ui.Rows
import org.signal.core.ui.Scaffolds
import org.signal.core.ui.Texts
import org.signal.core.ui.theme.SignalTheme
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.compose.StatusBarColorNestedScrollConnection
@@ -49,118 +52,179 @@ class PhoneNumberPrivacySettingsFragment : ComposeFragment() {
@Composable
override fun FragmentContent() {
val state: PhoneNumberPrivacySettingsState by viewModel.state
val onNavigationClick: () -> Unit = remember {
{ findNavController().popBackStack() }
}
val snackbarHostState = remember { SnackbarHostState() }
val snackbarMessage = stringResource(id = R.string.PhoneNumberPrivacySettingsFragment__to_change_this_setting)
Scaffolds.Settings(
title = stringResource(id = R.string.preferences_app_protection__phone_number),
onNavigationClick = onNavigationClick,
navigationIconPainter = painterResource(id = R.drawable.ic_arrow_left_24),
navigationContentDescription = stringResource(id = R.string.Material3SearchToolbar__close),
snackbarHost = {
SnackbarHost(snackbarHostState)
},
modifier = Modifier.nestedScroll(statusBarNestedScrollConnection)
) { contentPadding ->
Box(modifier = Modifier.padding(contentPadding)) {
LazyColumn {
item {
Texts.SectionHeader(
text = stringResource(id = R.string.PhoneNumberPrivacySettingsFragment__who_can_see_my_number)
Screen(
state = state,
snackbarHostState = snackbarHostState,
onNavigationClick = { findNavController().popBackStack() },
statusBarNestedScrollConnection = statusBarNestedScrollConnection,
onEveryoneCanSeeMyNumberClicked = viewModel::setEveryoneCanSeeMyNumber,
onNobodyCanSeeMyNumberClicked = viewModel::setNobodyCanSeeMyNumber,
onEveryoneCanFindMeByNumberClicked = viewModel::setEveryoneCanFindMeByMyNumber,
onNobodyCanFindMeByNumberClicked = {
if (!state.phoneNumberSharing) {
viewModel.setNobodyCanFindMeByMyNumber()
} else {
lifecycleScope.launch {
snackbarHostState.showSnackbar(
message = snackbarMessage,
duration = SnackbarDuration.Short
)
}
}
}
)
}
}
item {
Rows.RadioRow(
selected = state.phoneNumberSharing,
text = stringResource(id = R.string.PhoneNumberPrivacy_everyone),
modifier = Modifier.clickable(onClick = viewModel::setEveryoneCanSeeMyNumber)
)
}
@Composable
private fun Screen(
state: PhoneNumberPrivacySettingsState,
snackbarHostState: SnackbarHostState = SnackbarHostState(),
onNavigationClick: () -> Unit = {},
statusBarNestedScrollConnection: StatusBarColorNestedScrollConnection? = null,
onEveryoneCanSeeMyNumberClicked: () -> Unit = {},
onNobodyCanSeeMyNumberClicked: () -> Unit = {},
onEveryoneCanFindMeByNumberClicked: () -> Unit = {},
onNobodyCanFindMeByNumberClicked: () -> Unit = {}
) {
Scaffolds.Settings(
title = stringResource(id = R.string.preferences_app_protection__phone_number),
onNavigationClick = onNavigationClick,
navigationIconPainter = painterResource(id = R.drawable.ic_arrow_left_24),
navigationContentDescription = stringResource(id = R.string.Material3SearchToolbar__close),
snackbarHost = {
SnackbarHost(snackbarHostState)
},
modifier = statusBarNestedScrollConnection?.let { Modifier.nestedScroll(it) } ?: Modifier
) { contentPadding ->
Box(modifier = Modifier.padding(contentPadding)) {
LazyColumn {
item {
Texts.SectionHeader(
text = stringResource(id = R.string.PhoneNumberPrivacySettingsFragment_who_can_see_my_number_heading)
)
}
item {
Rows.RadioRow(
selected = !state.phoneNumberSharing,
text = stringResource(id = R.string.PhoneNumberPrivacy_nobody),
modifier = Modifier.clickable(onClick = viewModel::setNobodyCanSeeMyNumber)
)
}
item {
Rows.RadioRow(
selected = state.phoneNumberSharing,
text = stringResource(id = R.string.PhoneNumberPrivacy_everyone),
modifier = Modifier.clickable(onClick = onEveryoneCanSeeMyNumberClicked)
)
}
item {
Text(
text = stringResource(
id = if (state.phoneNumberSharing) {
R.string.PhoneNumberPrivacySettingsFragment__your_phone_number
} else {
R.string.PhoneNumberPrivacySettingsFragment__nobody_will_see_your
}
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter), vertical = 16.dp)
)
}
item {
Rows.RadioRow(
selected = !state.phoneNumberSharing,
text = stringResource(id = R.string.PhoneNumberPrivacy_nobody),
modifier = Modifier.clickable(onClick = onNobodyCanSeeMyNumberClicked)
)
}
item {
Dividers.Default()
}
item {
Texts.SectionHeader(text = stringResource(id = R.string.PhoneNumberPrivacySettingsFragment__who_can_find_me_by_number))
}
item {
Rows.RadioRow(
selected = state.discoverableByPhoneNumber,
text = stringResource(id = R.string.PhoneNumberPrivacy_everyone),
modifier = Modifier.clickable(onClick = viewModel::setEveryoneCanFindMeByMyNumber)
)
}
item {
val snackbarMessage = stringResource(id = R.string.PhoneNumberPrivacySettingsFragment__to_change_this_setting)
val onClick: () -> Unit = remember(state.phoneNumberSharing) {
if (!state.phoneNumberSharing) {
{ viewModel.setNobodyCanFindMeByMyNumber() }
item {
Text(
text = stringResource(
id = if (state.phoneNumberSharing) {
R.string.PhoneNumberPrivacySettingsFragment_sharing_on_description
} else {
{
lifecycleScope.launch {
snackbarHostState.showSnackbar(
message = snackbarMessage,
duration = SnackbarDuration.Short
)
}
if (state.discoverableByPhoneNumber) {
R.string.PhoneNumberPrivacySettingsFragment_sharing_off_discovery_on_description
} else {
R.string.PhoneNumberPrivacySettingsFragment_sharing_off_discovery_off_description
}
}
}
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter), vertical = 16.dp)
)
}
Rows.RadioRow(
enabled = !state.phoneNumberSharing,
selected = !state.discoverableByPhoneNumber,
text = stringResource(id = R.string.PhoneNumberPrivacy_nobody),
modifier = Modifier.clickable(onClick = onClick)
)
}
item {
Dividers.Default()
}
item {
Text(
text = stringResource(
id = if (state.discoverableByPhoneNumber) {
R.string.PhoneNumberPrivacySettingsFragment__anyone_who_has
} else {
R.string.PhoneNumberPrivacySettingsFragment__nobody_will_be_able_to_see
}
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter), vertical = 16.dp)
)
}
item {
Texts.SectionHeader(text = stringResource(id = R.string.PhoneNumberPrivacySettingsFragment_who_can_find_me_by_number_heading))
}
item {
Rows.RadioRow(
selected = state.discoverableByPhoneNumber,
text = stringResource(id = R.string.PhoneNumberPrivacy_everyone),
modifier = Modifier.clickable(onClick = onEveryoneCanFindMeByNumberClicked)
)
}
item {
Rows.RadioRow(
enabled = !state.phoneNumberSharing,
selected = !state.discoverableByPhoneNumber,
text = stringResource(id = R.string.PhoneNumberPrivacy_nobody),
modifier = Modifier.clickable(onClick = onNobodyCanFindMeByNumberClicked)
)
}
item {
Text(
text = stringResource(
id = if (state.discoverableByPhoneNumber) {
R.string.PhoneNumberPrivacySettingsFragment_discovery_on_description
} else {
R.string.PhoneNumberPrivacySettingsFragment_discovery_off_description
}
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter), vertical = 16.dp)
)
}
}
}
}
}
@Preview(name = "Light Theme", group = "Screen", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", group = "Screen", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ScreenPreviewSharingAndDiscoverable() {
SignalTheme {
Screen(
state = PhoneNumberPrivacySettingsState(
phoneNumberSharing = true,
discoverableByPhoneNumber = true
)
)
}
}
@Preview(name = "Light Theme", group = "Screen", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", group = "Screen", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ScreenPreviewNotSharingDiscoverable() {
SignalTheme {
Screen(
state = PhoneNumberPrivacySettingsState(
phoneNumberSharing = false,
discoverableByPhoneNumber = true
)
)
}
}
@Preview(name = "Light Theme", group = "Screen", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", group = "Screen", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ScreenPreviewNotSharingNotDiscoverable() {
SignalTheme {
Screen(
state = PhoneNumberPrivacySettingsState(
phoneNumberSharing = false,
discoverableByPhoneNumber = false
)
)
}
}

View File

@@ -19,7 +19,7 @@ class PhoneNumberPrivacySettingsViewModel : ViewModel() {
private val _state = mutableStateOf(
PhoneNumberPrivacySettingsState(
phoneNumberSharing = SignalStore.phoneNumberPrivacy().isPhoneNumberSharingEnabled,
discoverableByPhoneNumber = SignalStore.phoneNumberPrivacy().isDiscoverableByPhoneNumber
discoverableByPhoneNumber = SignalStore.phoneNumberPrivacy().phoneNumberDiscoverabilityMode != PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE
)
)
@@ -60,7 +60,7 @@ class PhoneNumberPrivacySettingsViewModel : ViewModel() {
fun refresh() {
_state.value = PhoneNumberPrivacySettingsState(
phoneNumberSharing = SignalStore.phoneNumberPrivacy().isPhoneNumberSharingEnabled,
discoverableByPhoneNumber = SignalStore.phoneNumberPrivacy().isDiscoverableByPhoneNumber
discoverableByPhoneNumber = SignalStore.phoneNumberPrivacy().phoneNumberDiscoverabilityMode != PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE
)
}
}

View File

@@ -7,19 +7,18 @@ import kotlinx.parcelize.Parcelize
@Parcelize
data class PayPalConfirmationResult(
val payerId: String,
val paymentId: String,
val paymentId: String?,
val paymentToken: String
) : Parcelable {
companion object {
private const val KEY_PAYER_ID = "PayerID"
private const val KEY_PAYMENT_ID = "paymentId"
private const val KEY_PAYMENT_TOKEN = "token"
fun fromUrl(url: String): PayPalConfirmationResult? {
val uri = Uri.parse(url)
return PayPalConfirmationResult(
payerId = uri.getQueryParameter(KEY_PAYER_ID) ?: return null,
paymentId = uri.getQueryParameter(KEY_PAYMENT_ID) ?: return null,
paymentId = null,
paymentToken = uri.getQueryParameter(KEY_PAYMENT_TOKEN) ?: return null
)
}

View File

@@ -126,7 +126,7 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
val listener = FragmentResultListener { _, bundle ->
val result: PayPalConfirmationResult? = bundle.getParcelableCompat(PayPalConfirmationDialogFragment.REQUEST_KEY, PayPalConfirmationResult::class.java)
if (result != null) {
emitter.onSuccess(result)
emitter.onSuccess(result.copy(paymentId = createPaymentIntentResponse.paymentId))
} else {
emitter.onError(DonationError.UserCancelledPaymentError(args.request.donateToSignalType.toErrorSource()))
}

View File

@@ -49,6 +49,7 @@ import androidx.lifecycle.LifecycleOwner
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.navigation.navGraphViewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.signal.core.ui.Buttons
import org.signal.core.ui.Scaffolds
import org.signal.core.ui.Texts
@@ -156,13 +157,30 @@ class IdealTransferDetailsFragment : ComposeFragment(), DonationCheckoutDelegate
}
private fun onDonateClick() {
stripePaymentViewModel.provideIDEALData(viewModel.state.value.asIDEALData())
findNavController().safeNavigate(
IdealTransferDetailsFragmentDirections.actionBankTransferDetailsFragmentToStripePaymentInProgressFragment(
DonationProcessorAction.PROCESS_NEW_DONATION,
args.request
val state = viewModel.state.value
val continueTransfer = {
stripePaymentViewModel.provideIDEALData(state.asIDEALData())
findNavController().safeNavigate(
IdealTransferDetailsFragmentDirections.actionBankTransferDetailsFragmentToStripePaymentInProgressFragment(
DonationProcessorAction.PROCESS_NEW_DONATION,
args.request
)
)
)
}
if (args.request.donateToSignalType == DonateToSignalType.MONTHLY) {
MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.IdealTransferDetailsFragment__confirm_your_donation_with_s, getString(state.idealBank!!.getUIValues().name)))
.setMessage(R.string.IdealTransferDetailsFragment__monthly_ideal_warning)
.setPositiveButton(R.string.IdealTransferDetailsFragment__continue) { _, _ ->
continueTransfer()
}
.setNegativeButton(android.R.string.cancel, null)
.show()
} else {
continueTransfer()
}
}
override fun onUserLaunchedAnExternalApplication() {

View File

@@ -8,6 +8,7 @@ import androidx.compose.ui.graphics.Color
enum class UsernameQrCodeColorScheme(
val borderColor: Color,
val foregroundColor: Color,
val backgroundColor: Color,
val textColor: Color = Color.White,
val outlineColor: Color = Color.Transparent,
private val key: String
@@ -15,11 +16,13 @@ enum class UsernameQrCodeColorScheme(
Blue(
borderColor = Color(0xFF506ECD),
foregroundColor = Color(0xFF2449C0),
backgroundColor = Color(0xFFEDF0FA),
key = "blue"
),
White(
borderColor = Color(0xFFFFFFFF),
foregroundColor = Color(0xFF000000),
backgroundColor = Color(0xFFF5F5F5),
textColor = Color.Black,
outlineColor = Color(0xFFE9E9E9),
key = "white"
@@ -27,31 +30,37 @@ enum class UsernameQrCodeColorScheme(
Grey(
borderColor = Color(0xFF6A6C74),
foregroundColor = Color(0xFF464852),
backgroundColor = Color(0xFFF0F0F1),
key = "grey"
),
Tan(
borderColor = Color(0xFFBBB29A),
foregroundColor = Color(0xFF73694F),
backgroundColor = Color(0xFFF6F5F2),
key = "tan"
),
Green(
borderColor = Color(0xFF97AA89),
foregroundColor = Color(0xFF55733F),
backgroundColor = Color(0xFFF2F5F0),
key = "green"
),
Orange(
borderColor = Color(0xFFDE7134),
foregroundColor = Color(0xFFDA6C2E),
backgroundColor = Color(0xFFFCF1EB),
key = "orange"
),
Pink(
borderColor = Color(0xFFEA7B9D),
foregroundColor = Color(0xFFBB617B),
backgroundColor = Color(0xFFFCF1F5),
key = "pink"
),
Purple(
borderColor = Color(0xFF9E7BE9),
foregroundColor = Color(0xFF7651C5),
backgroundColor = Color(0xFFF5F3FA),
key = "purple"
);

View File

@@ -1,7 +1,8 @@
@file:OptIn(ExperimentalMaterial3Api::class)
@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class)
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
import android.app.Activity
import android.content.Intent
import android.content.res.Configuration
import android.graphics.Bitmap
@@ -11,6 +12,7 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -23,7 +25,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@@ -46,6 +47,8 @@ import androidx.compose.ui.unit.dp
import androidx.core.app.ShareCompat
import androidx.fragment.app.setFragmentResultListener
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import com.google.accompanist.permissions.ExperimentalPermissionsApi
@@ -53,12 +56,17 @@ import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.PermissionStatus
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import io.reactivex.rxjava3.disposables.CompositeDisposable
import kotlinx.coroutines.CoroutineScope
import org.signal.core.ui.Buttons
import org.signal.core.ui.Dialogs
import org.signal.core.ui.Snackbars
import org.signal.core.ui.theme.SignalTheme
import org.signal.core.util.concurrent.LifecycleDisposable
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeState
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.UsernameLinkSettingsState.ActiveTab
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.providers.BlobProvider
@@ -80,88 +88,33 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
override fun FragmentContent() {
val state by viewModel.state
val snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
val scope: CoroutineScope = rememberCoroutineScope()
val navController: NavController by remember { mutableStateOf(findNavController()) }
var showResetDialog: Boolean by remember { mutableStateOf(false) }
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val cameraPermissionState: PermissionState = rememberPermissionState(permission = android.Manifest.permission.CAMERA)
val linkCopiedEvent: UUID? by viewModel.linkCopiedEvent
val helpText = stringResource(id = R.string.UsernameLinkSettings_scan_this_qr_code)
val linkCopiedString = stringResource(R.string.UsernameLinkSettings_link_copied_toast)
LaunchedEffect(linkCopiedEvent) {
if (linkCopiedEvent != null) {
snackbarHostState.showSnackbar(linkCopiedString)
}
val cameraPermissionState: PermissionState = rememberPermissionState(permission = android.Manifest.permission.CAMERA) {
viewModel.onTabSelected(ActiveTab.Scan)
}
Scaffold(
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
topBar = {
TopAppBarContent(
activeTab = state.activeTab,
scrollBehavior = scrollBehavior,
onCodeTabSelected = { viewModel.onTabSelected(ActiveTab.Code) },
onScanTabSelected = { viewModel.onTabSelected(ActiveTab.Scan) },
cameraPermissionState = cameraPermissionState
)
},
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
) { contentPadding ->
if (state.indeterminateProgress) {
Dialogs.IndeterminateProgressDialog()
}
AnimatedVisibility(
visible = state.activeTab == ActiveTab.Code,
enter = slideInHorizontally(initialOffsetX = { fullWidth -> -fullWidth }),
exit = slideOutHorizontally(targetOffsetX = { fullWidth -> -fullWidth })
) {
UsernameLinkShareScreen(
state = state,
snackbarHostState = snackbarHostState,
scope = scope,
modifier = Modifier.padding(contentPadding),
navController = navController,
onShareBadge = {
shareQrBadge(viewModel.generateQrCodeImage())
},
onResetClicked = { showResetDialog = true },
onLinkResultHandled = { viewModel.onUsernameLinkResetResultHandled() }
)
}
AnimatedVisibility(
visible = state.activeTab == ActiveTab.Scan,
enter = slideInHorizontally(initialOffsetX = { fullWidth -> fullWidth }),
exit = slideOutHorizontally(targetOffsetX = { fullWidth -> fullWidth })
) {
UsernameQrScanScreen(
lifecycleOwner = viewLifecycleOwner,
disposables = disposables.disposables,
qrScanResult = state.qrScanResult,
onQrCodeScanned = { data -> viewModel.onQrCodeScanned(data) },
onQrResultHandled = { viewModel.onQrResultHandled() },
modifier = Modifier.padding(contentPadding)
)
}
}
if (showResetDialog) {
ResetDialog(
onConfirm = {
viewModel.onUsernameLinkReset()
showResetDialog = false
},
onDismiss = { showResetDialog = false }
)
}
MainScreen(
state = state,
navController = navController,
lifecycleOwner = viewLifecycleOwner,
disposables = disposables.disposables,
cameraPermissionState = cameraPermissionState,
onCodeTabSelected = { viewModel.onTabSelected(ActiveTab.Code) },
onScanTabSelected = { viewModel.onTabSelected(ActiveTab.Scan) },
onUsernameLinkResetResultHandled = { viewModel.onUsernameLinkResetResultHandled() },
onShareBadge = { shareQrBadge(requireActivity(), viewModel.generateQrCodeImage(helpText)) },
onQrCodeScanned = { data -> viewModel.onQrCodeScanned(data) },
onQrResultHandled = { viewModel.onQrResultHandled() },
onLinkReset = { viewModel.onUsernameLinkReset() },
onBackNavigationPressed = { requireActivity().onBackPressed() },
linkCopiedEvent = linkCopiedEvent
)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -172,153 +125,266 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
super.onResume()
viewModel.onResume()
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopAppBarContent(
activeTab: ActiveTab,
scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
onCodeTabSelected: () -> Unit = {},
onScanTabSelected: () -> Unit = {},
cameraPermissionState: PermissionState = previewPermissionState()
) {
CenterAlignedTopAppBar(
modifier = Modifier
.fillMaxWidth(),
title = {
Row(
modifier = Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
TabButton(
label = stringResource(R.string.UsernameLinkSettings_code_tab_name),
active = activeTab == ActiveTab.Code,
onClick = onCodeTabSelected,
modifier = Modifier.padding(end = 8.dp)
)
TabButton(
label = stringResource(R.string.UsernameLinkSettings_scan_tab_name),
active = activeTab == ActiveTab.Scan,
onClick = {
if (cameraPermissionState.status.isGranted) {
onScanTabSelected()
} else {
cameraPermissionState.launchPermissionRequest()
}
},
modifier = Modifier.padding(end = 8.dp)
)
}
},
navigationIcon = {
IconButton(
onClick = { requireActivity().onBackPressed() }
) {
Icon(
painter = painterResource(R.drawable.symbol_x_24),
contentDescription = stringResource(android.R.string.cancel)
)
}
},
scrollBehavior = scrollBehavior
)
@Composable
private fun MainScreen(
state: UsernameLinkSettingsState,
navController: NavController? = null,
lifecycleOwner: LifecycleOwner = previewLifecycleOwner,
disposables: CompositeDisposable = CompositeDisposable(),
cameraPermissionState: PermissionState = previewPermissionState(),
onCodeTabSelected: () -> Unit = {},
onScanTabSelected: () -> Unit = {},
onUsernameLinkResetResultHandled: () -> Unit = {},
onShareBadge: () -> Unit = {},
onQrCodeScanned: (String) -> Unit = {},
onQrResultHandled: () -> Unit = {},
onLinkReset: () -> Unit = {},
onBackNavigationPressed: () -> Unit = {},
linkCopiedEvent: UUID? = null
) {
val snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
val scope: CoroutineScope = rememberCoroutineScope()
var showResetDialog: Boolean by remember { mutableStateOf(false) }
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val linkCopiedString = stringResource(R.string.UsernameLinkSettings_link_copied_toast)
LaunchedEffect(linkCopiedEvent) {
if (linkCopiedEvent != null) {
snackbarHostState.showSnackbar(linkCopiedString)
}
}
@Composable
private fun TabButton(label: String, active: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
val colors = if (active) {
ButtonDefaults.filledTonalButtonColors()
} else {
ButtonDefaults.buttonColors(
containerColor = SignalTheme.colors.colorSurface2,
contentColor = MaterialTheme.colorScheme.onSurface
Scaffold(
snackbarHost = { Snackbars.Host(snackbarHostState) },
topBar = {
TopAppBarContent(
activeTab = state.activeTab,
scrollBehavior = scrollBehavior,
onCodeTabSelected = onCodeTabSelected,
onScanTabSelected = onScanTabSelected,
cameraPermissionState = cameraPermissionState,
onBackNavigationPressed = onBackNavigationPressed
)
},
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
) { contentPadding ->
if (state.indeterminateProgress) {
Dialogs.IndeterminateProgressDialog()
}
AnimatedVisibility(
visible = state.activeTab == ActiveTab.Code,
enter = slideInHorizontally(initialOffsetX = { fullWidth -> -fullWidth }),
exit = slideOutHorizontally(targetOffsetX = { fullWidth -> -fullWidth })
) {
UsernameLinkShareScreen(
state = state,
snackbarHostState = snackbarHostState,
scope = scope,
modifier = Modifier.padding(contentPadding),
navController = navController,
onShareBadge = onShareBadge,
onResetClicked = { showResetDialog = true },
onLinkResultHandled = onUsernameLinkResetResultHandled
)
}
Buttons.Small(
onClick = onClick,
modifier = modifier.defaultMinSize(minWidth = 100.dp),
shape = RoundedCornerShape(12.dp),
colors = colors
AnimatedVisibility(
visible = state.activeTab == ActiveTab.Scan,
enter = slideInHorizontally(initialOffsetX = { fullWidth -> fullWidth }),
exit = slideOutHorizontally(targetOffsetX = { fullWidth -> fullWidth })
) {
Text(label)
UsernameQrScanScreen(
lifecycleOwner = lifecycleOwner,
disposables = disposables,
qrScanResult = state.qrScanResult,
onQrCodeScanned = onQrCodeScanned,
onQrResultHandled = onQrResultHandled,
modifier = Modifier.padding(contentPadding)
)
}
}
@Composable
private fun ResetDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
Dialogs.SimpleAlertDialog(
title = stringResource(id = R.string.UsernameLinkSettings_reset_link_dialog_title),
body = stringResource(id = R.string.UsernameLinkSettings_reset_link_dialog_body),
confirm = stringResource(id = R.string.UsernameLinkSettings_reset_link_dialog_confirm_button),
dismiss = stringResource(id = android.R.string.cancel),
onConfirm = onConfirm,
onDismiss = onDismiss
if (showResetDialog) {
ResetDialog(
onConfirm = {
onLinkReset()
showResetDialog = false
},
onDismiss = { showResetDialog = false }
)
}
}
@Preview
@Composable
private fun PreviewAppBar() {
SignalTheme {
Surface {
@Composable
private fun TopAppBarContent(
activeTab: ActiveTab,
scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
onCodeTabSelected: () -> Unit = {},
onScanTabSelected: () -> Unit = {},
cameraPermissionState: PermissionState = previewPermissionState(),
onBackNavigationPressed: () -> Unit = {}
) {
CenterAlignedTopAppBar(
modifier = Modifier
.fillMaxWidth(),
title = {
Row(
modifier = Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
TabButton(
label = stringResource(R.string.UsernameLinkSettings_code_tab_name),
active = activeTab == ActiveTab.Code,
onClick = onCodeTabSelected,
modifier = Modifier.padding(end = 8.dp)
)
TabButton(
label = stringResource(R.string.UsernameLinkSettings_scan_tab_name),
active = activeTab == ActiveTab.Scan,
onClick = {
if (cameraPermissionState.status.isGranted) {
onScanTabSelected()
} else {
cameraPermissionState.launchPermissionRequest()
}
},
modifier = Modifier.padding(end = 8.dp)
)
}
},
navigationIcon = {
IconButton(
onClick = onBackNavigationPressed
) {
Icon(
painter = painterResource(R.drawable.symbol_x_24),
contentDescription = stringResource(android.R.string.cancel)
)
}
},
scrollBehavior = scrollBehavior
)
}
@Composable
private fun TabButton(label: String, active: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
val colors = if (active) {
ButtonDefaults.filledTonalButtonColors()
} else {
ButtonDefaults.buttonColors(
containerColor = SignalTheme.colors.colorSurface2,
contentColor = MaterialTheme.colorScheme.onSurface
)
}
Buttons.Small(
onClick = onClick,
modifier = modifier.defaultMinSize(minWidth = 100.dp),
shape = RoundedCornerShape(12.dp),
colors = colors
) {
Text(label)
}
}
@Composable
private fun ResetDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
Dialogs.SimpleAlertDialog(
title = stringResource(id = R.string.UsernameLinkSettings_reset_link_dialog_title),
body = stringResource(id = R.string.UsernameLinkSettings_reset_link_dialog_body),
confirm = stringResource(id = R.string.UsernameLinkSettings_reset_link_dialog_confirm_button),
dismiss = stringResource(id = android.R.string.cancel),
onConfirm = onConfirm,
onDismiss = onDismiss
)
}
@Preview(name = "Light Theme", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun AppBarPreview() {
SignalTheme {
Surface {
Column {
TopAppBarContent(activeTab = ActiveTab.Code)
TopAppBarContent(activeTab = ActiveTab.Scan)
}
}
}
@Preview(name = "Light Theme", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewAll() {
FragmentContent()
}
@Preview
@Composable
private fun PreviewResetDialog() {
SignalTheme {
Surface {
ResetDialog(onConfirm = {}, onDismiss = {})
}
}
}
private fun previewPermissionState(): PermissionState {
return object : PermissionState {
override val permission: String = ""
override val status: PermissionStatus = PermissionStatus.Granted
override fun launchPermissionRequest() = Unit
}
}
private fun shareQrBadge(badge: Bitmap?) {
if (badge == null) {
return
}
try {
ByteArrayOutputStream().use { byteArrayOutputStream ->
badge.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
byteArrayOutputStream.flush()
val bytes = byteArrayOutputStream.toByteArray()
val shareUri = BlobProvider.getInstance()
.forData(bytes)
.withMimeType("image/png")
.withFileName("SignalGroupQr.png")
.createForSingleSessionInMemory()
val intent = ShareCompat.IntentBuilder.from(requireActivity())
.setType("image/png")
.setStream(shareUri)
.createChooserIntent()
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivity(intent)
}
} finally {
badge.recycle()
}
}
}
@Preview(name = "Light Theme", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun MainScreenPreview() {
SignalTheme {
MainScreen(
state = UsernameLinkSettingsState(
activeTab = ActiveTab.Code,
username = "PeterParker.42",
usernameLinkState = UsernameLinkState.Present("https://signal.org"),
qrCodeState = QrCodeState.Present(QrCodeData.forData("PeterParker.42", 64)),
qrCodeColorScheme = UsernameQrCodeColorScheme.Orange
)
)
}
}
@Preview(name = "Light Theme", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ResetDialogPreview() {
SignalTheme {
Surface {
ResetDialog(onConfirm = {}, onDismiss = {})
}
}
}
private fun previewPermissionState(): PermissionState {
return object : PermissionState {
override val permission: String = ""
override val status: PermissionStatus = PermissionStatus.Granted
override fun launchPermissionRequest() = Unit
}
}
private val previewLifecycleOwner: LifecycleOwner = object : LifecycleOwner {
override val lifecycle: Lifecycle
get() = throw UnsupportedOperationException("Only for tests")
}
private fun shareQrBadge(activity: Activity, badge: Bitmap?) {
if (badge == null) {
return
}
try {
ByteArrayOutputStream().use { byteArrayOutputStream ->
badge.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
byteArrayOutputStream.flush()
val bytes = byteArrayOutputStream.toByteArray()
val shareUri = BlobProvider.getInstance()
.forData(bytes)
.withMimeType("image/png")
.withFileName("SignalGroupQr.png")
.createForSingleSessionInMemory()
val intent = ShareCompat.IntentBuilder(activity)
.setType("image/png")
.setStream(shareUri)
.createChooserIntent()
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
activity.startActivity(intent)
}
} finally {
badge.recycle()
}
}

View File

@@ -10,6 +10,9 @@ import android.graphics.Rect
import android.graphics.RectF
import android.graphics.Typeface
import android.os.Build
import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
@@ -17,8 +20,10 @@ import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.graphics.withSave
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.core.graphics.withTranslation
import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single
@@ -122,7 +127,9 @@ class UsernameLinkSettingsViewModel : ViewModel() {
val components: Optional<UsernameLinkComponents> = when (result) {
is UsernameLinkResetResult.Success -> Optional.of(result.components)
is UsernameLinkResetResult.NetworkError -> Optional.empty()
else -> { usernameLink.value ?: Optional.empty() }
else -> {
usernameLink.value ?: Optional.empty()
}
}
_state.value = _state.value.copy(
@@ -200,7 +207,7 @@ class UsernameLinkSettingsViewModel : ViewModel() {
*
* I hate this as much as you do.
*/
fun generateQrCodeImage(): Bitmap? {
fun generateQrCodeImage(helpText: String): Bitmap? {
val state: UsernameLinkSettingsState = _state.value
if (state.qrCodeState !is QrCodeState.Present) {
@@ -210,12 +217,23 @@ class UsernameLinkSettingsViewModel : ViewModel() {
val qrCodeData: QrCodeData = state.qrCodeState.data
val width = 480
val height = 525
val qrSize = 300f
val qrPadding = 25f
val borderSizeX = 64f
val borderSizeY = 52f
val scaleFactor = 2
val width = 424 * scaleFactor
val height = 576 * scaleFactor
val backgroundPadHorizontal = 64f * scaleFactor
val backgroundPadVertical = 80f * scaleFactor
val qrBorderWidth = width - (backgroundPadHorizontal * 2)
val qrBorderHeight = 324f * scaleFactor
val qrBorderRadius = 30f * scaleFactor
val qrSize = 184f * scaleFactor
val qrPadding = 16f * scaleFactor
val borderSizeX = 40f * scaleFactor
val borderSizeY = 32f * scaleFactor
val helpTextHorizontalPad = 72 * scaleFactor
val helpTextVerticalPad = 444f * scaleFactor
val helpTextSize = 14f * scaleFactor
val usernameVerticalPad = 348f * scaleFactor
val usernameTextSize = 20f * scaleFactor
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply {
eraseColor(Color.TRANSPARENT)
@@ -225,43 +243,51 @@ class UsernameLinkSettingsViewModel : ViewModel() {
val composeCanvas = Canvas(androidCanvas)
val canvasDrawScope = CanvasDrawScope()
// Draw the background
androidCanvas.drawRoundRect(0f, 0f, width.toFloat(), height.toFloat(), 30f, 30f, Paint().apply { color = state.qrCodeColorScheme.borderColor.toArgb() })
androidCanvas.drawRoundRect(borderSizeX, borderSizeY, borderSizeX + qrSize + qrPadding * 2, borderSizeY + qrSize + qrPadding * 2, 15f, 15f, Paint().apply { color = Color.WHITE })
androidCanvas.drawRoundRect(
borderSizeX,
borderSizeY,
borderSizeX + qrSize + qrPadding * 2,
borderSizeY + qrSize + qrPadding * 2,
15f,
15f,
Paint().apply {
color = state.qrCodeColorScheme.outlineColor.toArgb()
style = Paint.Style.STROKE
strokeWidth = 4f
}
)
// Background
androidCanvas.drawColor(state.qrCodeColorScheme.backgroundColor.toArgb())
// Draw the QR code
composeCanvas.translate((width / 2) - (qrSize / 2), 80f)
canvasDrawScope.draw(
density = object : Density {
override val density: Float = 1f
override val fontScale: Float = 1f
},
layoutDirection = LayoutDirection.Ltr,
canvas = composeCanvas,
size = Size(qrSize, qrSize)
) {
drawQr(
data = qrCodeData,
foregroundColor = state.qrCodeColorScheme.foregroundColor,
backgroundColor = state.qrCodeColorScheme.borderColor,
deadzonePercent = 0.35f,
logo = null
// QR Border
androidCanvas.withTranslation(x = backgroundPadHorizontal, y = backgroundPadVertical) {
drawRoundRect(0f, 0f, qrBorderWidth, qrBorderHeight, qrBorderRadius, qrBorderRadius, Paint().apply { color = state.qrCodeColorScheme.borderColor.toArgb() })
drawRoundRect(borderSizeX, borderSizeY, borderSizeX + qrSize + qrPadding * 2, borderSizeY + qrSize + qrPadding * 2, 15f, 15f, Paint().apply { color = Color.WHITE })
drawRoundRect(
borderSizeX,
borderSizeY,
borderSizeX + qrSize + qrPadding * 2,
borderSizeY + qrSize + qrPadding * 2,
15f * scaleFactor,
15f * scaleFactor,
Paint().apply {
color = state.qrCodeColorScheme.outlineColor.toArgb()
style = Paint.Style.STROKE
strokeWidth = 4f
}
)
// Draw the QR code
composeCanvas.withSave {
composeCanvas.translate((qrBorderWidth / 2) - (qrSize / 2), borderSizeY + qrPadding)
canvasDrawScope.draw(
density = object : Density {
override val density: Float = 1f
override val fontScale: Float = 1f
},
layoutDirection = LayoutDirection.Ltr,
canvas = composeCanvas,
size = Size(qrSize, qrSize)
) {
drawQr(
data = qrCodeData,
foregroundColor = state.qrCodeColorScheme.foregroundColor,
backgroundColor = state.qrCodeColorScheme.borderColor,
deadzonePercent = 0.35f,
logo = null
)
}
}
}
composeCanvas.translate(-90f, -80f)
// Draw the signal logo -- unfortunately can't have the normal QR code drawing handle it because it requires a composable ImageBitmap
BitmapFactory.decodeResource(ApplicationDependencies.getApplication().resources, R.drawable.qrcode_logo).also { logoBitmap ->
@@ -269,14 +295,18 @@ class UsernameLinkSettingsViewModel : ViewModel() {
colorFilter = PorterDuffColorFilter(state.qrCodeColorScheme.foregroundColor.toArgb(), PorterDuff.Mode.SRC_IN)
}
val sourceRect = Rect(0, 0, logoBitmap.width, logoBitmap.height)
val destRect = RectF(210f, 200f, 270f, 260f)
val logoSize = 36f * scaleFactor
val destLeft = (width / 2f) - (logoSize / 2f)
val destTop = destLeft - (10f * scaleFactor) + (logoSize / 2f)
val destRect = RectF(destLeft, destTop, destLeft + logoSize, destTop + logoSize)
androidCanvas.drawBitmap(logoBitmap, sourceRect, destRect, tintedPaint)
}
// Draw the text
val textPaint = Paint().apply {
// Draw the username
val usernamePaint = TextPaint().apply {
color = state.qrCodeColorScheme.textColor.toArgb()
textSize = 34f
textSize = usernameTextSize
typeface = if (Build.VERSION.SDK_INT < 26) {
Typeface.DEFAULT_BOLD
} else {
@@ -286,10 +316,40 @@ class UsernameLinkSettingsViewModel : ViewModel() {
.build()
}
}
val textBounds = Rect()
textPaint.getTextBounds(state.username, 0, state.username.length, textBounds)
androidCanvas.drawText(state.username, (width / 2f) - (textBounds.width() / 2f), 465f, textPaint)
val usernameMaxWidth = qrBorderWidth - borderSizeX * 2f
val usernameLayout = StaticLayout(state.username, usernamePaint, usernameMaxWidth.toInt(), Layout.Alignment.ALIGN_CENTER, 1f, 0f, true)
val usernameVerticalOffset = when (usernameLayout.lineCount) {
1 -> 0f
2 -> usernameTextSize / 2f
else -> usernameTextSize
}
androidCanvas.withTranslation(x = backgroundPadHorizontal + borderSizeX, y = usernameVerticalPad - usernameVerticalOffset) {
usernameLayout.draw(this)
}
// Draw the help text
val helpTextPaint = TextPaint().apply {
isAntiAlias = true
color = 0xFF3C3C43.toInt()
textSize = helpTextSize
typeface = if (Build.VERSION.SDK_INT < 26) {
Typeface.DEFAULT
} else {
Typeface.Builder("")
.setFallback("sans-serif")
.setWeight(400)
.build()
}
}
val maxWidth = width - helpTextHorizontalPad * 2
val helpTextLayout = StaticLayout(helpText, helpTextPaint, maxWidth, Layout.Alignment.ALIGN_CENTER, 1f, 0f, true)
androidCanvas.withTranslation(x = helpTextHorizontalPad.toFloat(), y = helpTextVerticalPad) {
helpTextLayout.draw(androidCanvas)
}
return bitmap
}

View File

@@ -44,6 +44,8 @@ import org.thoughtcrime.securesms.components.settings.app.usernamelinks.Username
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.UsernameLinkSettingsState.ActiveTab
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.whispersystems.signalservice.api.push.UsernameLinkComponents
import java.util.UUID
/**
* A screen that shows all the data around your username link and how to share it, including a QR code.
@@ -54,7 +56,7 @@ fun UsernameLinkShareScreen(
onLinkResultHandled: () -> Unit,
snackbarHostState: SnackbarHostState,
scope: CoroutineScope,
navController: NavController,
navController: NavController?,
onShareBadge: () -> Unit,
modifier: Modifier = Modifier,
onResetClicked: () -> Unit
@@ -68,6 +70,12 @@ fun UsernameLinkShareScreen(
UsernameLinkResetResult.NetworkError -> {
ResetLinkResultDialog(stringResource(R.string.UsernameLinkSettings_reset_link_result_network_error), onDismiss = onLinkResultHandled)
}
UsernameLinkResetResult.UnexpectedError -> {
ResetLinkResultDialog(stringResource(R.string.UsernameLinkSettings_reset_link_result_unknown_error), onDismiss = onLinkResultHandled)
}
is UsernameLinkResetResult.Success -> {
ResetLinkResultDialog(stringResource(R.string.UsernameLinkSettings_reset_link_result_success), onDismiss = onLinkResultHandled)
}
else -> {}
}
@@ -93,21 +101,18 @@ fun UsernameLinkShareScreen(
ButtonBar(
onShareClicked = onShareBadge,
onColorClicked = { navController.safeNavigate(UsernameLinkSettingsFragmentDirections.actionUsernameLinkSettingsFragmentToUsernameLinkQrColorPickerFragment()) }
)
LinkRow(
linkState = state.usernameLinkState,
onClick = {
navController.safeNavigate(UsernameLinkSettingsFragmentDirections.actionUsernameLinkSettingsFragmentToUsernameLinkShareBottomSheet())
}
onColorClicked = { navController?.safeNavigate(UsernameLinkSettingsFragmentDirections.actionUsernameLinkSettingsFragmentToUsernameLinkQrColorPickerFragment()) },
onLinkClicked = {
navController?.safeNavigate(UsernameLinkSettingsFragmentDirections.actionUsernameLinkSettingsFragmentToUsernameLinkShareBottomSheet())
},
linkState = state.usernameLinkState
)
Text(
text = stringResource(id = R.string.UsernameLinkSettings_qr_description),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 19.dp, start = 43.dp, end = 43.dp),
modifier = Modifier.padding(top = 42.dp, bottom = 19.dp, start = 43.dp, end = 43.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
@@ -127,11 +132,22 @@ fun UsernameLinkShareScreen(
}
@Composable
private fun ButtonBar(onShareClicked: () -> Unit, onColorClicked: () -> Unit) {
private fun ButtonBar(
linkState: UsernameLinkState,
onLinkClicked: () -> Unit,
onShareClicked: () -> Unit,
onColorClicked: () -> Unit
) {
Row(
horizontalArrangement = Arrangement.spacedBy(space = 32.dp, alignment = Alignment.CenterHorizontally),
modifier = Modifier.fillMaxWidth()
) {
Buttons.ActionButton(
enabled = linkState is UsernameLinkState.Present,
onClick = onLinkClicked,
iconResId = R.drawable.symbol_link_24,
labelResId = R.string.UsernameLinkSettings_link_button_label
)
Buttons.ActionButton(
onClick = onShareClicked,
iconResId = R.drawable.symbol_share_android_24,
@@ -215,6 +231,82 @@ private fun ScreenPreview() {
}
}
@Preview(name = "Light Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ScreenPreviewResetSuccess() {
SignalTheme {
Surface {
UsernameLinkShareScreen(
state = previewState().copy(usernameLinkResetResult = UsernameLinkResetResult.Success(UsernameLinkComponents(Util.getSecretBytes(32), UUID.randomUUID()))),
snackbarHostState = SnackbarHostState(),
scope = rememberCoroutineScope(),
navController = NavController(LocalContext.current),
onShareBadge = {},
onResetClicked = {},
onLinkResultHandled = {}
)
}
}
}
@Preview(name = "Light Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ScreenPreviewResetNetworkError() {
SignalTheme {
Surface {
UsernameLinkShareScreen(
state = previewState().copy(usernameLinkResetResult = UsernameLinkResetResult.NetworkError),
snackbarHostState = SnackbarHostState(),
scope = rememberCoroutineScope(),
navController = NavController(LocalContext.current),
onShareBadge = {},
onResetClicked = {},
onLinkResultHandled = {}
)
}
}
}
@Preview(name = "Light Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ScreenPreviewResetNetworkUnavailable() {
SignalTheme {
Surface {
UsernameLinkShareScreen(
state = previewState().copy(usernameLinkResetResult = UsernameLinkResetResult.NetworkUnavailable),
snackbarHostState = SnackbarHostState(),
scope = rememberCoroutineScope(),
navController = NavController(LocalContext.current),
onShareBadge = {},
onResetClicked = {},
onLinkResultHandled = {}
)
}
}
}
@Preview(name = "Light Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ScreenPreviewResetUnexpectedError() {
SignalTheme {
Surface {
UsernameLinkShareScreen(
state = previewState().copy(usernameLinkResetResult = UsernameLinkResetResult.UnexpectedError),
snackbarHostState = SnackbarHostState(),
scope = rememberCoroutineScope(),
navController = NavController(LocalContext.current),
onShareBadge = {},
onResetClicked = {},
onLinkResultHandled = {}
)
}
}
}
@Preview(name = "Light Theme", group = "LinkRow", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", group = "LinkRow", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable

View File

@@ -9,16 +9,15 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@@ -46,13 +45,17 @@ fun UsernameQrScanScreen(
onQrResultHandled: () -> Unit,
modifier: Modifier = Modifier
) {
val path = remember { Path() }
when (qrScanResult) {
QrScanResult.InvalidData -> {
QrScanResultDialog(stringResource(R.string.UsernameLinkSettings_qr_result_invalid), onDismiss = onQrResultHandled)
}
QrScanResult.NetworkError -> {
QrScanResultDialog(stringResource(R.string.UsernameLinkSettings_qr_result_network_error), onDismiss = onQrResultHandled)
}
is QrScanResult.NotFound -> {
if (qrScanResult.username != null) {
QrScanResultDialog(stringResource(R.string.UsernameLinkSettings_qr_result_not_found, qrScanResult.username), onDismiss = onQrResultHandled)
@@ -60,10 +63,12 @@ fun UsernameQrScanScreen(
QrScanResultDialog(stringResource(R.string.UsernameLinkSettings_qr_result_not_found_no_username), onDismiss = onQrResultHandled)
}
}
is QrScanResult.Success -> {
CommunicationActions.startConversation(LocalContext.current, qrScanResult.recipient, null)
onQrResultHandled()
}
null -> {}
}
@@ -88,7 +93,7 @@ fun UsernameQrScanScreen(
.weight(1f, true)
.drawWithContent {
drawContent()
drawQrCrosshair()
drawQrCrosshair(path)
}
)
@@ -117,33 +122,39 @@ private fun QrScanResultDialog(message: String, onDismiss: () -> Unit) {
)
}
private fun DrawScope.drawQrCrosshair() {
private fun DrawScope.drawQrCrosshair(path: Path) {
val crosshairWidth: Float = size.minDimension * 0.6f
val clearWidth: Float = crosshairWidth * 0.75f
val crosshairLineLength = crosshairWidth * 0.125f
// Draw a full white rounded rect...
drawRoundRect(
color = Color.White,
topLeft = center - Offset(crosshairWidth / 2, crosshairWidth / 2),
style = Stroke(width = 3.dp.toPx()),
size = Size(crosshairWidth, crosshairWidth),
cornerRadius = CornerRadius(10.dp.toPx(), 10.dp.toPx())
)
val topLeft = center - Offset(crosshairWidth / 2, crosshairWidth / 2)
val topRight = center + Offset(crosshairWidth / 2, -crosshairWidth / 2)
val bottomRight = center + Offset(crosshairWidth / 2, crosshairWidth / 2)
val bottomLeft = center + Offset(-crosshairWidth / 2, crosshairWidth / 2)
// ...then cut out the middle parts with BlendMode.Clear to leave us with just the corners
drawRect(
color = Color.White,
topLeft = Offset(center.x - clearWidth / 2, 0f),
style = Fill,
size = Size(clearWidth, size.height),
blendMode = BlendMode.Clear
)
path.reset()
drawRect(
drawPath(
path = path.apply {
moveTo(topLeft.x, topLeft.y + crosshairLineLength)
lineTo(topLeft.x, topLeft.y)
lineTo(topLeft.x + crosshairLineLength, topLeft.y)
moveTo(topRight.x - crosshairLineLength, topRight.y)
lineTo(topRight.x, topRight.y)
lineTo(topRight.x, topRight.y + crosshairLineLength)
moveTo(bottomRight.x, bottomRight.y - crosshairLineLength)
lineTo(bottomRight.x, bottomRight.y)
lineTo(bottomRight.x - crosshairLineLength, bottomRight.y)
moveTo(bottomLeft.x + crosshairLineLength, bottomLeft.y)
lineTo(bottomLeft.x, bottomLeft.y)
lineTo(bottomLeft.x, bottomLeft.y - crosshairLineLength)
},
color = Color.White,
topLeft = Offset(0f, center.y - clearWidth / 2),
style = Fill,
size = Size(size.width, clearWidth),
blendMode = BlendMode.Clear
style = Stroke(
width = 3.dp.toPx(),
pathEffect = PathEffect.cornerPathEffect(10.dp.toPx())
)
)
}

View File

@@ -28,7 +28,9 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.DimensionUnit
import org.signal.core.util.Result
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.concurrent.addTo
import org.signal.core.util.getParcelableArrayListExtraCompat
import org.thoughtcrime.securesms.AvatarPreviewActivity
import org.thoughtcrime.securesms.BlockUnblockDialog
@@ -74,6 +76,7 @@ import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupInviteSentD
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupsLearnMoreBottomSheetDialogFragment
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientExporter
@@ -112,17 +115,17 @@ class ConversationSettingsFragment : DSLSettingsFragment(
private val alertTint by lazy { ContextCompat.getColor(requireContext(), R.color.signal_alert_primary) }
private val alertDisabledTint by lazy { ContextCompat.getColor(requireContext(), R.color.signal_alert_primary_50) }
private val blockIcon by lazy {
ContextUtil.requireDrawable(requireContext(), R.drawable.ic_block_tinted_24).apply {
ContextUtil.requireDrawable(requireContext(), R.drawable.symbol_block_24).apply {
colorFilter = PorterDuffColorFilter(alertTint, PorterDuff.Mode.SRC_IN)
}
}
private val unblockIcon by lazy {
ContextUtil.requireDrawable(requireContext(), R.drawable.ic_block_tinted_24)
ContextUtil.requireDrawable(requireContext(), R.drawable.symbol_block_24)
}
private val leaveIcon by lazy {
ContextUtil.requireDrawable(requireContext(), R.drawable.ic_leave_tinted_24).apply {
ContextUtil.requireDrawable(requireContext(), R.drawable.symbol_leave_24).apply {
colorFilter = PorterDuffColorFilter(alertTint, PorterDuff.Mode.SRC_IN)
}
}
@@ -135,7 +138,8 @@ class ConversationSettingsFragment : DSLSettingsFragment(
recipientId = args.recipientId,
groupId = ParcelableGroupId.get(groupId),
callMessageIds = args.callMessageIds ?: longArrayOf(),
repository = ConversationSettingsRepository(requireContext())
repository = ConversationSettingsRepository(requireContext()),
messageRequestRepository = MessageRequestRepository(requireContext())
)
}
)
@@ -691,7 +695,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
recipient = member.member,
isAdmin = member.isAdmin,
onClick = {
RecipientBottomSheetDialogFragment.create(member.member.id, groupState.groupId).show(parentFragmentManager, "BOTTOM")
RecipientBottomSheetDialogFragment.show(parentFragmentManager, member.member.id, groupState.groupId)
}
)
)
@@ -798,6 +802,49 @@ class ConversationSettingsFragment : DSLSettingsFragment(
}
}
)
val reportSpamTint = if (state.isDeprecatedOrUnregistered) R.color.signal_alert_primary_50 else R.color.signal_alert_primary
clickPref(
title = DSLSettingsText.from(R.string.ConversationFragment_report_spam, ContextCompat.getColor(requireContext(), reportSpamTint)),
icon = DSLSettingsIcon.from(R.drawable.symbol_spam_24, reportSpamTint),
isEnabled = !state.isDeprecatedOrUnregistered,
onClick = {
BlockUnblockDialog.showReportSpamFor(
requireContext(),
viewLifecycleOwner.lifecycle,
state.recipient,
{
viewModel
.onReportSpam()
.subscribeBy {
Toast.makeText(requireContext(), R.string.ConversationFragment_reported_as_spam, Toast.LENGTH_SHORT).show()
onToolbarNavigationClicked()
}
.addTo(lifecycleDisposable)
},
if (state.recipient.isBlocked) {
null
} else {
Runnable {
viewModel
.onBlockAndReportSpam()
.subscribeBy { result ->
when (result) {
is Result.Success -> {
Toast.makeText(requireContext(), R.string.ConversationFragment_reported_as_spam_and_blocked, Toast.LENGTH_SHORT).show()
onToolbarNavigationClicked()
}
is Result.Failure -> {
Toast.makeText(requireContext(), GroupErrors.getUserDisplayMessage(result.failure), Toast.LENGTH_SHORT).show()
}
}
}
.addTo(lifecycleDisposable)
}
}
)
}
)
}
}
}

Some files were not shown because too many files have changed in this diff Show More