mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-15 15:37:29 +00:00
Compare commits
324 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8004565c84 | ||
|
|
a101dc4fd1 | ||
|
|
57f730d8ee | ||
|
|
3543cc80ba | ||
|
|
71613d9db1 | ||
|
|
4a0e6a3eb2 | ||
|
|
f1a87518e1 | ||
|
|
61f880fd78 | ||
|
|
09904e7a16 | ||
|
|
94658e9090 | ||
|
|
a47448b6c6 | ||
|
|
7e4b9b685a | ||
|
|
64922a8e51 | ||
|
|
f65f4704c9 | ||
|
|
b04ca202f6 | ||
|
|
83086a5a2b | ||
|
|
51a521594f | ||
|
|
0a7a7cf5a9 | ||
|
|
6bd689504c | ||
|
|
efec40ff57 | ||
|
|
69716dde4a | ||
|
|
e90fa05d60 | ||
|
|
580c000bda | ||
|
|
2f3d04d3e8 | ||
|
|
bf37d412e9 | ||
|
|
fd115ebb72 | ||
|
|
b9657208fe | ||
|
|
5d6d78a51e | ||
|
|
916006e664 | ||
|
|
55c69cd50a | ||
|
|
14565b0864 | ||
|
|
a157c1ae1d | ||
|
|
a4d458f969 | ||
|
|
3f53abedab | ||
|
|
68a2d5ed20 | ||
|
|
35e9e31a7b | ||
|
|
444d947743 | ||
|
|
c427dbad08 | ||
|
|
2cefe813e4 | ||
|
|
123ffe42c3 | ||
|
|
da20e66ecd | ||
|
|
901440017a | ||
|
|
0be76a37fe | ||
|
|
36dadc8777 | ||
|
|
182749c101 | ||
|
|
d9228bd911 | ||
|
|
a361fcc8f3 | ||
|
|
ff4f0b9f42 | ||
|
|
060dffc9cc | ||
|
|
172cc302fc | ||
|
|
416e62112f | ||
|
|
e584a90f81 | ||
|
|
9876ffb5e4 | ||
|
|
53e10f2cad | ||
|
|
cb79f75ac1 | ||
|
|
5ec9c1cd90 | ||
|
|
1f28a30ace | ||
|
|
7715917436 | ||
|
|
f79b445fdf | ||
|
|
14484deabe | ||
|
|
3ac395d33e | ||
|
|
f83b520ca9 | ||
|
|
0123f9aa87 | ||
|
|
06b64fe619 | ||
|
|
1bb87834d8 | ||
|
|
ae4167ddae | ||
|
|
383beafdef | ||
|
|
062e88b24f | ||
|
|
8299d49042 | ||
|
|
4677883838 | ||
|
|
7f0a0bef5a | ||
|
|
acc825971b | ||
|
|
62040d06b4 | ||
|
|
0921ebe5f1 | ||
|
|
3d0e15e2b8 | ||
|
|
5372f79c40 | ||
|
|
92e8f9de0e | ||
|
|
c3cf846a10 | ||
|
|
5826b0c068 | ||
|
|
2d7c043398 | ||
|
|
e20d6b63cf | ||
|
|
b85c5eb54a | ||
|
|
a1c8573fad | ||
|
|
90a27d2227 | ||
|
|
c54c6018b2 | ||
|
|
7419570f94 | ||
|
|
8860f792c4 | ||
|
|
e47db0d532 | ||
|
|
ab5d3badc2 | ||
|
|
fce362960f | ||
|
|
5bf23dcfb3 | ||
|
|
65c7dc6ca2 | ||
|
|
e30a8b6954 | ||
|
|
838e318200 | ||
|
|
62ee411901 | ||
|
|
ceefd2d92f | ||
|
|
e3870f5656 | ||
|
|
6e7022ab70 | ||
|
|
031d1551e7 | ||
|
|
6755b25361 | ||
|
|
11d0a73675 | ||
|
|
44119b6437 | ||
|
|
d4a3b442f4 | ||
|
|
aba5774446 | ||
|
|
911dd9efb1 | ||
|
|
f2a490b07e | ||
|
|
5675f080f2 | ||
|
|
f0dbe230b5 | ||
|
|
8b81800052 | ||
|
|
f598c14298 | ||
|
|
58b070e6e3 | ||
|
|
71c92a1c90 | ||
|
|
b86acb9773 | ||
|
|
1b8758b657 | ||
|
|
ed4bab1b8b | ||
|
|
a71fe0fd75 | ||
|
|
3d2a634aac | ||
|
|
01047f0546 | ||
|
|
9dac5691f0 | ||
|
|
3c489ad247 | ||
|
|
7797351341 | ||
|
|
f7212b9916 | ||
|
|
93bb49dc16 | ||
|
|
e504c490c8 | ||
|
|
42e865813c | ||
|
|
fc14d1d464 | ||
|
|
2a1e5e4471 | ||
|
|
da2ee33dff | ||
|
|
f19033a7a2 | ||
|
|
6502ef64ce | ||
|
|
b3ebf778fd | ||
|
|
1dca3698d2 | ||
|
|
2bfe1198d1 | ||
|
|
4f704670b1 | ||
|
|
a1aafd7453 | ||
|
|
4932623937 | ||
|
|
b93568d9c6 | ||
|
|
b3041ab6e0 | ||
|
|
3a151b30ac | ||
|
|
97b3d36433 | ||
|
|
81e3252128 | ||
|
|
426c83c6cc | ||
|
|
b427754a81 | ||
|
|
08f023fb12 | ||
|
|
5f1454aeb8 | ||
|
|
0d254e0724 | ||
|
|
e882e6e111 | ||
|
|
4b0811f9aa | ||
|
|
817f1ee938 | ||
|
|
2d93d74b9f | ||
|
|
93f37ad70f | ||
|
|
3c6bed90db | ||
|
|
fa26fb6b8b | ||
|
|
263ddb0d1e | ||
|
|
8be659c1c8 | ||
|
|
e5c9dddb5a | ||
|
|
6da72aad6d | ||
|
|
5dd5a024c9 | ||
|
|
c0eac5564c | ||
|
|
0d0ee753df | ||
|
|
908f952893 | ||
|
|
1c80e65c5a | ||
|
|
20b13a929b | ||
|
|
4637e1b5d8 | ||
|
|
4b6cb79c75 | ||
|
|
feaf2a33a9 | ||
|
|
4c893a11fc | ||
|
|
f4dd80c929 | ||
|
|
4af078007e | ||
|
|
be297120a1 | ||
|
|
a9741cadbf | ||
|
|
79200c82da | ||
|
|
d9c9ae8dae | ||
|
|
8ee96b40d0 | ||
|
|
67f0f45b67 | ||
|
|
881ab90982 | ||
|
|
6d7e09fec1 | ||
|
|
c274ed6a96 | ||
|
|
53ffca964d | ||
|
|
3da3367291 | ||
|
|
412ee220ce | ||
|
|
a3e3667dc2 | ||
|
|
d5f63da9e4 | ||
|
|
f8d2044356 | ||
|
|
4d2dc61f5d | ||
|
|
5492685df2 | ||
|
|
ad8c6bc579 | ||
|
|
fb08f8ae17 | ||
|
|
7833d7c99a | ||
|
|
335ff61011 | ||
|
|
2029ea378f | ||
|
|
cd7bc63cec | ||
|
|
958331a8ea | ||
|
|
2ba206b9db | ||
|
|
9b90e371f9 | ||
|
|
ff1c298817 | ||
|
|
dfe804dfa0 | ||
|
|
978c6f9349 | ||
|
|
c5c176a818 | ||
|
|
9f2d57493d | ||
|
|
0972d8f1e1 | ||
|
|
cf361334c4 | ||
|
|
c72dd86fed | ||
|
|
b6c653ff77 | ||
|
|
5e3bbb0e64 | ||
|
|
64124f6f4b | ||
|
|
6f6a6826d9 | ||
|
|
57c0b8fd0f | ||
|
|
c54f016213 | ||
|
|
bece58d939 | ||
|
|
36443c59f9 | ||
|
|
02f0301f25 | ||
|
|
334cf669ed | ||
|
|
8442143818 | ||
|
|
b25b8b90e4 | ||
|
|
06aec0b7d7 | ||
|
|
835d7f5ccb | ||
|
|
ffd0b16753 | ||
|
|
b351fb43e6 | ||
|
|
7da47c9586 | ||
|
|
e4755b298f | ||
|
|
4a65487842 | ||
|
|
1466875293 | ||
|
|
fd1e552ad1 | ||
|
|
be3e89ac20 | ||
|
|
b8f1b98c74 | ||
|
|
4bdd07db16 | ||
|
|
511b095647 | ||
|
|
23f4d30e57 | ||
|
|
45c587c5e4 | ||
|
|
115e74d844 | ||
|
|
1475a77260 | ||
|
|
d0e2fbf8e7 | ||
|
|
0f2f0450e3 | ||
|
|
e57f24c062 | ||
|
|
203d7de6a2 | ||
|
|
6e5f2f50fb | ||
|
|
875895524e | ||
|
|
3a21a2a49e | ||
|
|
262b4e7d62 | ||
|
|
c202f97088 | ||
|
|
f96eac96f9 | ||
|
|
c59006e06e | ||
|
|
84e27e7bff | ||
|
|
a3a4b10f83 | ||
|
|
a644c81736 | ||
|
|
27b9fbe490 | ||
|
|
2131c56513 | ||
|
|
95dba15db8 | ||
|
|
c23215604d | ||
|
|
8c9f274d5a | ||
|
|
504a70f3ee | ||
|
|
ad6f51901e | ||
|
|
52ef4c6235 | ||
|
|
9ba4005433 | ||
|
|
3cea3766ab | ||
|
|
ede24e0e73 | ||
|
|
df79bbc5aa | ||
|
|
3c522c677b | ||
|
|
08e86b8c82 | ||
|
|
66c3b1388a | ||
|
|
8992f59c3b | ||
|
|
1d6d27d46c | ||
|
|
625d36fb27 | ||
|
|
665ce14bb6 | ||
|
|
6e4f002b6d | ||
|
|
39a7dbda94 | ||
|
|
7ae8af4153 | ||
|
|
fb817e0c3b | ||
|
|
1eae360470 | ||
|
|
0314db0b58 | ||
|
|
4598387187 | ||
|
|
445c93a756 | ||
|
|
6c168ec575 | ||
|
|
1322f5bc08 | ||
|
|
1c40f2d167 | ||
|
|
18133e2a10 | ||
|
|
e5b0941d30 | ||
|
|
811bef8c35 | ||
|
|
057107ea7a | ||
|
|
273e5f9168 | ||
|
|
35930fb23a | ||
|
|
c794b5c2e7 | ||
|
|
e74d502ae6 | ||
|
|
e5ce6e3e2e | ||
|
|
65020dde1a | ||
|
|
98f432d23c | ||
|
|
2651b789dd | ||
|
|
dbabac34b0 | ||
|
|
6866b7a277 | ||
|
|
03c19f54c2 | ||
|
|
ba510ca77d | ||
|
|
bb7409fd91 | ||
|
|
23e5da4d95 | ||
|
|
fb1b46b67e | ||
|
|
7a21e6b5f8 | ||
|
|
6342a45b4e | ||
|
|
bcc5d485ab | ||
|
|
36fe150678 | ||
|
|
54f92ae466 | ||
|
|
b9b2924939 | ||
|
|
513e5b45c5 | ||
|
|
1fad5e2c1e | ||
|
|
5a28cf616d | ||
|
|
c08199659b | ||
|
|
ca508514a7 | ||
|
|
da2038dd46 | ||
|
|
f02e2d23d0 | ||
|
|
ef1c25c3d3 | ||
|
|
152cc27394 | ||
|
|
c582aca465 | ||
|
|
80e85fb49a | ||
|
|
d660e22e61 | ||
|
|
51856c4f06 | ||
|
|
fd37da42f9 | ||
|
|
11df2bc51f | ||
|
|
6770d21cf7 | ||
|
|
f490d1f6d2 | ||
|
|
f890ae8ddc | ||
|
|
5d5d61d8ed | ||
|
|
75589f1b2d | ||
|
|
6225c676e2 | ||
|
|
9b18668f49 | ||
|
|
2f80e7f1ff |
3
.github/workflows/android.yml
vendored
3
.github/workflows/android.yml
vendored
@@ -24,6 +24,9 @@ jobs:
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
- name: Remove Android S
|
||||
run: $ANDROID_HOME/tools/bin/sdkmanager --uninstall "platforms;android-S"
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew qa
|
||||
|
||||
|
||||
3
.idea/codeStyles/Project.xml
generated
3
.idea/codeStyles/Project.xml
generated
@@ -10,6 +10,9 @@
|
||||
<option name="ALIGN_MULTILINE_TEXT_BLOCKS" value="true" />
|
||||
</JavaCodeStyleSettings>
|
||||
<JetCodeStyleSettings>
|
||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||
<value />
|
||||
</option>
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
|
||||
105
app/build.gradle
105
app/build.gradle
@@ -11,6 +11,8 @@ apply plugin: 'witness'
|
||||
apply plugin: 'org.jlleitschuh.gradle.ktlint'
|
||||
apply from: 'translations.gradle'
|
||||
apply from: 'witness-verifications.gradle'
|
||||
apply plugin: 'org.jetbrains.kotlin.android'
|
||||
apply plugin: 'app.cash.exhaustive'
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
@@ -55,8 +57,8 @@ protobuf {
|
||||
}
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 848
|
||||
def canonicalVersionName = "5.12.0"
|
||||
def canonicalVersionCode = 879
|
||||
def canonicalVersionName = "5.17.1"
|
||||
|
||||
def postFixSize = 100
|
||||
def abiPostFix = ['universal' : 0,
|
||||
@@ -75,6 +77,7 @@ android {
|
||||
useLibrary 'org.apache.http.legacy'
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
freeCompilerArgs = ["-Xallow-result-return-type"]
|
||||
}
|
||||
|
||||
@@ -115,6 +118,8 @@ android {
|
||||
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\""
|
||||
buildConfigField "String[]", "SIGNAL_SFU_INTERNAL_NAMES", "new String[]{\"Test\", \"Staging\"}"
|
||||
buildConfigField "String[]", "SIGNAL_SFU_INTERNAL_URLS", "new String[]{\"https://sfu.test.voip.signal.org\", \"https://sfu.staging.voip.signal.org\"}"
|
||||
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
|
||||
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
||||
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
|
||||
@@ -132,6 +137,10 @@ android {
|
||||
buildConfigField "String", "GIPHY_API_KEY", "\"3o6ZsYH6U6Eri53TXy\""
|
||||
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/challenge/generate.html\""
|
||||
|
||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"unset\""
|
||||
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"unset\""
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"unset\""
|
||||
|
||||
ndk {
|
||||
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
}
|
||||
@@ -196,28 +205,34 @@ android {
|
||||
'proguard/proguard.cfg'
|
||||
testProguardFiles 'proguard/proguard-automation.pro',
|
||||
'proguard/proguard.cfg'
|
||||
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Debug\""
|
||||
}
|
||||
flipper {
|
||||
initWith debug
|
||||
isDefault false
|
||||
minifyEnabled false
|
||||
matchingFallbacks = ['debug']
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Flipper\""
|
||||
}
|
||||
release {
|
||||
minifyEnabled true
|
||||
proguardFiles = buildTypes.debug.proguardFiles
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Release\""
|
||||
}
|
||||
perf {
|
||||
initWith debug
|
||||
isDefault false
|
||||
debuggable false
|
||||
matchingFallbacks = ['debug']
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Perf\""
|
||||
}
|
||||
mock {
|
||||
initWith debug
|
||||
isDefault false
|
||||
minifyEnabled false
|
||||
matchingFallbacks = ['debug']
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Mock\""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,6 +243,7 @@ android {
|
||||
ext.websiteUpdateUrl = "null"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"play\""
|
||||
}
|
||||
|
||||
website {
|
||||
@@ -235,6 +251,7 @@ android {
|
||||
ext.websiteUpdateUrl = "https://updates.signal.org/android"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
|
||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"website\""
|
||||
}
|
||||
|
||||
internal {
|
||||
@@ -242,6 +259,16 @@ android {
|
||||
ext.websiteUpdateUrl = "null"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"internal\""
|
||||
}
|
||||
|
||||
nightly {
|
||||
dimension 'distribution'
|
||||
versionNameSuffix "-nightly-untagged-${getDateSuffix()}"
|
||||
ext.websiteUpdateUrl = "null"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"internal\""
|
||||
}
|
||||
|
||||
study {
|
||||
@@ -251,6 +278,7 @@ android {
|
||||
ext.websiteUpdateUrl = "null"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"study\""
|
||||
}
|
||||
|
||||
prod {
|
||||
@@ -259,6 +287,7 @@ android {
|
||||
isDefault true
|
||||
|
||||
buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"mainnet\""
|
||||
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"Prod\""
|
||||
}
|
||||
|
||||
staging {
|
||||
@@ -281,18 +310,28 @@ android {
|
||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdls=\""
|
||||
buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\""
|
||||
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/staging/challenge/generate.html\""
|
||||
|
||||
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"Staging\""
|
||||
}
|
||||
}
|
||||
|
||||
android.applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
output.outputFileName = output.outputFileName.replace(".apk", "-${variant.versionName}.apk")
|
||||
def abiName = output.getFilter("ABI") ?: 'universal'
|
||||
def postFix = abiPostFix.get(abiName, 0)
|
||||
if (output.baseName.contains('nightly')) {
|
||||
output.versionCodeOverride = canonicalVersionCode * postFixSize + 5
|
||||
def tag = getCurrentGitTag()
|
||||
if (tag != null && tag.length() > 0) {
|
||||
output.versionNameOverride = tag
|
||||
}
|
||||
} else {
|
||||
output.outputFileName = output.outputFileName.replace(".apk", "-${variant.versionName}.apk")
|
||||
def abiName = output.getFilter("ABI") ?: 'universal'
|
||||
def postFix = abiPostFix.get(abiName, 0)
|
||||
|
||||
if (postFix >= postFixSize) throw new AssertionError("postFix is too large")
|
||||
if (postFix >= postFixSize) throw new AssertionError("postFix is too large")
|
||||
|
||||
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
|
||||
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,6 +344,12 @@ android {
|
||||
variant.setIgnore(true)
|
||||
} else if (distribution != 'study' && buildType == 'mock') {
|
||||
variant.setIgnore(true)
|
||||
} else if (distribution == 'internal' && buildType != 'flipper' && buildType != 'perf' && buildType != 'release') {
|
||||
variant.setIgnore(true)
|
||||
} else if (distribution == 'nightly' && environment != 'prod') {
|
||||
variant.setIgnore(true)
|
||||
} else if (distribution == 'nightly' && buildType != 'flipper' && buildType != 'perf' && buildType != 'release') {
|
||||
variant.setIgnore(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,6 +367,8 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.core:core-ktx:1.5.0'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
||||
lintChecks project(':lintchecks')
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
@@ -351,8 +398,9 @@ dependencies {
|
||||
implementation "androidx.concurrent:concurrent-futures:1.0.0"
|
||||
implementation "androidx.autofill:autofill:1.0.0"
|
||||
implementation "androidx.biometric:biometric:1.1.0"
|
||||
implementation "androidx.sharetarget:sharetarget:1.1.0"
|
||||
|
||||
implementation ('com.google.firebase:firebase-messaging:20.2.0') {
|
||||
implementation ('com.google.firebase:firebase-messaging:22.0.0') {
|
||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
||||
@@ -375,16 +423,16 @@ dependencies {
|
||||
implementation project(':device-transfer')
|
||||
|
||||
implementation 'org.signal:zkgroup-android:0.7.0'
|
||||
implementation 'org.whispersystems:signal-client-android:0.5.1'
|
||||
implementation 'org.whispersystems:signal-client-android:0.8.3'
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.10.0'
|
||||
|
||||
implementation('com.mobilecoin:android-sdk:1.0.0') {
|
||||
implementation('com.mobilecoin:android-sdk:1.1.0') {
|
||||
exclude group: 'com.google.protobuf'
|
||||
}
|
||||
|
||||
implementation 'org.signal:argon2:13.1@aar'
|
||||
|
||||
implementation 'org.signal:ringrtc-android:2.9.6'
|
||||
implementation 'org.signal:ringrtc-android:2.10.6'
|
||||
|
||||
implementation "me.leolin:ShortcutBadger:1.1.22"
|
||||
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
||||
@@ -436,8 +484,8 @@ dependencies {
|
||||
}
|
||||
implementation 'dnsjava:dnsjava:2.1.9'
|
||||
|
||||
flipperImplementation 'com.facebook.flipper:flipper:0.32.2'
|
||||
flipperImplementation 'com.facebook.soloader:soloader:0.8.2'
|
||||
flipperImplementation 'com.facebook.flipper:flipper:0.91.0'
|
||||
flipperImplementation 'com.facebook.soloader:soloader:0.10.1'
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.assertj:assertj-core:3.11.1'
|
||||
@@ -454,11 +502,16 @@ dependencies {
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
||||
testImplementation 'org.hamcrest:hamcrest:2.2'
|
||||
|
||||
testImplementation(testFixtures(project(":libsignal-service")))
|
||||
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.12.0"
|
||||
|
||||
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
|
||||
implementation 'io.reactivex.rxjava3:rxkotlin:3.0.1'
|
||||
}
|
||||
|
||||
dependencyVerification {
|
||||
@@ -555,6 +608,26 @@ def getGitHash() {
|
||||
return stdout.toString().trim()
|
||||
}
|
||||
|
||||
def getCurrentGitTag() {
|
||||
if (!(new File('.git').exists())) {
|
||||
return ''
|
||||
}
|
||||
|
||||
def stdout = new ByteArrayOutputStream()
|
||||
exec {
|
||||
commandLine 'git', 'tag', '--points-at', 'HEAD'
|
||||
standardOutput = stdout
|
||||
}
|
||||
|
||||
def output = stdout.toString().trim()
|
||||
|
||||
if (output != null && output.size() > 0) {
|
||||
return output.split('\n')[0];
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(Test) {
|
||||
testLogging {
|
||||
events "failed"
|
||||
@@ -575,3 +648,9 @@ def loadKeystoreProperties(filename) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
def getDateSuffix() {
|
||||
def date = new Date()
|
||||
def formattedDate = date.format('yyyy-MM-dd-HH:mm')
|
||||
return formattedDate
|
||||
}
|
||||
|
||||
@@ -107,6 +107,9 @@
|
||||
<meta-data android:name="com.google.android.gms.version"
|
||||
android:value="@integer/google_play_services_version" />
|
||||
|
||||
<meta-data android:name="com.google.android.gms.car.application"
|
||||
android:resource="@xml/automotive_app_desc" />
|
||||
|
||||
<meta-data android:name="firebase_analytics_collection_deactivated" android:value="true" />
|
||||
<meta-data android:name="google_analytics_adid_collection_enabled" android:value="false" />
|
||||
<meta-data android:name="firebase_messaging_auto_init_enabled" android:value="false" />
|
||||
@@ -150,6 +153,14 @@
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="tsdevice"/>
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="sgnl"
|
||||
android:host="linkdevice"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".preferences.MmsPreferencesActivity"
|
||||
@@ -187,7 +198,7 @@
|
||||
|
||||
<meta-data
|
||||
android:name="android.service.chooser.chooser_target_service"
|
||||
android:value=".service.DirectShareService" />
|
||||
android:value="androidx.sharetarget.ChooserTargetServiceCompat" />
|
||||
|
||||
</activity>
|
||||
|
||||
@@ -297,14 +308,6 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.managegroup.ManageGroupActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".recipients.ui.managerecipient.ManageRecipientActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".recipients.ui.disappearingmessages.RecipientDisappearingMessagesActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
@@ -316,7 +319,7 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".migrations.ApplicationMigrationActivity"
|
||||
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
@@ -370,6 +373,13 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".components.settings.conversation.ConversationSettingsActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/Signal.DayNight.ConversationSettings"
|
||||
android:windowSoftInputMode="stateAlwaysHidden">
|
||||
</activity>
|
||||
|
||||
|
||||
<activity android:name=".wallpaper.ChatWallpaperActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:windowSoftInputMode="stateAlwaysHidden">
|
||||
@@ -489,7 +499,7 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".scribbles.ImageEditorStickerSelectActivity"
|
||||
android:theme="@style/TextSecure.DarkTheme"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".profiles.edit.EditProfileActivity"
|
||||
@@ -592,6 +602,10 @@
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.Signal.WallpaperCropper" />
|
||||
|
||||
<activity android:name=".reactions.edit.EditReactionsActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<service android:enabled="true" android:name=".service.webrtc.WebRtcCallService"/>
|
||||
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
|
||||
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
|
||||
@@ -637,13 +651,6 @@
|
||||
<meta-data android:name="android.provider.CONTACTS_STRUCTURE" android:resource="@xml/contactsformat" />
|
||||
</service>
|
||||
|
||||
<service android:name=".service.DirectShareService"
|
||||
android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.chooser.ChooserTargetService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service android:name=".service.GenericForegroundService"/>
|
||||
|
||||
<service android:name=".gcm.FcmFetchService" />
|
||||
@@ -703,24 +710,12 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".notifications.AndroidAutoHeardReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="org.thoughtcrime.securesms.notifications.ANDROID_AUTO_HEARD"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".notifications.AndroidAutoReplyReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="org.thoughtcrime.securesms.notifications.ANDROID_AUTO_REPLY"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".service.ExpirationListener" />
|
||||
|
||||
<receiver android:name=".revealable.ViewOnceMessageManager$ViewOnceAlarm" />
|
||||
|
||||
<receiver android:name=".service.PendingRetryReceiptManager$PendingRetryReceiptAlarm" />
|
||||
|
||||
<receiver android:name=".service.TrimThreadsByDateManager$TrimThreadsByDateAlarm" />
|
||||
|
||||
<receiver android:name=".payments.backup.phrase.ClearClipboardAlarmReceiver" />
|
||||
|
||||
@@ -512,7 +512,12 @@ final class SignalCameraXModule {
|
||||
return rotationDegrees;
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeExperimentalUsageError")
|
||||
public void invalidateView() {
|
||||
if (mPreview != null) {
|
||||
mPreview.setTargetRotation(getDisplaySurfaceRotation()); // Fixes issue #10940 (rotation not updated on phones using "Legacy API")
|
||||
}
|
||||
|
||||
updateViewInfo();
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,6 @@ public final class AppCapabilities {
|
||||
* asking if the user has set a Signal PIN or not.
|
||||
*/
|
||||
public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
|
||||
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION);
|
||||
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, FeatureFlags.senderKey());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.insights.InsightsOptOut;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
||||
@@ -60,6 +61,7 @@ public final class AppInitialization {
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
|
||||
EmojiSearchIndexDownloadJob.scheduleImmediately();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,6 +27,8 @@ import androidx.multidex.MultiDexApplication;
|
||||
|
||||
import com.google.android.gms.security.ProviderInstaller;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.conscrypt.Conscrypt;
|
||||
import org.signal.aesgcmprovider.AesGcmProvider;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
@@ -37,6 +39,7 @@ import org.signal.core.util.tracing.Tracer;
|
||||
import org.signal.glide.SignalGlideCodecs;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.SqlCipherLibraryLoader;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
|
||||
@@ -44,6 +47,7 @@ import org.thoughtcrime.securesms.emoji.EmojiSource;
|
||||
import org.thoughtcrime.securesms.gcm.FcmJobService;
|
||||
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
|
||||
import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob;
|
||||
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
@@ -70,6 +74,7 @@ import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.AppForegroundObserver;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
import org.thoughtcrime.securesms.util.ByteUnit;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
|
||||
@@ -84,6 +89,9 @@ import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider;
|
||||
import java.security.Security;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
/**
|
||||
* Will be called once when the TextSecure process is created.
|
||||
*
|
||||
@@ -117,11 +125,15 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
|
||||
AppStartup.getInstance().addBlocking("security-provider", this::initializeSecurityProvider)
|
||||
.addBlocking("logging", () -> {
|
||||
initializeLogging();
|
||||
Log.i(TAG, "onCreate()");
|
||||
initializeLogging();
|
||||
Log.i(TAG, "onCreate()");
|
||||
})
|
||||
.addBlocking("crash-handling", this::initializeCrashHandling)
|
||||
.addBlocking("eat-db", () -> DatabaseFactory.getInstance(this))
|
||||
.addBlocking("sqlcipher-init", () -> SqlCipherLibraryLoader.load(this))
|
||||
.addBlocking("rx-init", () -> {
|
||||
RxJavaPlugins.setInitIoSchedulerHandler(schedulerSupplier -> Schedulers.from(SignalExecutors.BOUNDED_IO, true, false));
|
||||
RxJavaPlugins.setInitComputationSchedulerHandler(schedulerSupplier -> Schedulers.from(SignalExecutors.BOUNDED, true, false));
|
||||
})
|
||||
.addBlocking("app-dependencies", this::initializeAppDependencies)
|
||||
.addBlocking("notification-channels", () -> NotificationChannels.create(this))
|
||||
.addBlocking("first-launch", this::initializeFirstEverAppLaunch)
|
||||
@@ -145,6 +157,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addBlocking("blob-provider", this::initializeBlobProvider)
|
||||
.addBlocking("feature-flags", FeatureFlags::init)
|
||||
.addNonBlocking(this::initializeRevealableMessageManager)
|
||||
.addNonBlocking(this::initializePendingRetryReceiptManager)
|
||||
.addNonBlocking(this::initializeGcmCheck)
|
||||
.addNonBlocking(this::initializeSignedPreKeyCheck)
|
||||
.addNonBlocking(this::initializePeriodicTasks)
|
||||
@@ -156,10 +169,12 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addNonBlocking(StorageSyncHelper::scheduleRoutineSync)
|
||||
.addNonBlocking(() -> ApplicationDependencies.getJobManager().beginJobLoop())
|
||||
.addNonBlocking(EmojiSource::refresh)
|
||||
.addNonBlocking(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
|
||||
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
|
||||
.addPostRender(this::initializeExpiringMessageManager)
|
||||
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
|
||||
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
|
||||
.addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
|
||||
.addPostRender(() -> DatabaseFactory.getMessageLogDatabase(this).trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
|
||||
.execute();
|
||||
|
||||
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
@@ -233,7 +248,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
}
|
||||
|
||||
private void initializeLogging() {
|
||||
persistentLogger = new PersistentLogger(this, LogSecretProvider.getOrCreateAttachmentSecret(this), BuildConfig.VERSION_NAME);
|
||||
persistentLogger = new PersistentLogger(this, LogSecretProvider.getOrCreateAttachmentSecret(this), BuildConfig.VERSION_NAME, FeatureFlags.internalUser() ? 15 : 7, ByteUnit.KILOBYTES.toBytes(300));
|
||||
org.signal.core.util.logging.Log.initialize(FeatureFlags::internalUser, new AndroidLogger(), persistentLogger);
|
||||
|
||||
SignalProtocolLoggerProvider.setProvider(new CustomSignalProtocolLogger());
|
||||
@@ -298,6 +313,10 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
ApplicationDependencies.getViewOnceMessageManager().scheduleIfNecessary();
|
||||
}
|
||||
|
||||
private void initializePendingRetryReceiptManager() {
|
||||
ApplicationDependencies.getPendingRetryReceiptManager().scheduleIfNecessary();
|
||||
}
|
||||
|
||||
private void initializePeriodicTasks() {
|
||||
RotateSignedPreKeyListener.schedule(this);
|
||||
DirectoryRefreshListener.schedule(this);
|
||||
|
||||
@@ -11,6 +11,8 @@ import androidx.lifecycle.Observer;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizable;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
||||
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
@@ -29,7 +31,7 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
public interface BindableConversationItem extends Unbindable, GiphyMp4Playable {
|
||||
public interface BindableConversationItem extends Unbindable, GiphyMp4Playable, Colorizable {
|
||||
void bind(@NonNull LifecycleOwner lifecycleOwner,
|
||||
@NonNull ConversationMessage messageRecord,
|
||||
@NonNull Optional<MessageRecord> previousMessageRecord,
|
||||
@@ -43,12 +45,17 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable {
|
||||
boolean hasWallpaper,
|
||||
boolean isMessageRequestAccepted,
|
||||
@NonNull AttachmentMediaSourceFactory attachmentMediaSourceFactory,
|
||||
boolean canPlayInline);
|
||||
boolean canPlayInline,
|
||||
@NonNull Colorizer colorizer);
|
||||
|
||||
ConversationMessage getConversationMessage();
|
||||
|
||||
void setEventListener(@Nullable EventListener listener);
|
||||
|
||||
default void updateTimestamps() {
|
||||
// Intentionally Blank.
|
||||
}
|
||||
|
||||
interface EventListener {
|
||||
void onQuoteClicked(MmsMessageRecord messageRecord);
|
||||
void onLinkPreviewClicked(@NonNull LinkPreview linkPreview);
|
||||
@@ -63,13 +70,16 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable {
|
||||
void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
|
||||
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
||||
void onMessageWithRecaptchaNeededClicked(@NonNull MessageRecord messageRecord);
|
||||
void onIncomingIdentityMismatchClicked(@NonNull RecipientId recipientId);
|
||||
void onRegisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
|
||||
void onUnregisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
|
||||
void onVoiceNotePause(@NonNull Uri uri);
|
||||
void onVoiceNotePlay(@NonNull Uri uri, long messageId, double position);
|
||||
void onVoiceNoteSeekTo(@NonNull Uri uri, double position);
|
||||
void onVoiceNotePlaybackSpeedChanged(@NonNull Uri uri, float speed);
|
||||
void onGroupMigrationLearnMoreClicked(@NonNull GroupMigrationMembershipChange membershipChange);
|
||||
void onDecryptionFailedLearnMoreClicked();
|
||||
void onChatSessionRefreshLearnMoreClicked();
|
||||
void onBadDecryptLearnMoreClicked(@NonNull RecipientId author);
|
||||
void onSafetyNumberLearnMoreClicked(@NonNull Recipient recipient);
|
||||
void onJoinGroupCallClicked();
|
||||
void onInviteFriendsToGroupClicked(@NonNull GroupId.V2 groupId);
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.AsyncTask;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSessionLock;
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
import org.thoughtcrime.securesms.database.PushDatabase;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.PushDecryptMessageJob;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.VerifySpan;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class ConfirmIdentityDialog extends AlertDialog {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = Log.tag(ConfirmIdentityDialog.class);
|
||||
|
||||
private OnClickListener callback;
|
||||
|
||||
public ConfirmIdentityDialog(Context context,
|
||||
MessageRecord messageRecord,
|
||||
IdentityKeyMismatch mismatch)
|
||||
{
|
||||
super(context);
|
||||
|
||||
Recipient recipient = Recipient.resolved(mismatch.getRecipientId(context));
|
||||
String name = recipient.getDisplayName(context);
|
||||
String introduction = context.getString(R.string.ConfirmIdentityDialog_your_safety_number_with_s_has_changed, name, name);
|
||||
SpannableString spannableString = new SpannableString(introduction + " " +
|
||||
context.getString(R.string.ConfirmIdentityDialog_you_may_wish_to_verify_your_safety_number_with_this_contact));
|
||||
|
||||
spannableString.setSpan(new VerifySpan(context, mismatch),
|
||||
introduction.length()+1, spannableString.length(),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
setTitle(name);
|
||||
setMessage(spannableString);
|
||||
|
||||
setButton(AlertDialog.BUTTON_POSITIVE, context.getString(R.string.ConfirmIdentityDialog_accept), new AcceptListener(messageRecord, mismatch, recipient.getId()));
|
||||
setButton(AlertDialog.BUTTON_NEGATIVE, context.getString(android.R.string.cancel), new CancelListener());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show() {
|
||||
super.show();
|
||||
((TextView)this.findViewById(android.R.id.message))
|
||||
.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
|
||||
public void setCallback(OnClickListener callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
private class AcceptListener implements OnClickListener {
|
||||
|
||||
private final MessageRecord messageRecord;
|
||||
private final IdentityKeyMismatch mismatch;
|
||||
private final RecipientId recipientId;
|
||||
|
||||
private AcceptListener(MessageRecord messageRecord, IdentityKeyMismatch mismatch, RecipientId recipientId) {
|
||||
this.messageRecord = messageRecord;
|
||||
this.mismatch = mismatch;
|
||||
this.recipientId = recipientId;
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
new AsyncTask<Void, Void, Void>()
|
||||
{
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) {
|
||||
SignalProtocolAddress mismatchAddress = new SignalProtocolAddress(Recipient.resolved(recipientId).requireServiceId(), 1);
|
||||
TextSecureIdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(getContext());
|
||||
|
||||
identityKeyStore.saveIdentity(mismatchAddress, mismatch.getIdentityKey(), true);
|
||||
}
|
||||
|
||||
processMessageRecord(messageRecord);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void processMessageRecord(MessageRecord messageRecord) {
|
||||
if (messageRecord.isOutgoing()) processOutgoingMessageRecord(messageRecord);
|
||||
else processIncomingMessageRecord(messageRecord);
|
||||
}
|
||||
|
||||
private void processOutgoingMessageRecord(MessageRecord messageRecord) {
|
||||
MessageDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
|
||||
MessageDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(getContext());
|
||||
|
||||
if (messageRecord.isMms()) {
|
||||
mmsDatabase.removeMismatchedIdentity(messageRecord.getId(),
|
||||
mismatch.getRecipientId(getContext()),
|
||||
mismatch.getIdentityKey());
|
||||
|
||||
if (messageRecord.getRecipient().isPushGroup()) {
|
||||
MessageSender.resendGroupMessage(getContext(), messageRecord, Recipient.resolved(mismatch.getRecipientId(getContext())).getId());
|
||||
} else {
|
||||
MessageSender.resend(getContext(), messageRecord);
|
||||
}
|
||||
} else {
|
||||
smsDatabase.removeMismatchedIdentity(messageRecord.getId(),
|
||||
mismatch.getRecipientId(getContext()),
|
||||
mismatch.getIdentityKey());
|
||||
|
||||
MessageSender.resend(getContext(), messageRecord);
|
||||
}
|
||||
}
|
||||
|
||||
private void processIncomingMessageRecord(MessageRecord messageRecord) {
|
||||
try {
|
||||
MessageDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
|
||||
|
||||
smsDatabase.removeMismatchedIdentity(messageRecord.getId(),
|
||||
mismatch.getRecipientId(getContext()),
|
||||
mismatch.getIdentityKey());
|
||||
|
||||
boolean legacy = !messageRecord.isContentBundleKeyExchange();
|
||||
|
||||
SignalServiceEnvelope envelope = new SignalServiceEnvelope(SignalServiceProtos.Envelope.Type.PREKEY_BUNDLE_VALUE,
|
||||
Optional.of(RecipientUtil.toSignalServiceAddress(getContext(), messageRecord.getIndividualRecipient())),
|
||||
messageRecord.getRecipientDeviceId(),
|
||||
messageRecord.getDateSent(),
|
||||
legacy ? Base64.decode(messageRecord.getBody()) : null,
|
||||
!legacy ? Base64.decode(messageRecord.getBody()) : null,
|
||||
0,
|
||||
0,
|
||||
null);
|
||||
|
||||
ApplicationDependencies.getJobManager().add(new PushDecryptMessageJob(getContext(), envelope, messageRecord.getId()));
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
|
||||
if (callback != null) callback.onClick(null, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private class CancelListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
if (callback != null) callback.onClick(null, 0);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,13 +20,13 @@ import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterToolbar;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
@@ -56,7 +56,8 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
||||
|
||||
protected ContactSelectionListFragment contactsFragment;
|
||||
|
||||
private ContactFilterToolbar toolbar;
|
||||
private Toolbar toolbar;
|
||||
private ContactFilterView contactFilterView;
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
@@ -73,6 +74,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
||||
|
||||
setContentView(getIntent().getIntExtra(EXTRA_LAYOUT_RES_ID, R.layout.contact_selection_activity));
|
||||
|
||||
initializeContactFilterView();
|
||||
initializeToolbar();
|
||||
initializeResources();
|
||||
initializeSearch();
|
||||
@@ -84,16 +86,23 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
protected ContactFilterToolbar getToolbar() {
|
||||
protected Toolbar getToolbar() {
|
||||
return toolbar;
|
||||
}
|
||||
|
||||
protected ContactFilterView getContactFilterView() {
|
||||
return contactFilterView;
|
||||
}
|
||||
|
||||
private void initializeContactFilterView() {
|
||||
this.contactFilterView = findViewById(R.id.contact_filter_edit_text);
|
||||
}
|
||||
|
||||
private void initializeToolbar() {
|
||||
this.toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
||||
getSupportActionBar().setDisplayShowTitleEnabled(false);
|
||||
getSupportActionBar().setIcon(null);
|
||||
getSupportActionBar().setLogo(null);
|
||||
}
|
||||
@@ -104,7 +113,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
||||
}
|
||||
|
||||
private void initializeSearch() {
|
||||
toolbar.setOnFilterChangedListener(filter -> contactsFragment.setQueryFilter(filter));
|
||||
contactFilterView.setOnFilterChangedListener(filter -> contactsFragment.setQueryFilter(filter));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -155,7 +164,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
||||
ContactSelectionActivity activity = this.activity.get();
|
||||
|
||||
if (activity != null && !activity.isFinishing()) {
|
||||
activity.toolbar.clear();
|
||||
activity.contactFilterView.clear();
|
||||
activity.contactsFragment.resetQueryFilter();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,13 +57,14 @@ import com.pnikosis.materialishprogress.ProgressWheel;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
|
||||
import org.thoughtcrime.securesms.components.emoji.WarningTextView;
|
||||
import org.thoughtcrime.securesms.components.recyclerview.ToolbarShadowAnimationHelper;
|
||||
import org.thoughtcrime.securesms.contacts.AbstractContactsCursorLoader;
|
||||
import org.thoughtcrime.securesms.contacts.ContactChip;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.LetterHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
||||
@@ -74,7 +75,6 @@ import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
@@ -114,6 +114,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
public static final String HIDE_COUNT = "hide_count";
|
||||
public static final String CAN_SELECT_SELF = "can_select_self";
|
||||
public static final String DISPLAY_CHIPS = "display_chips";
|
||||
public static final String RV_PADDING_BOTTOM = "recycler_view_padding_bottom";
|
||||
public static final String RV_CLIP = "recycler_view_clipping";
|
||||
|
||||
private ConstraintLayout constraintLayout;
|
||||
private TextView emptyText;
|
||||
@@ -129,9 +131,10 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
|
||||
private ChipGroup chipGroup;
|
||||
private HorizontalScrollView chipGroupScrollContainer;
|
||||
private WarningTextView groupLimit;
|
||||
private OnSelectionLimitReachedListener onSelectionLimitReachedListener;
|
||||
private AbstractContactsCursorLoaderFactoryProvider cursorFactoryProvider;
|
||||
private View shadowView;
|
||||
private ToolbarShadowAnimationHelper toolbarShadowAnimationHelper;
|
||||
|
||||
|
||||
@Nullable private FixedViewsAdapter headerAdapter;
|
||||
@@ -231,9 +234,12 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
showContactsProgress = view.findViewById(R.id.progress);
|
||||
chipGroup = view.findViewById(R.id.chipGroup);
|
||||
chipGroupScrollContainer = view.findViewById(R.id.chipGroupScrollContainer);
|
||||
groupLimit = view.findViewById(R.id.group_limit);
|
||||
constraintLayout = view.findViewById(R.id.container);
|
||||
shadowView = view.findViewById(R.id.toolbar_shadow);
|
||||
|
||||
toolbarShadowAnimationHelper = new ToolbarShadowAnimationHelper(shadowView);
|
||||
|
||||
recyclerView.addOnScrollListener(toolbarShadowAnimationHelper);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
recyclerView.setItemAnimator(new DefaultItemAnimator() {
|
||||
@Override
|
||||
@@ -245,6 +251,15 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
Intent intent = requireActivity().getIntent();
|
||||
Bundle arguments = safeArguments();
|
||||
|
||||
int recyclerViewPadBottom = arguments.getInt(RV_PADDING_BOTTOM, intent.getIntExtra(RV_PADDING_BOTTOM, -1));
|
||||
boolean recyclerViewClipping = arguments.getBoolean(RV_CLIP, intent.getBooleanExtra(RV_CLIP, true));
|
||||
|
||||
if (recyclerViewPadBottom != -1) {
|
||||
ViewUtil.setPaddingBottom(recyclerView, recyclerViewPadBottom);
|
||||
}
|
||||
|
||||
recyclerView.setClipToPadding(recyclerViewClipping);
|
||||
|
||||
swipeRefresh.setEnabled(arguments.getBoolean(REFRESHABLE, intent.getBooleanExtra(REFRESHABLE, true)));
|
||||
|
||||
hideCount = arguments.getBoolean(HIDE_COUNT, intent.getBooleanExtra(HIDE_COUNT, false));
|
||||
@@ -261,8 +276,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
|
||||
currentSelection = getCurrentSelection();
|
||||
|
||||
updateGroupLimit(getChipCount());
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@@ -270,13 +283,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
return getArguments() != null ? getArguments() : new Bundle();
|
||||
}
|
||||
|
||||
private void updateGroupLimit(int chipCount) {
|
||||
int members = currentSelection.size() + chipCount;
|
||||
groupLimit.setText(getResources().getQuantityString(R.plurals.ContactSelectionListFragment_d_members, members, members));
|
||||
groupLimit.setVisibility(isMulti && !hideCount ? View.VISIBLE : View.GONE);
|
||||
groupLimit.setWarning(selectionWarningLimitExceeded());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
@@ -298,6 +304,14 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
return cursorRecyclerViewAdapter.getSelectedContactsCount();
|
||||
}
|
||||
|
||||
public int getTotalMemberCount() {
|
||||
if (cursorRecyclerViewAdapter == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return cursorRecyclerViewAdapter.getSelectedContactsCount() + cursorRecyclerViewAdapter.getCurrentContactsCount();
|
||||
}
|
||||
|
||||
private Set<RecipientId> getCurrentSelection() {
|
||||
List<RecipientId> currentSelection = safeArguments().getParcelableArrayList(CURRENT_SELECTION);
|
||||
if (currentSelection == null) {
|
||||
@@ -338,8 +352,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
concatenateAdapter.addAdapter(footerAdapter);
|
||||
}
|
||||
|
||||
recyclerView.addItemDecoration(new LetterHeaderDecoration(requireContext(), this::hideLetterHeaders));
|
||||
recyclerView.setAdapter(concatenateAdapter);
|
||||
recyclerView.addItemDecoration(new StickyHeaderDecoration(concatenateAdapter, true, true, 0));
|
||||
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
|
||||
@@ -350,6 +364,14 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onSelectionChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hideLetterHeaders() {
|
||||
return hasQueryFilter() || shouldDisplayRecents();
|
||||
}
|
||||
|
||||
private View createInviteActionView(@NonNull ListCallback listCallback) {
|
||||
@@ -418,7 +440,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
FragmentActivity activity = requireActivity();
|
||||
int displayMode = safeArguments().getInt(DISPLAY_MODE, activity.getIntent().getIntExtra(DISPLAY_MODE, DisplayMode.FLAG_ALL));
|
||||
boolean displayRecents = safeArguments().getBoolean(RECENTS, activity.getIntent().getBooleanExtra(RECENTS, false));
|
||||
boolean displayRecents = shouldDisplayRecents();
|
||||
|
||||
if (cursorFactoryProvider != null) {
|
||||
return cursorFactoryProvider.get().create();
|
||||
@@ -464,6 +486,10 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
fastScroller.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private boolean shouldDisplayRecents() {
|
||||
return safeArguments().getBoolean(RECENTS, requireActivity().getIntent().getBooleanExtra(RECENTS, false));
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void handleContactPermissionGranted() {
|
||||
final Context context = requireContext();
|
||||
@@ -595,12 +621,19 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
if (isMulti) {
|
||||
addChipForSelectedContact(selectedContact);
|
||||
}
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onSelectionChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void markContactUnselected(@NonNull SelectedContact selectedContact) {
|
||||
cursorRecyclerViewAdapter.removeFromSelectedContacts(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
removeChipForContact(selectedContact);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onSelectionChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void removeChipForContact(@NonNull SelectedContact contact) {
|
||||
@@ -611,8 +644,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
}
|
||||
}
|
||||
|
||||
updateGroupLimit(getChipCount());
|
||||
|
||||
if (getChipCount() == 0) {
|
||||
setChipGroupVisibility(ConstraintSet.GONE);
|
||||
}
|
||||
@@ -662,7 +693,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
|
||||
private void addChip(@NonNull ContactChip chip) {
|
||||
chipGroup.addView(chip);
|
||||
updateGroupLimit(getChipCount());
|
||||
if (selectionWarningLimitReachedExactly()) {
|
||||
if (onSelectionLimitReachedListener != null) {
|
||||
onSelectionLimitReachedListener.onSuggestedLimitReached(selectionLimit.getRecommendedLimit());
|
||||
@@ -715,6 +745,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
/** @return True if the contact is allowed to be selected, otherwise false. */
|
||||
boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number);
|
||||
void onContactDeselected(Optional<RecipientId> recipientId, String number);
|
||||
void onSelectionChanged();
|
||||
}
|
||||
|
||||
public interface OnSelectionLimitReachedListener {
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import cn.carbswang.android.numberpickerview.library.NumberPickerView;
|
||||
|
||||
public class ExpirationDialog extends AlertDialog {
|
||||
|
||||
protected ExpirationDialog(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
protected ExpirationDialog(Context context, int theme) {
|
||||
super(context, theme);
|
||||
}
|
||||
|
||||
protected ExpirationDialog(Context context, boolean cancelable, OnCancelListener cancelListener) {
|
||||
super(context, cancelable, cancelListener);
|
||||
}
|
||||
|
||||
public static void show(final Context context,
|
||||
final int currentExpiration,
|
||||
final @NonNull OnClickListener listener)
|
||||
{
|
||||
final View view = createNumberPickerView(context, currentExpiration);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(context.getString(R.string.ExpirationDialog_disappearing_messages));
|
||||
builder.setView(view);
|
||||
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
int selected = ((NumberPickerView)view.findViewById(R.id.expiration_number_picker)).getValue();
|
||||
listener.onClick(getExpirationTimes(context, currentExpiration)[selected]);
|
||||
});
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private static View createNumberPickerView(final Context context, final int currentExpiration) {
|
||||
final LayoutInflater inflater = LayoutInflater.from(context);
|
||||
final View view = inflater.inflate(R.layout.expiration_dialog, null);
|
||||
final NumberPickerView numberPickerView = view.findViewById(R.id.expiration_number_picker);
|
||||
final TextView textView = view.findViewById(R.id.expiration_details);
|
||||
final int[] expirationTimes = getExpirationTimes(context, currentExpiration);
|
||||
final String[] expirationDisplayValues = new String[expirationTimes.length];
|
||||
|
||||
int selectedIndex = expirationTimes.length - 1;
|
||||
|
||||
for (int i=0;i<expirationTimes.length;i++) {
|
||||
expirationDisplayValues[i] = ExpirationUtil.getExpirationDisplayValue(context, expirationTimes[i]);
|
||||
|
||||
if ((currentExpiration >= expirationTimes[i]) &&
|
||||
(i == expirationTimes.length -1 || currentExpiration < expirationTimes[i+1])) {
|
||||
selectedIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
numberPickerView.setDisplayedValues(expirationDisplayValues);
|
||||
numberPickerView.setMinValue(0);
|
||||
numberPickerView.setMaxValue(expirationTimes.length-1);
|
||||
|
||||
NumberPickerView.OnValueChangeListener listener = (picker, oldVal, newVal) -> {
|
||||
if (newVal == 0) {
|
||||
textView.setText(R.string.ExpirationDialog_your_messages_will_not_expire);
|
||||
} else {
|
||||
textView.setText(context.getString(R.string.ExpirationDialog_your_messages_will_disappear_s_after_they_have_been_seen, picker.getDisplayedValues()[newVal]));
|
||||
}
|
||||
};
|
||||
|
||||
numberPickerView.setOnValueChangedListener(listener);
|
||||
numberPickerView.setValue(selectedIndex);
|
||||
listener.onValueChange(numberPickerView, selectedIndex, selectedIndex);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private static int[] getExpirationTimes(Context context, int currentExpiration) {
|
||||
int[] expirationTimes = context.getResources().getIntArray(R.array.expiration_times);
|
||||
int location = Arrays.binarySearch(expirationTimes, currentExpiration);
|
||||
if (location < 0) {
|
||||
int[] temp = Arrays.copyOf(expirationTimes, expirationTimes.length + 1);
|
||||
temp[temp.length - 1] = currentExpiration;
|
||||
Arrays.sort(temp);
|
||||
expirationTimes = temp;
|
||||
}
|
||||
|
||||
return expirationTimes;
|
||||
}
|
||||
|
||||
public interface OnClickListener {
|
||||
public void onClick(int expirationTime);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,8 +7,6 @@ import android.graphics.PorterDuff;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
@@ -24,8 +22,8 @@ import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
||||
|
||||
import org.thoughtcrime.securesms.components.ContactFilterToolbar;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterToolbar.OnFilterChangedListener;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView.OnFilterChangedListener;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
@@ -100,7 +98,8 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
View shareButton = findViewById(R.id.share_button);
|
||||
Button smsButton = findViewById(R.id.sms_button);
|
||||
Button smsCancelButton = findViewById(R.id.cancel_sms_button);
|
||||
ContactFilterToolbar contactFilter = findViewById(R.id.contact_filter);
|
||||
Toolbar smsToolbar = findViewById(R.id.sms_send_frame_toolbar);
|
||||
ContactFilterView contactFilter = findViewById(R.id.contact_filter_edit_text);
|
||||
|
||||
inviteText = findViewById(R.id.invite_text);
|
||||
smsSendFrame = findViewById(R.id.sms_send_frame);
|
||||
@@ -121,7 +120,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
smsCancelButton.setOnClickListener(new SmsCancelClickListener());
|
||||
smsSendButton.setOnClickListener(new SmsSendClickListener());
|
||||
contactFilter.setOnFilterChangedListener(new ContactFilterChangedListener());
|
||||
contactFilter.setNavigationIcon(R.drawable.ic_search_conversation_24);
|
||||
smsToolbar.setNavigationIcon(R.drawable.ic_search_conversation_24);
|
||||
|
||||
if (Util.isDefaultSmsProvider(this)) {
|
||||
shareButton.setOnClickListener(new ShareClickListener());
|
||||
@@ -150,6 +149,10 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectionChanged() {
|
||||
}
|
||||
|
||||
private void sendSmsInvites() {
|
||||
new SendSmsInvitesAsyncTask(this, inviteText.getText().toString())
|
||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
|
||||
|
||||
@@ -9,6 +9,8 @@ import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
|
||||
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferLockedDialog;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
@@ -17,13 +19,15 @@ import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
public class MainActivity extends PassphraseRequiredActivity {
|
||||
public class MainActivity extends PassphraseRequiredActivity implements VoiceNoteMediaControllerOwner {
|
||||
|
||||
public static final int RESULT_CONFIG_CHANGED = Activity.RESULT_FIRST_USER + 901;
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
private final MainNavigator navigator = new MainNavigator(this);
|
||||
|
||||
private VoiceNoteMediaController mediaController;
|
||||
|
||||
public static @NonNull Intent clearTop(@NonNull Context context) {
|
||||
Intent intent = new Intent(context, MainActivity.class);
|
||||
|
||||
@@ -40,6 +44,7 @@ public class MainActivity extends PassphraseRequiredActivity {
|
||||
super.onCreate(savedInstanceState, ready);
|
||||
setContentView(R.layout.main_activity);
|
||||
|
||||
mediaController = new VoiceNoteMediaController(this);
|
||||
navigator.onCreate(savedInstanceState);
|
||||
|
||||
handleGroupLinkInIntent(getIntent());
|
||||
@@ -109,4 +114,9 @@ public class MainActivity extends PassphraseRequiredActivity {
|
||||
CommunicationActions.handlePotentialProxyLinkUrl(this, data.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull VoiceNoteMediaController getVoiceNoteMediaController() {
|
||||
return mediaController;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
public static final String HIDE_ALL_MEDIA_EXTRA = "came_from_all_media";
|
||||
public static final String SHOW_THREAD_EXTRA = "show_thread";
|
||||
public static final String SORTING_EXTRA = "sorting";
|
||||
public static final String IS_VIDEO_GIF = "is_video_gif";
|
||||
|
||||
private ViewPager mediaPager;
|
||||
private View detailsContainer;
|
||||
@@ -115,6 +116,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
private String initialMediaType;
|
||||
private long initialMediaSize;
|
||||
private String initialCaption;
|
||||
private boolean initialMediaIsVideoGif;
|
||||
private boolean leftIsRecent;
|
||||
private MediaPreviewViewModel viewModel;
|
||||
private ViewPagerListener viewPagerListener;
|
||||
@@ -139,6 +141,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, attachment.getSize());
|
||||
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, attachment.getCaption());
|
||||
intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, leftIsRecent);
|
||||
intent.putExtra(MediaPreviewActivity.IS_VIDEO_GIF, attachment.isVideoGif());
|
||||
intent.setDataAndType(attachment.getUri(), mediaRecord.getContentType());
|
||||
return intent;
|
||||
}
|
||||
@@ -296,12 +299,13 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
showThread = intent.getBooleanExtra(SHOW_THREAD_EXTRA, false);
|
||||
sorting = MediaDatabase.Sorting.values()[intent.getIntExtra(SORTING_EXTRA, 0)];
|
||||
|
||||
initialMediaUri = intent.getData();
|
||||
initialMediaType = intent.getType();
|
||||
initialMediaSize = intent.getLongExtra(SIZE_EXTRA, 0);
|
||||
initialCaption = intent.getStringExtra(CAPTION_EXTRA);
|
||||
leftIsRecent = intent.getBooleanExtra(LEFT_IS_RECENT_EXTRA, false);
|
||||
restartItem = -1;
|
||||
initialMediaUri = intent.getData();
|
||||
initialMediaType = intent.getType();
|
||||
initialMediaSize = intent.getLongExtra(SIZE_EXTRA, 0);
|
||||
initialCaption = intent.getStringExtra(CAPTION_EXTRA);
|
||||
leftIsRecent = intent.getBooleanExtra(LEFT_IS_RECENT_EXTRA, false);
|
||||
initialMediaIsVideoGif = intent.getBooleanExtra(IS_VIDEO_GIF, false);
|
||||
restartItem = -1;
|
||||
}
|
||||
|
||||
private void initializeObservers() {
|
||||
@@ -354,7 +358,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
if (isMediaInDb()) {
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this);
|
||||
} else {
|
||||
mediaPager.setAdapter(new SingleItemPagerAdapter(getSupportFragmentManager(), initialMediaUri, initialMediaType, initialMediaSize));
|
||||
mediaPager.setAdapter(new SingleItemPagerAdapter(getSupportFragmentManager(), initialMediaUri, initialMediaType, initialMediaSize, initialMediaIsVideoGif));
|
||||
|
||||
if (initialCaption != null) {
|
||||
detailsContainer.setVisibility(View.VISIBLE);
|
||||
@@ -632,21 +636,24 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
|
||||
private static class SingleItemPagerAdapter extends FragmentStatePagerAdapter implements MediaItemAdapter {
|
||||
|
||||
private final Uri uri;
|
||||
private final String mediaType;
|
||||
private final long size;
|
||||
private final Uri uri;
|
||||
private final String mediaType;
|
||||
private final long size;
|
||||
private final boolean isVideoGif;
|
||||
|
||||
private MediaPreviewFragment mediaPreviewFragment;
|
||||
|
||||
SingleItemPagerAdapter(@NonNull FragmentManager fragmentManager,
|
||||
@NonNull Uri uri,
|
||||
@NonNull String mediaType,
|
||||
long size)
|
||||
long size,
|
||||
boolean isVideoGif)
|
||||
{
|
||||
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
|
||||
this.uri = uri;
|
||||
this.mediaType = mediaType;
|
||||
this.size = size;
|
||||
this.uri = uri;
|
||||
this.mediaType = mediaType;
|
||||
this.size = size;
|
||||
this.isVideoGif = isVideoGif;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -657,7 +664,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
mediaPreviewFragment = MediaPreviewFragment.newInstance(uri, mediaType, size, true);
|
||||
mediaPreviewFragment = MediaPreviewFragment.newInstance(uri, mediaType, size, true, isVideoGif);
|
||||
return mediaPreviewFragment;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class MuteDialog extends AlertDialog {
|
||||
@@ -29,7 +31,7 @@ public class MuteDialog extends AlertDialog {
|
||||
}
|
||||
|
||||
public static void show(final Context context, final @NonNull MuteSelectionListener listener, @Nullable Runnable cancelListener) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(context);
|
||||
builder.setTitle(R.string.MuteDialog_mute_notifications);
|
||||
builder.setItems(R.array.mute_durations, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
|
||||
@@ -56,6 +56,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
super.onCreate(bundle, ready);
|
||||
assert getSupportActionBar() != null;
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setTitle(R.string.NewConversationActivity__new_message);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -96,6 +97,10 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectionChanged() {
|
||||
}
|
||||
|
||||
private void launch(Recipient recipient) {
|
||||
long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId());
|
||||
Intent intent = ConversationIntents.createBuilder(this, recipient.getId(), existingThread)
|
||||
|
||||
@@ -65,4 +65,8 @@ public class PushContactSelectionActivity extends ContactSelectionActivity {
|
||||
setResult(RESULT_OK, resultIntent);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectionChanged() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.os.Vibrator;
|
||||
@@ -63,9 +62,8 @@ import androidx.fragment.app.FragmentTransaction;
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.components.camera.CameraView;
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSessionLock;
|
||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
@@ -87,16 +85,13 @@ import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.WindowUtil;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.fingerprint.Fingerprint;
|
||||
import org.whispersystems.libsignal.fingerprint.FingerprintParsingException;
|
||||
import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException;
|
||||
import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator;
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Locale;
|
||||
|
||||
@@ -218,12 +213,6 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
private void setActionBarNotificationBarColor(MaterialColor color) {
|
||||
getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color.toActionBarColor(this)));
|
||||
|
||||
WindowUtil.setStatusBarColor(getWindow(), color.toStatusBarColor(this));
|
||||
}
|
||||
|
||||
public static class VerifyDisplayFragment extends Fragment implements CompoundButton.OnCheckedChangeListener {
|
||||
|
||||
public static final String RECIPIENT_ID = "recipient_id";
|
||||
@@ -420,11 +409,9 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
} else {
|
||||
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_your_contact_is_running_an_old_version_of_signal, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
} catch (FingerprintParsingException e) {
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, e);
|
||||
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_the_scanned_qr_code_is_not_a_correctly_formatted_safety_number, Toast.LENGTH_LONG).show();
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -623,7 +610,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
final RecipientId recipientId = recipient.getId();
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) {
|
||||
try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
if (isChecked) {
|
||||
Log.i(TAG, "Saving identity: " + recipientId);
|
||||
DatabaseFactory.getIdentityDatabase(getActivity())
|
||||
|
||||
@@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||
import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor;
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsListUpdatePopupWindow;
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallToastPopupWindow;
|
||||
import org.thoughtcrime.securesms.components.webrtc.GroupCallSafetyNumberChangeNotificationUtil;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView;
|
||||
@@ -62,11 +63,15 @@ import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
|
||||
import org.thoughtcrime.securesms.util.FullscreenHelper;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
|
||||
|
||||
public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChangeDialog.Callback {
|
||||
|
||||
private static final String TAG = Log.tag(WebRtcCallActivity.class);
|
||||
@@ -253,7 +258,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
viewModel.getWebRtcControls().observe(this, callScreen::setWebRtcControls);
|
||||
viewModel.getEvents().observe(this, this::handleViewModelEvent);
|
||||
viewModel.getCallTime().observe(this, this::handleCallTime);
|
||||
viewModel.getCallParticipantsState().observe(this, callScreen::updateCallParticipants);
|
||||
LiveDataUtil.combineLatest(viewModel.getCallParticipantsState(), viewModel.getOrientation(), (s, o) -> new Pair<>(s, o == PORTRAIT_BOTTOM_EDGE))
|
||||
.observe(this, p -> callScreen.updateCallParticipants(p.first(), p.second()));
|
||||
viewModel.getCallParticipantListUpdate().observe(this, participantUpdateWindow::addCallParticipantListUpdate);
|
||||
viewModel.getSafetyNumberChangeEvent().observe(this, this::handleSafetyNumberChangeEvent);
|
||||
viewModel.getGroupMembers().observe(this, unused -> updateGroupMembersForGroupCall());
|
||||
@@ -291,6 +297,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
} else if (event instanceof WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) {
|
||||
SafetyNumberChangeDialog.showForGroupCall(getSupportFragmentManager(), ((WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) event).getIdentityRecords());
|
||||
return;
|
||||
} else if (event instanceof WebRtcCallViewModel.Event.SwitchToSpeaker) {
|
||||
callScreen.switchToSpeakerView();
|
||||
return;
|
||||
} else if (event instanceof WebRtcCallViewModel.Event.ShowSwipeToSpeakerHint) {
|
||||
CallToastPopupWindow.show(callScreen);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInPipMode()) {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.thoughtcrime.securesms.animation.transitions
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ObjectAnimator
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.transition.Transition
|
||||
import androidx.transition.TransitionValues
|
||||
|
||||
private const val ALPHA = "signal.alpha_transition.alpha"
|
||||
|
||||
/**
|
||||
* Alpha transition that can be used with [ConstraintLayout]
|
||||
*/
|
||||
class AlphaTransition : Transition() {
|
||||
|
||||
override fun captureStartValues(transitionValues: TransitionValues) {
|
||||
captureValues(transitionValues)
|
||||
}
|
||||
|
||||
override fun captureEndValues(transitionValues: TransitionValues) {
|
||||
captureValues(transitionValues)
|
||||
}
|
||||
|
||||
private fun captureValues(transitionValues: TransitionValues) {
|
||||
val view: View = transitionValues.view
|
||||
if (view !is ConstraintLayout) {
|
||||
transitionValues.values[ALPHA] = view.alpha
|
||||
}
|
||||
}
|
||||
|
||||
override fun createAnimator(sceneRoot: ViewGroup, startValues: TransitionValues?, endValues: TransitionValues?): Animator? {
|
||||
if (startValues == null || endValues == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val view: View = endValues.view
|
||||
val startAlpha: Float = startValues.values[ALPHA] as? Float ?: view.alpha
|
||||
val endAlpha: Float = endValues.values[ALPHA] as? Float ?: view.alpha
|
||||
|
||||
return ObjectAnimator.ofFloat(view, "alpha", startAlpha, endAlpha)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package org.thoughtcrime.securesms.animation.transitions
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ObjectAnimator
|
||||
import android.animation.PropertyValuesHolder
|
||||
import android.animation.TypeEvaluator
|
||||
import android.content.Context
|
||||
import android.transition.Transition
|
||||
import android.transition.TransitionValues
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AccelerateInterpolator
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.view.animation.Interpolator
|
||||
import androidx.annotation.RequiresApi
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
|
||||
private const val POSITION_ON_SCREEN = "signal.circleavatartransition.positiononscreen"
|
||||
private const val WIDTH = "signal.circleavatartransition.width"
|
||||
private const val HEIGHT = "signal.circleavatartransition.height"
|
||||
|
||||
/**
|
||||
* Custom transition for Circular avatars, because once you have multiple things animating stuff was getting broken and weird.
|
||||
*/
|
||||
@RequiresApi(21)
|
||||
class CircleAvatarTransition(context: Context, attrs: AttributeSet?) : Transition(context, attrs) {
|
||||
override fun captureStartValues(transitionValues: TransitionValues) {
|
||||
captureValues(transitionValues)
|
||||
}
|
||||
|
||||
override fun captureEndValues(transitionValues: TransitionValues) {
|
||||
captureValues(transitionValues)
|
||||
}
|
||||
|
||||
private fun captureValues(transitionValues: TransitionValues) {
|
||||
val view: View = transitionValues.view
|
||||
|
||||
if (view is AvatarImageView) {
|
||||
val topLeft = intArrayOf(0, 0)
|
||||
view.getLocationOnScreen(topLeft)
|
||||
transitionValues.values[POSITION_ON_SCREEN] = topLeft
|
||||
transitionValues.values[WIDTH] = view.measuredWidth
|
||||
transitionValues.values[HEIGHT] = view.measuredHeight
|
||||
}
|
||||
}
|
||||
|
||||
override fun createAnimator(sceneRoot: ViewGroup, startValues: TransitionValues?, endValues: TransitionValues?): Animator? {
|
||||
if (startValues == null || endValues == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val view: View = endValues.view
|
||||
if (view !is AvatarImageView || view.transitionName != "avatar") {
|
||||
return null
|
||||
}
|
||||
|
||||
val startCoords: IntArray = startValues.values[POSITION_ON_SCREEN] as? IntArray ?: intArrayOf(0, 0).apply { view.getLocationOnScreen(this) }
|
||||
val endCoords: IntArray = endValues.values[POSITION_ON_SCREEN] as? IntArray ?: intArrayOf(0, 0).apply { view.getLocationOnScreen(this) }
|
||||
|
||||
val startWidth: Int = startValues.values[WIDTH] as? Int ?: view.measuredWidth
|
||||
val endWidth: Int = endValues.values[WIDTH] as? Int ?: view.measuredWidth
|
||||
|
||||
val startHeight: Int = startValues.values[HEIGHT] as? Int ?: view.measuredHeight
|
||||
val endHeight: Int = endValues.values[HEIGHT] as? Int ?: view.measuredHeight
|
||||
|
||||
val startHeightOffset = (endHeight - startHeight) / 2f
|
||||
val startWidthOffset = (endWidth - startWidth) / 2f
|
||||
|
||||
val translateXHolder = PropertyValuesHolder.ofFloat("translationX", startCoords[0] - endCoords[0] - startWidthOffset, 0f).apply {
|
||||
setEvaluator(FloatInterpolatorEvaluator(DecelerateInterpolator()))
|
||||
}
|
||||
val translateYHolder = PropertyValuesHolder.ofFloat("translationY", startCoords[1] - endCoords[1] - startHeightOffset, 0f).apply {
|
||||
setEvaluator(FloatInterpolatorEvaluator(AccelerateInterpolator()))
|
||||
}
|
||||
|
||||
val widthRatio = startWidth.toFloat() / endWidth
|
||||
val scaleXHolder = PropertyValuesHolder.ofFloat("scaleX", widthRatio, 1f)
|
||||
|
||||
val heightRatio = startHeight.toFloat() / endHeight
|
||||
val scaleYHolder = PropertyValuesHolder.ofFloat("scaleY", heightRatio, 1f)
|
||||
|
||||
return ObjectAnimator.ofPropertyValuesHolder(view, translateXHolder, translateYHolder, scaleXHolder, scaleYHolder)
|
||||
}
|
||||
|
||||
private class FloatInterpolatorEvaluator(
|
||||
private val interpolator: Interpolator
|
||||
) : TypeEvaluator<Float> {
|
||||
|
||||
override fun evaluate(fraction: Float, startValue: Float, endValue: Float): Float {
|
||||
val interpolatedFraction = interpolator.getInterpolation(fraction)
|
||||
val delta = endValue - startValue
|
||||
|
||||
return delta * interpolatedFraction + startValue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.thoughtcrime.securesms.animation.transitions
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ObjectAnimator
|
||||
import android.animation.RectEvaluator
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.transition.Transition
|
||||
import android.transition.TransitionValues
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.animation.addListener
|
||||
import androidx.fragment.app.FragmentContainerView
|
||||
|
||||
private const val BOUNDS = "signal.wipedowntransition.bottom"
|
||||
|
||||
/**
|
||||
* WipeDownTransition will animate the bottom position of a view such that it "wipes" down the screen to a final position.
|
||||
*/
|
||||
@RequiresApi(21)
|
||||
class WipeDownTransition(context: Context, attrs: AttributeSet?) : Transition(context, attrs) {
|
||||
override fun captureStartValues(transitionValues: TransitionValues) {
|
||||
captureValues(transitionValues)
|
||||
}
|
||||
|
||||
override fun captureEndValues(transitionValues: TransitionValues) {
|
||||
captureValues(transitionValues)
|
||||
}
|
||||
|
||||
private fun captureValues(transitionValues: TransitionValues) {
|
||||
val view: View = transitionValues.view
|
||||
|
||||
if (view is ViewGroup) {
|
||||
val rect = Rect()
|
||||
view.getLocalVisibleRect(rect)
|
||||
transitionValues.values[BOUNDS] = rect
|
||||
}
|
||||
}
|
||||
|
||||
override fun createAnimator(sceneRoot: ViewGroup, startValues: TransitionValues?, endValues: TransitionValues?): Animator? {
|
||||
if (startValues == null || endValues == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val view: View = endValues.view
|
||||
if (view !is FragmentContainerView) {
|
||||
return null
|
||||
}
|
||||
|
||||
val startBottom: Rect = startValues.values[BOUNDS] as? Rect ?: Rect().apply { view.getLocalVisibleRect(this) }
|
||||
val endBottom: Rect = endValues.values[BOUNDS] as? Rect ?: Rect().apply { view.getLocalVisibleRect(this) }
|
||||
|
||||
return ObjectAnimator.ofObject(view, "clipBounds", RectEvaluator(), startBottom, endBottom).apply {
|
||||
addListener(
|
||||
onEnd = {
|
||||
view.clipBounds = null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,11 +11,11 @@ import androidx.annotation.NonNull;
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -51,7 +51,7 @@ public class AudioRecorder {
|
||||
captureUri = BlobProvider.getInstance()
|
||||
.forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0)
|
||||
.withMimeType(MediaUtil.AUDIO_AAC)
|
||||
.createForSingleSessionOnDiskAsync(context, () -> Log.i(TAG, "Write successful."), e -> Log.w(TAG, "Error during recording", e));
|
||||
.createForDraftAttachmentAsync(context, () -> Log.i(TAG, "Write successful."), e -> Log.w(TAG, "Error during recording", e));
|
||||
audioCodec = new AudioCodec();
|
||||
|
||||
audioCodec.start(new ParcelFileDescriptor.AutoCloseOutputStream(fds[1]));
|
||||
@@ -61,10 +61,10 @@ public class AudioRecorder {
|
||||
});
|
||||
}
|
||||
|
||||
public @NonNull ListenableFuture<Pair<Uri, Long>> stopRecording() {
|
||||
public @NonNull ListenableFuture<VoiceNoteDraft> stopRecording() {
|
||||
Log.i(TAG, "stopRecording()");
|
||||
|
||||
final SettableFuture<Pair<Uri, Long>> future = new SettableFuture<>();
|
||||
final SettableFuture<VoiceNoteDraft> future = new SettableFuture<>();
|
||||
|
||||
executor.execute(() -> {
|
||||
if (audioCodec == null) {
|
||||
@@ -76,7 +76,7 @@ public class AudioRecorder {
|
||||
|
||||
try {
|
||||
long size = MediaUtil.getMediaSize(context, captureUri);
|
||||
sendToFuture(future, new Pair<>(captureUri, size));
|
||||
sendToFuture(future, new VoiceNoteDraft(captureUri, size));
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, ioe);
|
||||
sendToFuture(future, ioe);
|
||||
|
||||
@@ -65,12 +65,6 @@ public final class AudioWaveForm {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(attachment instanceof DatabaseAttachment)) {
|
||||
Log.i(TAG, "Not yet in database");
|
||||
ThreadUtil.runOnMain(onFailure);
|
||||
return;
|
||||
}
|
||||
|
||||
String cacheKey = uri.toString();
|
||||
AudioFileInfo cached = WAVE_FORM_CACHE.get(cacheKey);
|
||||
if (cached != null) {
|
||||
@@ -104,26 +98,46 @@ public final class AudioWaveForm {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
DatabaseAttachment dbAttachment = (DatabaseAttachment) attachment;
|
||||
long startTime = System.currentTimeMillis();
|
||||
if (attachment instanceof DatabaseAttachment) {
|
||||
try {
|
||||
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
DatabaseAttachment dbAttachment = (DatabaseAttachment) attachment;
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
attachmentDatabase.writeAudioHash(dbAttachment.getAttachmentId(), AudioWaveFormData.getDefaultInstance());
|
||||
attachmentDatabase.writeAudioHash(dbAttachment.getAttachmentId(), AudioWaveFormData.getDefaultInstance());
|
||||
|
||||
Log.i(TAG, String.format("Starting wave form generation (%s)", cacheKey));
|
||||
Log.i(TAG, String.format("Starting wave form generation (%s)", cacheKey));
|
||||
|
||||
AudioFileInfo fileInfo = generateWaveForm(uri);
|
||||
AudioFileInfo fileInfo = generateWaveForm(uri);
|
||||
|
||||
Log.i(TAG, String.format(Locale.US, "Audio wave form generation time %d ms (%s)", System.currentTimeMillis() - startTime, cacheKey));
|
||||
Log.i(TAG, String.format(Locale.US, "Audio wave form generation time %d ms (%s)", System.currentTimeMillis() - startTime, cacheKey));
|
||||
|
||||
attachmentDatabase.writeAudioHash(dbAttachment.getAttachmentId(), fileInfo.toDatabaseProtobuf());
|
||||
attachmentDatabase.writeAudioHash(dbAttachment.getAttachmentId(), fileInfo.toDatabaseProtobuf());
|
||||
|
||||
WAVE_FORM_CACHE.put(cacheKey, fileInfo);
|
||||
ThreadUtil.runOnMain(() -> onSuccess.accept(fileInfo));
|
||||
} catch (Throwable e) {
|
||||
Log.w(TAG, "Failed to create audio wave form for " + cacheKey, e);
|
||||
ThreadUtil.runOnMain(onFailure);
|
||||
WAVE_FORM_CACHE.put(cacheKey, fileInfo);
|
||||
ThreadUtil.runOnMain(() -> onSuccess.accept(fileInfo));
|
||||
} catch (Throwable e) {
|
||||
Log.w(TAG, "Failed to create audio wave form for " + cacheKey, e);
|
||||
ThreadUtil.runOnMain(onFailure);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
Log.i(TAG, "Not in database and not cached. Generating wave form on-the-fly.");
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
Log.i(TAG, String.format("Starting wave form generation (%s)", cacheKey));
|
||||
|
||||
AudioFileInfo fileInfo = generateWaveForm(uri);
|
||||
|
||||
Log.i(TAG, String.format(Locale.US, "Audio wave form generation time %d ms (%s)", System.currentTimeMillis() - startTime, cacheKey));
|
||||
|
||||
WAVE_FORM_CACHE.put(cacheKey, fileInfo);
|
||||
ThreadUtil.runOnMain(() -> onSuccess.accept(fileInfo));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to create audio wave form for " + cacheKey, e);
|
||||
ThreadUtil.runOnMain(onFailure);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,12 +24,16 @@ import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
|
||||
import org.thoughtcrime.securesms.database.KeyValueDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
|
||||
import org.thoughtcrime.securesms.database.PendingRetryReceiptDatabase;
|
||||
import org.thoughtcrime.securesms.database.SearchDatabase;
|
||||
import org.thoughtcrime.securesms.database.SenderKeyDatabase;
|
||||
import org.thoughtcrime.securesms.database.SenderKeySharedDatabase;
|
||||
import org.thoughtcrime.securesms.database.SessionDatabase;
|
||||
import org.thoughtcrime.securesms.database.SignedPreKeyDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
@@ -38,6 +42,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.service.PendingRetryReceiptManager;
|
||||
import org.thoughtcrime.securesms.util.SetUtil;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
@@ -75,7 +80,11 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
OneTimePreKeyDatabase.TABLE_NAME,
|
||||
SessionDatabase.TABLE_NAME,
|
||||
SearchDatabase.SMS_FTS_TABLE_NAME,
|
||||
SearchDatabase.MMS_FTS_TABLE_NAME
|
||||
SearchDatabase.MMS_FTS_TABLE_NAME,
|
||||
EmojiSearchDatabase.TABLE_NAME,
|
||||
SenderKeyDatabase.TABLE_NAME,
|
||||
SenderKeySharedDatabase.TABLE_NAME,
|
||||
PendingRetryReceiptDatabase.TABLE_NAME
|
||||
);
|
||||
|
||||
public static void export(@NonNull Context context,
|
||||
@@ -210,11 +219,11 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
String type = cursor.getString(2);
|
||||
|
||||
if (sql != null) {
|
||||
boolean isSmsFtsSecretTable = name != null && !name.equals(SearchDatabase.SMS_FTS_TABLE_NAME) && name.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME);
|
||||
boolean isMmsFtsSecretTable = name != null && !name.equals(SearchDatabase.MMS_FTS_TABLE_NAME) && name.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME);
|
||||
boolean isEmojiFtsSecretTable = name != null && !name.equals(EmojiSearchDatabase.TABLE_NAME) && name.startsWith(EmojiSearchDatabase.TABLE_NAME);
|
||||
|
||||
boolean isSmsFtsSecretTable = name != null && !name.equals(SearchDatabase.SMS_FTS_TABLE_NAME) && name.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME);
|
||||
boolean isMmsFtsSecretTable = name != null && !name.equals(SearchDatabase.MMS_FTS_TABLE_NAME) && name.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME);
|
||||
|
||||
if (!isSmsFtsSecretTable && !isMmsFtsSecretTable) {
|
||||
if (!isSmsFtsSecretTable && !isMmsFtsSecretTable && !isEmojiFtsSecretTable) {
|
||||
if ("table".equals(type)) {
|
||||
tables.add(name);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.backup.BackupProtos.Sticker;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
|
||||
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
|
||||
import org.thoughtcrime.securesms.database.KeyValueDatabase;
|
||||
import org.thoughtcrime.securesms.database.SearchDatabase;
|
||||
import org.thoughtcrime.securesms.database.StickerDatabase;
|
||||
@@ -136,9 +137,10 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
private static void processStatement(@NonNull SQLiteDatabase db, SqlStatement statement) {
|
||||
boolean isForSmsFtsSecretTable = statement.getStatement().contains(SearchDatabase.SMS_FTS_TABLE_NAME + "_");
|
||||
boolean isForMmsFtsSecretTable = statement.getStatement().contains(SearchDatabase.MMS_FTS_TABLE_NAME + "_");
|
||||
boolean isForEmojiSecretTable = statement.getStatement().contains(EmojiSearchDatabase.TABLE_NAME + "_");
|
||||
boolean isForSqliteSecretTable = statement.getStatement().toLowerCase().startsWith("create table sqlite_");
|
||||
|
||||
if (isForSmsFtsSecretTable || isForMmsFtsSecretTable || isForSqliteSecretTable) {
|
||||
if (isForSmsFtsSecretTable || isForMmsFtsSecretTable || isForEmojiSecretTable || isForSqliteSecretTable) {
|
||||
Log.i(TAG, "Ignoring import for statement: " + statement.getStatement());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.ViewSwitcher;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
@@ -18,7 +17,7 @@ import com.google.android.material.snackbar.Snackbar;
|
||||
import org.thoughtcrime.securesms.ContactSelectionListFragment;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterToolbar;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
@@ -47,27 +46,26 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
|
||||
|
||||
viewModel = ViewModelProviders.of(this, factory).get(BlockedUsersViewModel.class);
|
||||
|
||||
ViewSwitcher viewSwitcher = findViewById(R.id.toolbar_switcher);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
ContactFilterToolbar contactFilterToolbar = findViewById(R.id.filter_toolbar);
|
||||
View container = findViewById(R.id.fragment_container);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
ContactFilterView contactFilterView = findViewById(R.id.contact_filter_edit_text);
|
||||
View container = findViewById(R.id.fragment_container);
|
||||
|
||||
toolbar.setNavigationOnClickListener(unused -> onBackPressed());
|
||||
contactFilterToolbar.setNavigationOnClickListener(unused -> onBackPressed());
|
||||
contactFilterToolbar.setOnFilterChangedListener(query -> {
|
||||
contactFilterView.setOnFilterChangedListener(query -> {
|
||||
Fragment fragment = getSupportFragmentManager().findFragmentByTag(CONTACT_SELECTION_FRAGMENT);
|
||||
if (fragment != null) {
|
||||
((ContactSelectionListFragment) fragment).setQueryFilter(query);
|
||||
}
|
||||
});
|
||||
contactFilterToolbar.setHint(R.string.BlockedUsersActivity__add_blocked_user);
|
||||
contactFilterView.setHint(R.string.BlockedUsersActivity__add_blocked_user);
|
||||
|
||||
//noinspection CodeBlock2Expr
|
||||
getSupportFragmentManager().addOnBackStackChangedListener(() -> {
|
||||
viewSwitcher.setDisplayedChild(getSupportFragmentManager().getBackStackEntryCount());
|
||||
|
||||
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
|
||||
contactFilterToolbar.focusAndShowKeyboard();
|
||||
contactFilterView.setVisibility(View.VISIBLE);
|
||||
contactFilterView.focusAndShowKeyboard();
|
||||
} else {
|
||||
contactFilterView.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -119,6 +117,10 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectionChanged() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleAddUserToBlockedList() {
|
||||
ContactSelectionListFragment fragment = new ContactSelectionListFragment();
|
||||
@@ -164,6 +166,6 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
|
||||
throw new IllegalArgumentException("Unsupported event type " + event);
|
||||
}
|
||||
|
||||
Snackbar.make(view, getString(messageResId, displayName), Snackbar.LENGTH_SHORT).show();
|
||||
Snackbar.make(view, getString(messageResId, displayName), Snackbar.LENGTH_SHORT).setTextColor(Color.WHITE).show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,14 +91,14 @@ public enum MaterialColor {
|
||||
}
|
||||
|
||||
public @ColorRes int toQuoteBarColorResource(@NonNull Context context, boolean outgoing) {
|
||||
if (outgoing) {
|
||||
if (!outgoing) {
|
||||
return isDarkTheme(context) ? tintColor : shadeColor ;
|
||||
}
|
||||
return R.color.core_white;
|
||||
}
|
||||
|
||||
public @ColorInt int toQuoteBackgroundColor(@NonNull Context context, boolean outgoing) {
|
||||
if (outgoing) {
|
||||
if (!outgoing) {
|
||||
int color = toConversationColor(context);
|
||||
int alpha = isDarkTheme(context) ? (int) (0.2 * 255) : (int) (0.4 * 255);
|
||||
return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color));
|
||||
@@ -108,7 +108,7 @@ public enum MaterialColor {
|
||||
}
|
||||
|
||||
public @ColorInt int toQuoteFooterColor(@NonNull Context context, boolean outgoing) {
|
||||
if (outgoing) {
|
||||
if (!outgoing) {
|
||||
int color = toConversationColor(context);
|
||||
int alpha = isDarkTheme(context) ? (int) (0.4 * 255) : (int) (0.6 * 255);
|
||||
return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color));
|
||||
|
||||
@@ -45,17 +45,21 @@ public final class AudioView extends FrameLayout {
|
||||
|
||||
private static final String TAG = Log.tag(AudioView.class);
|
||||
|
||||
private static final int MODE_NORMAL = 0;
|
||||
private static final int MODE_SMALL = 1;
|
||||
private static final int MODE_DRAFT = 2;
|
||||
|
||||
private static final int FORWARDS = 1;
|
||||
private static final int REVERSE = -1;
|
||||
|
||||
@NonNull private final AnimatingToggle controlToggle;
|
||||
@NonNull private final View progressAndPlay;
|
||||
@NonNull private final LottieAnimationView playPauseButton;
|
||||
@NonNull private final ImageView downloadButton;
|
||||
@NonNull private final ProgressWheel circleProgress;
|
||||
@NonNull private final SeekBar seekBar;
|
||||
private final boolean smallView;
|
||||
private final boolean autoRewind;
|
||||
@NonNull private final AnimatingToggle controlToggle;
|
||||
@NonNull private final View progressAndPlay;
|
||||
@NonNull private final LottieAnimationView playPauseButton;
|
||||
@NonNull private final ImageView downloadButton;
|
||||
@Nullable private final ProgressWheel circleProgress;
|
||||
@NonNull private final SeekBar seekBar;
|
||||
private final boolean smallView;
|
||||
private final boolean autoRewind;
|
||||
|
||||
@Nullable private final TextView duration;
|
||||
|
||||
@@ -87,10 +91,23 @@ public final class AudioView extends FrameLayout {
|
||||
try {
|
||||
typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.AudioView, 0, 0);
|
||||
|
||||
smallView = typedArray.getBoolean(R.styleable.AudioView_small, false);
|
||||
int mode = typedArray.getInteger(R.styleable.AudioView_audioView_mode, MODE_NORMAL);
|
||||
smallView = mode == MODE_SMALL;
|
||||
autoRewind = typedArray.getBoolean(R.styleable.AudioView_autoRewind, false);
|
||||
|
||||
inflate(context, smallView ? R.layout.audio_view_small : R.layout.audio_view, this);
|
||||
switch (mode) {
|
||||
case MODE_NORMAL:
|
||||
inflate(context, R.layout.audio_view, this);
|
||||
break;
|
||||
case MODE_SMALL:
|
||||
inflate(context, R.layout.audio_view_small, this);
|
||||
break;
|
||||
case MODE_DRAFT:
|
||||
inflate(context, R.layout.audio_view_draft, this);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported mode: " + mode);
|
||||
}
|
||||
|
||||
this.controlToggle = findViewById(R.id.control_toggle);
|
||||
this.playPauseButton = findViewById(R.id.play);
|
||||
@@ -110,7 +127,7 @@ public final class AudioView extends FrameLayout {
|
||||
this.waveFormUnplayedBarsColor = typedArray.getColor(R.styleable.AudioView_waveformUnplayedBarsColor, Color.WHITE);
|
||||
this.waveFormThumbTint = typedArray.getColor(R.styleable.AudioView_waveformThumbTint, Color.WHITE);
|
||||
|
||||
progressAndPlay.getBackground().setColorFilter(typedArray.getColor(R.styleable.AudioView_progressAndPlayTint, Color.BLACK), PorterDuff.Mode.SRC_IN);
|
||||
setProgressAndPlayBackgroundTint(typedArray.getColor(R.styleable.AudioView_progressAndPlayTint, Color.BLACK));
|
||||
} finally {
|
||||
if (typedArray != null) {
|
||||
typedArray.recycle();
|
||||
@@ -130,6 +147,10 @@ public final class AudioView extends FrameLayout {
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
public void setProgressAndPlayBackgroundTint(@ColorInt int color) {
|
||||
progressAndPlay.getBackground().setColorFilter(color, PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
|
||||
public Observer<VoiceNotePlaybackState> getPlaybackStateObserver() {
|
||||
return playbackStateObserver;
|
||||
}
|
||||
@@ -158,16 +179,20 @@ public final class AudioView extends FrameLayout {
|
||||
controlToggle.displayQuick(downloadButton);
|
||||
seekBar.setEnabled(false);
|
||||
downloadButton.setOnClickListener(new DownloadClickedListener(audio));
|
||||
if (circleProgress.isSpinning()) circleProgress.stopSpinning();
|
||||
circleProgress.setVisibility(View.GONE);
|
||||
if (circleProgress != null) {
|
||||
if (circleProgress.isSpinning()) circleProgress.stopSpinning();
|
||||
circleProgress.setVisibility(View.GONE);
|
||||
}
|
||||
} else if (showControls && audio.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_STARTED) {
|
||||
controlToggle.displayQuick(progressAndPlay);
|
||||
seekBar.setEnabled(false);
|
||||
circleProgress.setVisibility(View.VISIBLE);
|
||||
circleProgress.spin();
|
||||
if (circleProgress != null) {
|
||||
circleProgress.setVisibility(View.VISIBLE);
|
||||
circleProgress.spin();
|
||||
}
|
||||
} else {
|
||||
seekBar.setEnabled(true);
|
||||
if (circleProgress.isSpinning()) circleProgress.stopSpinning();
|
||||
if (circleProgress != null && circleProgress.isSpinning()) circleProgress.stopSpinning();
|
||||
showPlayButton();
|
||||
}
|
||||
|
||||
@@ -211,10 +236,11 @@ public final class AudioView extends FrameLayout {
|
||||
|
||||
private void onPlaybackState(@NonNull VoiceNotePlaybackState voiceNotePlaybackState) {
|
||||
onDuration(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.getTrackDuration());
|
||||
onStart(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.isAutoReset());
|
||||
onProgress(voiceNotePlaybackState.getUri(),
|
||||
(double) voiceNotePlaybackState.getPlayheadPositionMillis() / voiceNotePlaybackState.getTrackDuration(),
|
||||
voiceNotePlaybackState.getPlayheadPositionMillis());
|
||||
onSpeedChanged(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.getSpeed());
|
||||
onStart(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.isPlaying(), voiceNotePlaybackState.isAutoReset());
|
||||
}
|
||||
|
||||
private void onDuration(@NonNull Uri uri, long durationMillis) {
|
||||
@@ -223,8 +249,8 @@ public final class AudioView extends FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
private void onStart(@NonNull Uri uri, boolean autoReset) {
|
||||
if (!isTarget(uri)) {
|
||||
private void onStart(@NonNull Uri uri, boolean statePlaying, boolean autoReset) {
|
||||
if (!isTarget(uri) || !statePlaying) {
|
||||
if (hasAudioUri()) {
|
||||
onStop(audioSlide.getUri(), autoReset);
|
||||
}
|
||||
@@ -274,6 +300,12 @@ public final class AudioView extends FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
private void onSpeedChanged(@NonNull Uri uri, float speed) {
|
||||
if (callbacks != null) {
|
||||
callbacks.onSpeedChanged(speed, isTarget(uri));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isTarget(@NonNull Uri uri) {
|
||||
return hasAudioUri() && Objects.equals(uri, audioSlide.getUri());
|
||||
}
|
||||
@@ -318,7 +350,7 @@ public final class AudioView extends FrameLayout {
|
||||
duration.setText(getResources().getString(R.string.AudioView_duration, remainingSecs / 60, remainingSecs % 60));
|
||||
}
|
||||
|
||||
if (smallView) {
|
||||
if (smallView && circleProgress != null) {
|
||||
circleProgress.setInstantProgress(seekBar.getProgress() == 0 ? 1 : progress);
|
||||
}
|
||||
}
|
||||
@@ -329,7 +361,10 @@ public final class AudioView extends FrameLayout {
|
||||
new LottieValueCallback<>(new SimpleColorFilter(foregroundTint))));
|
||||
|
||||
this.downloadButton.setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
|
||||
this.circleProgress.setBarColor(foregroundTint);
|
||||
|
||||
if (circleProgress != null) {
|
||||
this.circleProgress.setBarColor(foregroundTint);
|
||||
}
|
||||
|
||||
if (this.duration != null) {
|
||||
this.duration.setTextColor(foregroundTint);
|
||||
@@ -372,11 +407,14 @@ public final class AudioView extends FrameLayout {
|
||||
}
|
||||
|
||||
private void showPlayButton() {
|
||||
if (!smallView) {
|
||||
circleProgress.setVisibility(GONE);
|
||||
} else if (seekBar.getProgress() == 0) {
|
||||
circleProgress.setInstantProgress(1);
|
||||
if (circleProgress != null) {
|
||||
if (!smallView) {
|
||||
circleProgress.setVisibility(GONE);
|
||||
} else if (seekBar.getProgress() == 0) {
|
||||
circleProgress.setInstantProgress(1);
|
||||
}
|
||||
}
|
||||
|
||||
playPauseButton.setVisibility(VISIBLE);
|
||||
controlToggle.displayQuick(progressAndPlay);
|
||||
}
|
||||
@@ -451,6 +489,8 @@ public final class AudioView extends FrameLayout {
|
||||
if (callbacks != null) {
|
||||
if (wasPlaying) {
|
||||
callbacks.onSeekTo(audioSlide.getUri(), getProgress());
|
||||
} else {
|
||||
callbacks.onProgressUpdated(durationMillis, Math.round(durationMillis * getProgress()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -465,7 +505,7 @@ public final class AudioView extends FrameLayout {
|
||||
|
||||
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||
public void onEventAsync(final PartProgressEvent event) {
|
||||
if (audioSlide != null && event.attachment.equals(audioSlide.asAttachment())) {
|
||||
if (audioSlide != null && circleProgress != null && event.attachment.equals(audioSlide.asAttachment())) {
|
||||
circleProgress.setInstantProgress(((float) event.progress) / event.total);
|
||||
}
|
||||
}
|
||||
@@ -475,6 +515,7 @@ public final class AudioView extends FrameLayout {
|
||||
void onPause(@NonNull Uri audioUri);
|
||||
void onSeekTo(@NonNull Uri audioUri, double progress);
|
||||
void onStopAndReset(@NonNull Uri audioUri);
|
||||
void onSpeedChanged(float speed, boolean isPlaying);
|
||||
void onProgressUpdated(long durationMillis, long playheadMillis);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,15 @@ import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.IntRange;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.Px;
|
||||
import androidx.appcompat.widget.AppCompatImageView;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
@@ -20,21 +23,23 @@ import com.bumptech.glide.load.Transformation;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.resource.bitmap.CircleCrop;
|
||||
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy;
|
||||
import com.bumptech.glide.request.target.SimpleTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
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.groups.ui.managegroup.ManageGroupActivity;
|
||||
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.recipients.ui.managerecipient.ManageRecipientActivity;
|
||||
import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||
import org.thoughtcrime.securesms.util.BlurTransformation;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
@@ -73,6 +78,8 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
private OnClickListener listener;
|
||||
private Recipient.FallbackPhotoProvider fallbackPhotoProvider;
|
||||
private boolean blurred;
|
||||
private ChatColors chatColors;
|
||||
private FixedSizeTarget fixedSizeTarget;
|
||||
|
||||
private @Nullable RecipientContactPhoto recipientContactPhoto;
|
||||
private @NonNull Drawable unknownRecipientDrawable;
|
||||
@@ -92,15 +99,21 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.AvatarImageView, 0, 0);
|
||||
inverted = typedArray.getBoolean(R.styleable.AvatarImageView_inverted, false);
|
||||
size = typedArray.getInt(R.styleable.AvatarImageView_fallbackImageSize, SIZE_LARGE);
|
||||
inverted = typedArray.getBoolean(R.styleable.AvatarImageView_inverted, false);
|
||||
size = typedArray.getInt(R.styleable.AvatarImageView_fallbackImageSize, SIZE_LARGE);
|
||||
typedArray.recycle();
|
||||
}
|
||||
|
||||
outlinePaint = ThemeUtil.isDarkTheme(getContext()) ? DARK_THEME_OUTLINE_PAINT : LIGHT_THEME_OUTLINE_PAINT;
|
||||
outlinePaint = ThemeUtil.isDarkTheme(context) ? DARK_THEME_OUTLINE_PAINT : LIGHT_THEME_OUTLINE_PAINT;
|
||||
|
||||
unknownRecipientDrawable = new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20).asDrawable(getContext(), ContactColors.UNKNOWN_COLOR.toConversationColor(getContext()), inverted);
|
||||
unknownRecipientDrawable = new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20).asDrawable(context, AvatarColor.UNKNOWN.colorInt(), inverted);
|
||||
blurred = false;
|
||||
chatColors = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClipBounds(Rect clipBounds) {
|
||||
super.setClipBounds(clipBounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -146,6 +159,10 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
}
|
||||
}
|
||||
|
||||
public AvatarOptions.Builder buildOptions() {
|
||||
return new AvatarOptions.Builder(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows self as the note to self icon.
|
||||
*/
|
||||
@@ -165,21 +182,38 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
}
|
||||
|
||||
public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled, boolean useSelfProfileAvatar) {
|
||||
setAvatar(requestManager, recipient, new AvatarOptions.Builder(this)
|
||||
.withUseSelfProfileAvatar(useSelfProfileAvatar)
|
||||
.withQuickContactEnabled(quickContactEnabled)
|
||||
.build());
|
||||
}
|
||||
|
||||
private void setAvatar(@Nullable Recipient recipient, @NonNull AvatarOptions avatarOptions) {
|
||||
setAvatar(GlideApp.with(this), recipient, avatarOptions);
|
||||
}
|
||||
|
||||
private void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, @NonNull AvatarOptions avatarOptions) {
|
||||
if (recipient != null) {
|
||||
RecipientContactPhoto photo = (recipient.isSelf() && useSelfProfileAvatar) ? new RecipientContactPhoto(recipient,
|
||||
new ProfileContactPhoto(Recipient.self(),
|
||||
Recipient.self().getProfileAvatar()))
|
||||
: new RecipientContactPhoto(recipient);
|
||||
RecipientContactPhoto photo = (recipient.isSelf() && avatarOptions.useSelfProfileAvatar) ? new RecipientContactPhoto(recipient,
|
||||
new ProfileContactPhoto(Recipient.self(),
|
||||
Recipient.self().getProfileAvatar()))
|
||||
: new RecipientContactPhoto(recipient);
|
||||
|
||||
boolean shouldBlur = recipient.shouldBlurAvatar();
|
||||
boolean shouldBlur = recipient.shouldBlurAvatar();
|
||||
ChatColors chatColors = recipient.getChatColors();
|
||||
|
||||
if (!photo.equals(recipientContactPhoto) || shouldBlur != blurred) {
|
||||
if (!photo.equals(recipientContactPhoto) || shouldBlur != blurred || !Objects.equals(chatColors, this.chatColors)) {
|
||||
requestManager.clear(this);
|
||||
this.chatColors = chatColors;
|
||||
recipientContactPhoto = photo;
|
||||
|
||||
Drawable fallbackContactPhotoDrawable = size == SIZE_SMALL ? photo.recipient.getSmallFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider)
|
||||
: photo.recipient.getFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider);
|
||||
|
||||
if (fixedSizeTarget != null) {
|
||||
requestManager.clear(fixedSizeTarget);
|
||||
}
|
||||
|
||||
if (photo.contactPhoto != null) {
|
||||
|
||||
List<Transformation<Bitmap>> transforms = new ArrayList<>();
|
||||
@@ -189,25 +223,32 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
transforms.add(new CircleCrop());
|
||||
blurred = shouldBlur;
|
||||
|
||||
requestManager.load(photo.contactPhoto)
|
||||
.fallback(fallbackContactPhotoDrawable)
|
||||
.error(fallbackContactPhotoDrawable)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.downsample(DownsampleStrategy.CENTER_INSIDE)
|
||||
.transform(new MultiTransformation<>(transforms))
|
||||
.into(this);
|
||||
GlideRequest<Drawable> request = requestManager.load(photo.contactPhoto)
|
||||
.fallback(fallbackContactPhotoDrawable)
|
||||
.error(fallbackContactPhotoDrawable)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.downsample(DownsampleStrategy.CENTER_INSIDE)
|
||||
.transform(new MultiTransformation<>(transforms));
|
||||
|
||||
if (avatarOptions.fixedSize > 0) {
|
||||
fixedSizeTarget = new FixedSizeTarget(avatarOptions.fixedSize);
|
||||
request.into(fixedSizeTarget);
|
||||
} else {
|
||||
request.into(this);
|
||||
}
|
||||
|
||||
} else {
|
||||
setImageDrawable(fallbackContactPhotoDrawable);
|
||||
}
|
||||
}
|
||||
|
||||
setAvatarClickHandler(recipient, quickContactEnabled);
|
||||
setAvatarClickHandler(recipient, avatarOptions.quickContactEnabled);
|
||||
} else {
|
||||
recipientContactPhoto = null;
|
||||
requestManager.clear(this);
|
||||
if (fallbackPhotoProvider != null) {
|
||||
setImageDrawable(fallbackPhotoProvider.getPhotoForRecipientWithoutName()
|
||||
.asDrawable(getContext(), MaterialColor.STEEL.toAvatarColor(getContext()), inverted));
|
||||
.asDrawable(getContext(), AvatarColor.UNKNOWN.colorInt(), inverted));
|
||||
} else {
|
||||
setImageDrawable(unknownRecipientDrawable);
|
||||
}
|
||||
@@ -221,15 +262,15 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
super.setOnClickListener(v -> {
|
||||
Context context = getContext();
|
||||
if (recipient.isPushGroup()) {
|
||||
context.startActivity(ManageGroupActivity.newIntent(context, recipient.requireGroupId().requirePush()),
|
||||
ManageGroupActivity.createTransitionBundle(context, this));
|
||||
context.startActivity(ConversationSettingsActivity.forGroup(context, recipient.requireGroupId().requirePush()),
|
||||
ConversationSettingsActivity.createTransitionBundle(context, this));
|
||||
} else {
|
||||
if (context instanceof FragmentActivity) {
|
||||
RecipientBottomSheetDialogFragment.create(recipient.getId(), null)
|
||||
.show(((FragmentActivity) context).getSupportFragmentManager(), "BOTTOM");
|
||||
} else {
|
||||
context.startActivity(ManageRecipientActivity.newIntent(context, recipient.getId()),
|
||||
ManageRecipientActivity.createTransitionBundle(context, this));
|
||||
context.startActivity(ConversationSettingsActivity.forRecipient(context, recipient.getId()),
|
||||
ConversationSettingsActivity.createTransitionBundle(context, this));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -240,11 +281,11 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
|
||||
public void setImageBytesForGroup(@Nullable byte[] avatarBytes,
|
||||
@Nullable Recipient.FallbackPhotoProvider fallbackPhotoProvider,
|
||||
@NonNull MaterialColor color)
|
||||
@NonNull AvatarColor color)
|
||||
{
|
||||
Drawable fallback = Util.firstNonNull(fallbackPhotoProvider, Recipient.DEFAULT_FALLBACK_PHOTO_PROVIDER)
|
||||
.getPhotoForGroup()
|
||||
.asDrawable(getContext(), color.toAvatarColor(getContext()));
|
||||
.asDrawable(getContext(), color.colorInt());
|
||||
|
||||
GlideApp.with(this)
|
||||
.load(avatarBytes)
|
||||
@@ -285,9 +326,70 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
if (other == null) return false;
|
||||
|
||||
return other.recipient.equals(recipient) &&
|
||||
other.recipient.getColor().equals(recipient.getColor()) &&
|
||||
other.recipient.getChatColors().equals(recipient.getChatColors()) &&
|
||||
other.ready == ready &&
|
||||
Objects.equals(other.contactPhoto, contactPhoto);
|
||||
}
|
||||
}
|
||||
|
||||
private final class FixedSizeTarget extends SimpleTarget<Drawable> {
|
||||
|
||||
FixedSizeTarget(int size) {
|
||||
super(size, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
|
||||
setImageDrawable(resource);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class AvatarOptions {
|
||||
|
||||
private final boolean quickContactEnabled;
|
||||
private final boolean useSelfProfileAvatar;
|
||||
private final int fixedSize;
|
||||
|
||||
private AvatarOptions(@NonNull Builder builder) {
|
||||
this.quickContactEnabled = builder.quickContactEnabled;
|
||||
this.useSelfProfileAvatar = builder.useSelfProfileAvatar;
|
||||
this.fixedSize = builder.fixedSize;
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
private final AvatarImageView avatarImageView;
|
||||
|
||||
private boolean quickContactEnabled = false;
|
||||
private boolean useSelfProfileAvatar = false;
|
||||
private int fixedSize = -1;
|
||||
|
||||
private Builder(@NonNull AvatarImageView avatarImageView) {
|
||||
this.avatarImageView = avatarImageView;
|
||||
}
|
||||
|
||||
public @NonNull Builder withQuickContactEnabled(boolean quickContactEnabled) {
|
||||
this.quickContactEnabled = quickContactEnabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder withUseSelfProfileAvatar(boolean useSelfProfileAvatar) {
|
||||
this.useSelfProfileAvatar = useSelfProfileAvatar;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder withFixedSize(@Px @IntRange(from = 1) int fixedSize) {
|
||||
this.fixedSize = fixedSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AvatarOptions build() {
|
||||
return new AvatarOptions(this);
|
||||
}
|
||||
|
||||
public void load(@Nullable Recipient recipient) {
|
||||
avatarImageView.setAvatar(recipient, build());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.util.AttributeSet;
|
||||
import android.view.TouchDelegate;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
@@ -20,9 +21,8 @@ import androidx.core.widget.TextViewCompat;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.views.DarkOverflowToolbar;
|
||||
|
||||
public final class ContactFilterToolbar extends DarkOverflowToolbar {
|
||||
public final class ContactFilterView extends FrameLayout {
|
||||
private OnFilterChangedListener listener;
|
||||
|
||||
private final EditText searchText;
|
||||
@@ -32,17 +32,17 @@ public final class ContactFilterToolbar extends DarkOverflowToolbar {
|
||||
private final ImageView clearToggle;
|
||||
private final LinearLayout toggleContainer;
|
||||
|
||||
public ContactFilterToolbar(Context context) {
|
||||
public ContactFilterView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public ContactFilterToolbar(Context context, AttributeSet attrs) {
|
||||
public ContactFilterView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, R.attr.toolbarStyle);
|
||||
}
|
||||
|
||||
public ContactFilterToolbar(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
public ContactFilterView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
inflate(context, R.layout.contact_filter_toolbar, this);
|
||||
inflate(context, R.layout.contact_filter_view, this);
|
||||
|
||||
this.searchText = findViewById(R.id.search_view);
|
||||
this.toggle = findViewById(R.id.button_toggle);
|
||||
@@ -99,8 +99,6 @@ public final class ContactFilterToolbar extends DarkOverflowToolbar {
|
||||
}
|
||||
});
|
||||
|
||||
setLogo(null);
|
||||
setContentInsetStartWithNavigation(0);
|
||||
expandTapArea(toggleContainer, dialpadToggle);
|
||||
applyAttributes(searchText, context, attrs, defStyleAttr);
|
||||
searchText.requestFocus();
|
||||
@@ -4,6 +4,9 @@ import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
|
||||
import java.util.List;
|
||||
@@ -15,6 +18,8 @@ public class ControllableTabLayout extends TabLayout {
|
||||
|
||||
private List<View> touchables;
|
||||
|
||||
private NewTabListener newTabListener;
|
||||
|
||||
public ControllableTabLayout(Context context) {
|
||||
super(context);
|
||||
}
|
||||
@@ -39,4 +44,28 @@ public class ControllableTabLayout extends TabLayout {
|
||||
|
||||
super.setEnabled(enabled);
|
||||
}
|
||||
|
||||
public void setNewTabListener(@Nullable NewTabListener newTabListener) {
|
||||
this.newTabListener = newTabListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Tab newTab() {
|
||||
Tab tab = super.newTab();
|
||||
|
||||
if (newTabListener != null) {
|
||||
newTabListener.onNewTab(tab);
|
||||
}
|
||||
|
||||
return tab;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows implementor to modify tabs when they are created, before they are added to the tab layout.
|
||||
* This is useful for loading custom views, to ensure that time is not spent inflating these views
|
||||
* as the user is switching between pages.
|
||||
*/
|
||||
public interface NewTabListener {
|
||||
void onNewTab(@NonNull Tab tab);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.Manifest;
|
||||
import android.animation.Animator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.Rect;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
|
||||
import com.airbnb.lottie.LottieAnimationView;
|
||||
import com.airbnb.lottie.LottieProperty;
|
||||
import com.airbnb.lottie.model.KeyPath;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
@@ -30,7 +35,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.dualsim.SubscriptionInfoCompat;
|
||||
import org.thoughtcrime.securesms.util.dualsim.SubscriptionManagerCompat;
|
||||
@@ -39,17 +44,24 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class ConversationItemFooter extends LinearLayout {
|
||||
public class ConversationItemFooter extends ConstraintLayout {
|
||||
|
||||
private TextView dateView;
|
||||
private TextView simView;
|
||||
private ExpirationTimerView timerView;
|
||||
private ImageView insecureIndicatorView;
|
||||
private DeliveryStatusView deliveryStatusView;
|
||||
private boolean onlyShowSendingStatus;
|
||||
private View audioSpace;
|
||||
private TextView audioDuration;
|
||||
private LottieAnimationView revealDot;
|
||||
private TextView dateView;
|
||||
private TextView simView;
|
||||
private ExpirationTimerView timerView;
|
||||
private ImageView insecureIndicatorView;
|
||||
private DeliveryStatusView deliveryStatusView;
|
||||
private boolean onlyShowSendingStatus;
|
||||
private TextView audioDuration;
|
||||
private LottieAnimationView revealDot;
|
||||
private PlaybackSpeedToggleTextView playbackSpeedToggleTextView;
|
||||
private boolean isOutgoing;
|
||||
private boolean hasShrunkDate;
|
||||
|
||||
private OnTouchDelegateChangedListener onTouchDelegateChangedListener;
|
||||
|
||||
private final Rect speedToggleHitRect = new Rect();
|
||||
private final int touchTargetSize = ViewUtil.dpToPx(48);
|
||||
|
||||
public ConversationItemFooter(Context context) {
|
||||
super(context);
|
||||
@@ -67,24 +79,55 @@ public class ConversationItemFooter extends LinearLayout {
|
||||
}
|
||||
|
||||
private void init(@Nullable AttributeSet attrs) {
|
||||
inflate(getContext(), R.layout.conversation_item_footer, this);
|
||||
|
||||
dateView = findViewById(R.id.footer_date);
|
||||
simView = findViewById(R.id.footer_sim_info);
|
||||
timerView = findViewById(R.id.footer_expiration_timer);
|
||||
insecureIndicatorView = findViewById(R.id.footer_insecure_indicator);
|
||||
deliveryStatusView = findViewById(R.id.footer_delivery_status);
|
||||
audioDuration = findViewById(R.id.footer_audio_duration);
|
||||
audioSpace = findViewById(R.id.footer_audio_duration_space);
|
||||
revealDot = findViewById(R.id.footer_revealed_dot);
|
||||
|
||||
final TypedArray typedArray;
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemFooter, 0, 0);
|
||||
typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemFooter, 0, 0);
|
||||
} else {
|
||||
typedArray = null;
|
||||
}
|
||||
|
||||
final @LayoutRes int contentId;
|
||||
if (typedArray != null) {
|
||||
int mode = typedArray.getInt(R.styleable.ConversationItemFooter_footer_mode, 0);
|
||||
isOutgoing = mode == 0;
|
||||
|
||||
if (isOutgoing) {
|
||||
contentId = R.layout.conversation_item_footer_outgoing;
|
||||
} else {
|
||||
contentId = R.layout.conversation_item_footer_incoming;
|
||||
}
|
||||
} else {
|
||||
contentId = R.layout.conversation_item_footer_outgoing;
|
||||
isOutgoing = true;
|
||||
}
|
||||
|
||||
inflate(getContext(), contentId, this);
|
||||
|
||||
dateView = findViewById(R.id.footer_date);
|
||||
simView = findViewById(R.id.footer_sim_info);
|
||||
timerView = findViewById(R.id.footer_expiration_timer);
|
||||
insecureIndicatorView = findViewById(R.id.footer_insecure_indicator);
|
||||
deliveryStatusView = findViewById(R.id.footer_delivery_status);
|
||||
audioDuration = findViewById(R.id.footer_audio_duration);
|
||||
revealDot = findViewById(R.id.footer_revealed_dot);
|
||||
playbackSpeedToggleTextView = findViewById(R.id.footer_audio_playback_speed_toggle);
|
||||
|
||||
if (typedArray != null) {
|
||||
setTextColor(typedArray.getInt(R.styleable.ConversationItemFooter_footer_text_color, getResources().getColor(R.color.core_white)));
|
||||
setIconColor(typedArray.getInt(R.styleable.ConversationItemFooter_footer_icon_color, getResources().getColor(R.color.core_white)));
|
||||
setRevealDotColor(typedArray.getInt(R.styleable.ConversationItemFooter_footer_reveal_dot_color, getResources().getColor(R.color.core_white)));
|
||||
typedArray.recycle();
|
||||
}
|
||||
|
||||
dateView.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
|
||||
if (oldLeft != left || oldRight != right) {
|
||||
notifyTouchDelegateChanged(getPlaybackSpeedToggleTouchDelegateRect(), playbackSpeedToggleTextView);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setOnTouchDelegateChangedListener(@Nullable OnTouchDelegateChangedListener onTouchDelegateChangedListener) {
|
||||
this.onTouchDelegateChangedListener = onTouchDelegateChangedListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -107,6 +150,20 @@ public class ConversationItemFooter extends LinearLayout {
|
||||
audioDuration.setText(getResources().getString(R.string.AudioView_duration, remainingSecs / 60, remainingSecs % 60));
|
||||
}
|
||||
|
||||
public void setPlaybackSpeedListener(@Nullable PlaybackSpeedToggleTextView.PlaybackSpeedListener playbackSpeedListener) {
|
||||
playbackSpeedToggleTextView.setPlaybackSpeedListener(playbackSpeedListener);
|
||||
}
|
||||
|
||||
public void setAudioPlaybackSpeed(float playbackSpeed, boolean isPlaying) {
|
||||
if (isPlaying) {
|
||||
showPlaybackSpeedToggle();
|
||||
} else {
|
||||
hidePlaybackSpeedToggle();
|
||||
}
|
||||
|
||||
playbackSpeedToggleTextView.setCurrentSpeed(playbackSpeed);
|
||||
}
|
||||
|
||||
public void setTextColor(int color) {
|
||||
dateView.setTextColor(color);
|
||||
simView.setTextColor(color);
|
||||
@@ -123,7 +180,7 @@ public class ConversationItemFooter extends LinearLayout {
|
||||
revealDot.addValueCallback(
|
||||
new KeyPath("**"),
|
||||
LottieProperty.COLOR_FILTER,
|
||||
frameInfo -> new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
|
||||
frameInfo -> new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -146,6 +203,92 @@ public class ConversationItemFooter extends LinearLayout {
|
||||
setBackground(null);
|
||||
}
|
||||
|
||||
public @Nullable Projection getProjection() {
|
||||
if (getVisibility() == VISIBLE) {
|
||||
return Projection.relativeToViewRoot(this, new Projection.Corners(ViewUtil.dpToPx(11)));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyTouchDelegateChanged(@NonNull Rect rect, @NonNull View touchDelegate) {
|
||||
if (onTouchDelegateChangedListener != null) {
|
||||
onTouchDelegateChangedListener.onTouchDelegateChanged(rect, touchDelegate);
|
||||
}
|
||||
}
|
||||
|
||||
private void showPlaybackSpeedToggle() {
|
||||
if (hasShrunkDate) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasShrunkDate = true;
|
||||
|
||||
playbackSpeedToggleTextView.animate()
|
||||
.alpha(1f)
|
||||
.scaleX(1f)
|
||||
.scaleY(1f)
|
||||
.setDuration(150L)
|
||||
.setListener(new AnimationCompleteListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
playbackSpeedToggleTextView.setClickable(true);
|
||||
}
|
||||
});
|
||||
|
||||
if (isOutgoing) {
|
||||
dateView.setMaxWidth(ViewUtil.dpToPx(28));
|
||||
} else {
|
||||
ConstraintSet constraintSet = new ConstraintSet();
|
||||
constraintSet.clone(this);
|
||||
constraintSet.constrainMaxWidth(R.id.date_and_expiry_wrapper, ViewUtil.dpToPx(40));
|
||||
constraintSet.applyTo(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void hidePlaybackSpeedToggle() {
|
||||
if (!hasShrunkDate) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasShrunkDate = false;
|
||||
|
||||
playbackSpeedToggleTextView.animate()
|
||||
.alpha(0f)
|
||||
.scaleX(0.5f)
|
||||
.scaleY(0.5f)
|
||||
.setDuration(150L).setListener(new AnimationCompleteListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
playbackSpeedToggleTextView.setClickable(false);
|
||||
playbackSpeedToggleTextView.clearRequestedSpeed();
|
||||
}
|
||||
});
|
||||
|
||||
if (isOutgoing) {
|
||||
dateView.setMaxWidth(Integer.MAX_VALUE);
|
||||
} else {
|
||||
ConstraintSet constraintSet = new ConstraintSet();
|
||||
constraintSet.clone(this);
|
||||
constraintSet.constrainMaxWidth(R.id.date_and_expiry_wrapper, -1);
|
||||
constraintSet.applyTo(this);
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull Rect getPlaybackSpeedToggleTouchDelegateRect() {
|
||||
playbackSpeedToggleTextView.getHitRect(speedToggleHitRect);
|
||||
|
||||
int widthOffset = (touchTargetSize - speedToggleHitRect.width()) / 2;
|
||||
int heightOffset = (touchTargetSize - speedToggleHitRect.height()) / 2;
|
||||
|
||||
speedToggleHitRect.top -= heightOffset;
|
||||
speedToggleHitRect.left -= widthOffset;
|
||||
speedToggleHitRect.right += widthOffset;
|
||||
speedToggleHitRect.bottom += heightOffset;
|
||||
|
||||
return speedToggleHitRect;
|
||||
}
|
||||
|
||||
private void presentDate(@NonNull MessageRecord messageRecord, @NonNull Locale locale) {
|
||||
dateView.forceLayout();
|
||||
if (messageRecord.isFailed()) {
|
||||
@@ -180,7 +323,7 @@ public class ConversationItemFooter extends LinearLayout {
|
||||
simView.setText(getContext().getString(R.string.ConversationItem_from_s, subscriptionInfo.get().getDisplayName()));
|
||||
simView.setVisibility(View.VISIBLE);
|
||||
} else if (subscriptionInfo.isPresent()) {
|
||||
simView.setText(getContext().getString(R.string.ConversationItem_to_s, subscriptionInfo.get().getDisplayName()));
|
||||
simView.setText(getContext().getString(R.string.ConversationItem_to_s, subscriptionInfo.get().getDisplayName()));
|
||||
simView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
simView.setVisibility(View.GONE);
|
||||
@@ -209,7 +352,7 @@ public class ConversationItemFooter extends LinearLayout {
|
||||
boolean mms = messageRecord.isMms();
|
||||
|
||||
if (mms) DatabaseFactory.getMmsDatabase(getContext()).markExpireStarted(id);
|
||||
else DatabaseFactory.getSmsDatabase(getContext()).markExpireStarted(id);
|
||||
else DatabaseFactory.getSmsDatabase(getContext()).markExpireStarted(id);
|
||||
|
||||
expirationManager.scheduleDeletion(id, mms, messageRecord.getExpiresIn());
|
||||
});
|
||||
@@ -236,7 +379,7 @@ public class ConversationItemFooter extends LinearLayout {
|
||||
deliveryStatusView.setNone();
|
||||
}
|
||||
} else {
|
||||
if (!messageRecord.isOutgoing()) {
|
||||
if (!messageRecord.isOutgoing()) {
|
||||
deliveryStatusView.setNone();
|
||||
} else if (messageRecord.isPending()) {
|
||||
deliveryStatusView.setPending();
|
||||
@@ -255,11 +398,6 @@ public class ConversationItemFooter extends LinearLayout {
|
||||
MmsMessageRecord mmsMessageRecord = (MmsMessageRecord) messageRecord;
|
||||
|
||||
if (mmsMessageRecord.getSlideDeck().getAudioSlide() != null) {
|
||||
if (messageRecord.isOutgoing()) {
|
||||
moveAudioViewsForOutgoing();
|
||||
} else {
|
||||
moveAudioViewsForIncoming();
|
||||
}
|
||||
showAudioDurationViews();
|
||||
|
||||
if (messageRecord.getViewedReceiptCount() > 0) {
|
||||
@@ -275,44 +413,19 @@ public class ConversationItemFooter extends LinearLayout {
|
||||
}
|
||||
}
|
||||
|
||||
private void moveAudioViewsForOutgoing() {
|
||||
removeView(audioSpace);
|
||||
removeView(audioDuration);
|
||||
removeView(revealDot);
|
||||
addView(audioSpace, 0);
|
||||
addView(revealDot, 0);
|
||||
addView(audioDuration, 0);
|
||||
|
||||
int padStart = ViewUtil.dpToPx(60);
|
||||
int padLeft = ViewUtil.isLtr(this) ? padStart : 0;
|
||||
int padRight = ViewUtil.isRtl(this) ? padStart : 0;
|
||||
|
||||
audioDuration.setPadding(padLeft, 0, padRight, 0);
|
||||
}
|
||||
|
||||
private void moveAudioViewsForIncoming() {
|
||||
removeView(audioSpace);
|
||||
removeView(audioDuration);
|
||||
removeView(revealDot);
|
||||
addView(audioSpace);
|
||||
addView(revealDot);
|
||||
addView(audioDuration);
|
||||
|
||||
audioDuration.setPadding(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
private void showAudioDurationViews() {
|
||||
audioSpace.setVisibility(View.VISIBLE);
|
||||
audioDuration.setVisibility(View.GONE);
|
||||
|
||||
if (FeatureFlags.viewedReceipts()) {
|
||||
revealDot.setVisibility(View.VISIBLE);
|
||||
}
|
||||
audioDuration.setVisibility(View.VISIBLE);
|
||||
revealDot.setVisibility(View.VISIBLE);
|
||||
playbackSpeedToggleTextView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private void hideAudioDurationViews() {
|
||||
audioSpace.setVisibility(View.GONE);
|
||||
audioDuration.setVisibility(View.GONE);
|
||||
revealDot.setVisibility(View.GONE);
|
||||
playbackSpeedToggleTextView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public interface OnTouchDelegateChangedListener {
|
||||
void onTouchDelegateChanged(@NonNull Rect delegateRect, @NonNull View delegateView);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.List;
|
||||
@@ -116,8 +117,8 @@ public class ConversationItemThumbnail extends FrameLayout {
|
||||
thumbnail.setAlpha(1f);
|
||||
}
|
||||
|
||||
public @Nullable CornerMask getCornerMask() {
|
||||
return cornerMask;
|
||||
public @NonNull Projection.Corners getCorners() {
|
||||
return new Projection.Corners(cornerMask.getRadii());
|
||||
}
|
||||
|
||||
public void setPulseOutliner(@NonNull Outliner outliner) {
|
||||
|
||||
@@ -4,22 +4,31 @@ import android.content.Context;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class ConversationTypingView extends LinearLayout {
|
||||
public class ConversationTypingView extends ConstraintLayout {
|
||||
|
||||
private AvatarImageView avatar;
|
||||
private AvatarImageView avatar1;
|
||||
private AvatarImageView avatar2;
|
||||
private AvatarImageView avatar3;
|
||||
private View bubble;
|
||||
private TypingIndicatorView indicator;
|
||||
private TextView typistCount;
|
||||
|
||||
public ConversationTypingView(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
@@ -29,27 +38,58 @@ public class ConversationTypingView extends LinearLayout {
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
||||
avatar = findViewById(R.id.typing_avatar);
|
||||
bubble = findViewById(R.id.typing_bubble);
|
||||
indicator = findViewById(R.id.typing_indicator);
|
||||
avatar1 = findViewById(R.id.typing_avatar_1);
|
||||
avatar2 = findViewById(R.id.typing_avatar_2);
|
||||
avatar3 = findViewById(R.id.typing_avatar_3);
|
||||
typistCount = findViewById(R.id.typing_count);
|
||||
bubble = findViewById(R.id.typing_bubble);
|
||||
indicator = findViewById(R.id.typing_indicator);
|
||||
}
|
||||
|
||||
public void setTypists(@NonNull GlideRequests glideRequests, @NonNull List<Recipient> typists, boolean isGroupThread) {
|
||||
public void setTypists(@NonNull GlideRequests glideRequests, @NonNull List<Recipient> typists, boolean isGroupThread, boolean hasWallpaper) {
|
||||
if (typists.isEmpty()) {
|
||||
indicator.stopAnimation();
|
||||
return;
|
||||
}
|
||||
|
||||
Recipient typist = typists.get(0);
|
||||
bubble.getBackground().setColorFilter(typist.getColor().toConversationColor(getContext()), PorterDuff.Mode.MULTIPLY);
|
||||
avatar1.setVisibility(GONE);
|
||||
avatar2.setVisibility(GONE);
|
||||
avatar3.setVisibility(GONE);
|
||||
typistCount.setVisibility(GONE);
|
||||
|
||||
if (isGroupThread) {
|
||||
avatar.setAvatar(glideRequests, typist, true);
|
||||
avatar.setVisibility(VISIBLE);
|
||||
presentGroupThreadAvatars(glideRequests, typists);
|
||||
}
|
||||
|
||||
if (hasWallpaper) {
|
||||
bubble.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.conversation_item_wallpaper_bubble_color));
|
||||
typistCount.getBackground().setColorFilter(ContextCompat.getColor(getContext(), R.color.conversation_item_wallpaper_bubble_color), PorterDuff.Mode.SRC_IN);
|
||||
} else {
|
||||
avatar.setVisibility(GONE);
|
||||
bubble.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.signal_background_secondary));
|
||||
typistCount.getBackground().setColorFilter(ContextCompat.getColor(getContext(), R.color.signal_background_secondary), PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
|
||||
indicator.startAnimation();
|
||||
}
|
||||
|
||||
private void presentGroupThreadAvatars(@NonNull GlideRequests glideRequests, @NonNull List<Recipient> typists) {
|
||||
avatar1.setAvatar(glideRequests, typists.get(0), typists.size() == 1);
|
||||
avatar1.setVisibility(VISIBLE);
|
||||
|
||||
if (typists.size() > 1) {
|
||||
avatar2.setAvatar(glideRequests, typists.get(1), false);
|
||||
avatar2.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
if (typists.size() == 3) {
|
||||
avatar3.setAvatar(glideRequests, typists.get(2), false);
|
||||
avatar3.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
if (typists.size() > 3) {
|
||||
typistCount.setText(getResources().getString(R.string.ConversationTypingView__plus_d, typists.size() - 2));
|
||||
typistCount.setVisibility(VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,20 +22,12 @@ public class CornerMask {
|
||||
private final RectF bounds = new RectF();
|
||||
|
||||
public CornerMask(@NonNull View view) {
|
||||
this(view, null);
|
||||
}
|
||||
|
||||
public CornerMask(@NonNull View view, @Nullable CornerMask toClone) {
|
||||
view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
|
||||
|
||||
clearPaint.setColor(Color.BLACK);
|
||||
clearPaint.setStyle(Paint.Style.FILL);
|
||||
clearPaint.setAntiAlias(true);
|
||||
clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
|
||||
|
||||
if (toClone != null) {
|
||||
System.arraycopy(toClone.radii, 0, radii, 0, radii.length);
|
||||
}
|
||||
}
|
||||
|
||||
public void mask(Canvas canvas) {
|
||||
@@ -64,6 +56,13 @@ public class CornerMask {
|
||||
radii[6] = radii[7] = bottomLeft;
|
||||
}
|
||||
|
||||
public void setRadii(float topLeft, float topRight, float bottomRight, float bottomLeft) {
|
||||
radii[0] = radii[1] = topLeft;
|
||||
radii[2] = radii[3] = topRight;
|
||||
radii[4] = radii[5] = bottomRight;
|
||||
radii[6] = radii[7] = bottomLeft;
|
||||
}
|
||||
|
||||
public void setTopLeftRadius(int radius) {
|
||||
radii[0] = radii[1] = radius;
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
/**
|
||||
* Custom styled search view that we can insert into ActionBar menus
|
||||
*/
|
||||
public class DarkSearchView extends androidx.appcompat.widget.SearchView {
|
||||
public DarkSearchView(@NonNull Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public DarkSearchView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
this(context, attrs, R.attr.search_view_style_dark);
|
||||
}
|
||||
|
||||
public DarkSearchView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
EditText searchText = findViewById(androidx.appcompat.R.id.search_src_text);
|
||||
searchText.setTextColor(ContextCompat.getColor(context, R.color.signal_text_toolbar_subtitle));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
@@ -9,11 +12,15 @@ import android.text.style.StyleSpan;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class FromTextView extends EmojiTextView {
|
||||
|
||||
@@ -65,10 +72,17 @@ public class FromTextView extends EmojiTextView {
|
||||
|
||||
setText(builder);
|
||||
|
||||
if (recipient.isBlocked()) setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_block_grey600_18dp, 0, 0, 0);
|
||||
else if (recipient.isMuted()) setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_volume_off_grey600_18dp, 0, 0, 0);
|
||||
if (recipient.isBlocked()) setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_block_grey600_18dp, 0, 0, 0);
|
||||
else if (recipient.isMuted()) setCompoundDrawablesRelativeWithIntrinsicBounds(getMuted(), null, null, null);
|
||||
else setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
private Drawable getMuted() {
|
||||
Drawable mutedDrawable = Objects.requireNonNull(ContextCompat.getDrawable(getContext(), R.drawable.ic_bell_disabled_16));
|
||||
|
||||
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));
|
||||
|
||||
return mutedDrawable;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ public class InputAwareLayout extends KeyboardAwareLinearLayout implements OnKey
|
||||
addOnKeyboardShownListener(this);
|
||||
}
|
||||
|
||||
@Override public void onKeyboardShown() {
|
||||
hideAttachedInput(true);
|
||||
@Override
|
||||
public void onKeyboardShown() {
|
||||
}
|
||||
|
||||
public void show(@NonNull final EditText imeTarget, @NonNull final InputView input) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components;
|
||||
import android.animation.Animator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.net.Uri;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.AttributeSet;
|
||||
@@ -22,6 +23,8 @@ import androidx.annotation.DimenRes;
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
@@ -29,11 +32,15 @@ import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiEventListener;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationStickerSuggestionAdapter;
|
||||
import org.thoughtcrime.securesms.conversation.VoiceNoteDraftView;
|
||||
import org.thoughtcrime.securesms.database.DraftDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.keyboard.KeyboardPage;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
|
||||
@@ -42,20 +49,21 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
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 org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class InputPanel extends LinearLayout
|
||||
implements MicrophoneRecorderView.Listener,
|
||||
KeyboardAwareLinearLayout.OnKeyboardShownListener,
|
||||
EmojiKeyboardProvider.EmojiEventListener,
|
||||
EmojiEventListener,
|
||||
ConversationStickerSuggestionAdapter.EventListener
|
||||
{
|
||||
|
||||
@@ -74,12 +82,13 @@ public class InputPanel extends LinearLayout
|
||||
private View buttonToggle;
|
||||
private View recordingContainer;
|
||||
private View recordLockCancel;
|
||||
private View composeContainer;
|
||||
private ViewGroup composeContainer;
|
||||
|
||||
private MicrophoneRecorderView microphoneRecorderView;
|
||||
private SlideToCancel slideToCancel;
|
||||
private RecordTime recordTime;
|
||||
private ValueAnimator quoteAnimator;
|
||||
private VoiceNoteDraftView voiceNoteDraftView;
|
||||
|
||||
private @Nullable Listener listener;
|
||||
private boolean emojiVisible;
|
||||
@@ -115,6 +124,7 @@ public class InputPanel extends LinearLayout
|
||||
this.buttonToggle = findViewById(R.id.button_toggle);
|
||||
this.recordingContainer = findViewById(R.id.recording_container);
|
||||
this.recordLockCancel = findViewById(R.id.record_cancel);
|
||||
this.voiceNoteDraftView = findViewById(R.id.voice_note_draft_view);
|
||||
this.slideToCancel = new SlideToCancel(findViewById(R.id.slide_to_cancel));
|
||||
this.microphoneRecorderView = findViewById(R.id.recorder_view);
|
||||
this.microphoneRecorderView.setListener(this);
|
||||
@@ -151,6 +161,7 @@ public class InputPanel extends LinearLayout
|
||||
this.listener = listener;
|
||||
|
||||
mediaKeyboard.setOnClickListener(v -> listener.onEmojiToggle());
|
||||
voiceNoteDraftView.setListener(listener);
|
||||
}
|
||||
|
||||
public void setMediaListener(@NonNull MediaListener listener) {
|
||||
@@ -163,7 +174,7 @@ public class InputPanel extends LinearLayout
|
||||
@NonNull CharSequence body,
|
||||
@NonNull SlideDeck attachments)
|
||||
{
|
||||
this.quoteView.setQuote(glideRequests, id, author, body, false, attachments);
|
||||
this.quoteView.setQuote(glideRequests, id, author, body, false, attachments, null);
|
||||
|
||||
int originalHeight = this.quoteView.getVisibility() == VISIBLE ? this.quoteView.getMeasuredHeight()
|
||||
: 0;
|
||||
@@ -226,6 +237,10 @@ public class InputPanel extends LinearLayout
|
||||
return animator;
|
||||
}
|
||||
|
||||
public boolean hasSaveableContent() {
|
||||
return getQuote().isPresent() || voiceNoteDraftView.getDraft() != null;
|
||||
}
|
||||
|
||||
public Optional<QuoteModel> getQuote() {
|
||||
if (quoteView.getQuoteId() > 0 && quoteView.getVisibility() == View.VISIBLE) {
|
||||
return Optional.of(new QuoteModel(quoteView.getQuoteId(), quoteView.getAuthor().getId(), quoteView.getBody().toString(), false, quoteView.getAttachments(), quoteView.getMentions()));
|
||||
@@ -276,8 +291,8 @@ public class InputPanel extends LinearLayout
|
||||
mediaKeyboard.setVisibility(show ? View.VISIBLE : GONE);
|
||||
}
|
||||
|
||||
public void setMediaKeyboardToggleMode(boolean isSticker) {
|
||||
mediaKeyboard.setStickerMode(isSticker);
|
||||
public void setMediaKeyboardToggleMode(@NonNull KeyboardPage page) {
|
||||
mediaKeyboard.setStickerMode(page);
|
||||
}
|
||||
|
||||
public boolean isStickerMode() {
|
||||
@@ -288,13 +303,17 @@ public class InputPanel extends LinearLayout
|
||||
return mediaKeyboard;
|
||||
}
|
||||
|
||||
public MediaKeyboard.MediaKeyboardListener getMediaKeyboardListener() {
|
||||
return mediaKeyboard;
|
||||
}
|
||||
|
||||
public void setWallpaperEnabled(boolean enabled) {
|
||||
if (enabled) {
|
||||
setBackgroundColor(getContext().getResources().getColor(R.color.wallpaper_compose_background));
|
||||
composeContainer.setBackgroundResource(R.drawable.compose_background_wallpaper);
|
||||
setBackground(new ColorDrawable(getContext().getResources().getColor(R.color.wallpaper_compose_background)));
|
||||
composeContainer.setBackground(Objects.requireNonNull(ContextCompat.getDrawable(getContext(), R.drawable.compose_background_wallpaper)));
|
||||
} else {
|
||||
setBackgroundColor(getResources().getColor(R.color.signal_background_primary));
|
||||
composeContainer.setBackgroundResource(R.drawable.compose_background);
|
||||
setBackground(new ColorDrawable(getContext().getResources().getColor(R.color.signal_background_primary)));
|
||||
composeContainer.setBackground(Objects.requireNonNull(ContextCompat.getDrawable(getContext(), R.drawable.compose_background)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,7 +328,10 @@ public class InputPanel extends LinearLayout
|
||||
recordTime.display();
|
||||
slideToCancel.display();
|
||||
|
||||
if (emojiVisible) ViewUtil.fadeOut(mediaKeyboard, FADE_TIME, View.INVISIBLE);
|
||||
if (emojiVisible) {
|
||||
ViewUtil.fadeOut(mediaKeyboard, FADE_TIME, View.INVISIBLE);
|
||||
}
|
||||
|
||||
ViewUtil.fadeOut(composeText, FADE_TIME, View.INVISIBLE);
|
||||
ViewUtil.fadeOut(quickCameraToggle, FADE_TIME, View.INVISIBLE);
|
||||
ViewUtil.fadeOut(quickAudioToggle, FADE_TIME, View.INVISIBLE);
|
||||
@@ -362,6 +384,10 @@ public class InputPanel extends LinearLayout
|
||||
this.microphoneRecorderView.cancelAction();
|
||||
}
|
||||
|
||||
public @NonNull Observer<VoiceNotePlaybackState> getPlaybackStateObserver() {
|
||||
return voiceNoteDraftView.getPlaybackStateObserver();
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
composeText.setEnabled(enabled);
|
||||
mediaKeyboard.setEnabled(enabled);
|
||||
@@ -378,11 +404,7 @@ public class InputPanel extends LinearLayout
|
||||
future.addListener(new AssertedSuccessListener<Void>() {
|
||||
@Override
|
||||
public void onSuccess(Void result) {
|
||||
if (emojiVisible) ViewUtil.fadeIn(mediaKeyboard, FADE_TIME);
|
||||
ViewUtil.fadeIn(composeText, FADE_TIME);
|
||||
ViewUtil.fadeIn(quickCameraToggle, FADE_TIME);
|
||||
ViewUtil.fadeIn(quickAudioToggle, FADE_TIME);
|
||||
buttonToggle.animate().alpha(1).setDuration(FADE_TIME).start();
|
||||
fadeInNormalComposeViews();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -423,7 +445,57 @@ public class InputPanel extends LinearLayout
|
||||
microphoneRecorderView.unlockAction();
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
public void setVoiceNoteDraft(@Nullable DraftDatabase.Draft voiceNoteDraft) {
|
||||
if (voiceNoteDraft != null) {
|
||||
voiceNoteDraftView.setDraft(voiceNoteDraft);
|
||||
voiceNoteDraftView.setVisibility(VISIBLE);
|
||||
hideNormalComposeViews();
|
||||
} else {
|
||||
voiceNoteDraftView.clearDraft();
|
||||
ViewUtil.fadeOut(voiceNoteDraftView, FADE_TIME);
|
||||
fadeInNormalComposeViews();
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable DraftDatabase.Draft getVoiceNoteDraft() {
|
||||
return voiceNoteDraftView.getDraft();
|
||||
}
|
||||
|
||||
private void hideNormalComposeViews() {
|
||||
if (emojiVisible) {
|
||||
Animation animation = mediaKeyboard.getAnimation();
|
||||
if (animation != null) {
|
||||
animation.cancel();
|
||||
}
|
||||
|
||||
mediaKeyboard.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
for (Animation animation : Arrays.asList(composeText.getAnimation(), quickCameraToggle.getAnimation(), quickAudioToggle.getAnimation())) {
|
||||
if (animation != null) {
|
||||
animation.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
buttonToggle.animate().cancel();
|
||||
|
||||
composeText.setVisibility(View.INVISIBLE);
|
||||
quickCameraToggle.setVisibility(View.INVISIBLE);
|
||||
quickAudioToggle.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
private void fadeInNormalComposeViews() {
|
||||
if (emojiVisible) {
|
||||
ViewUtil.fadeIn(mediaKeyboard, FADE_TIME);
|
||||
}
|
||||
|
||||
ViewUtil.fadeIn(composeText, FADE_TIME);
|
||||
ViewUtil.fadeIn(quickCameraToggle, FADE_TIME);
|
||||
ViewUtil.fadeIn(quickAudioToggle, FADE_TIME);
|
||||
buttonToggle.animate().alpha(1).setDuration(FADE_TIME).start();
|
||||
}
|
||||
|
||||
public interface Listener extends VoiceNoteDraftView.Listener {
|
||||
void onRecorderStarted();
|
||||
void onRecorderLocked();
|
||||
void onRecorderFinished();
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
/**
|
||||
* Copyright (C) 2014 Open Whisper Systems
|
||||
*
|
||||
* <p>
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* <p>
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* <p>
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
@@ -23,8 +23,10 @@ import android.os.Build;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.Surface;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
|
||||
import androidx.appcompat.widget.LinearLayoutCompat;
|
||||
|
||||
@@ -45,21 +47,25 @@ import java.util.Set;
|
||||
public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
|
||||
private static final String TAG = Log.tag(KeyboardAwareLinearLayout.class);
|
||||
|
||||
private final Rect rect = new Rect();
|
||||
private final Set<OnKeyboardHiddenListener> hiddenListeners = new HashSet<>();
|
||||
private final Set<OnKeyboardShownListener> shownListeners = new HashSet<>();
|
||||
private final int minKeyboardSize;
|
||||
private final int minCustomKeyboardSize;
|
||||
private final int defaultCustomKeyboardSize;
|
||||
private final int minCustomKeyboardTopMarginPortrait;
|
||||
private final int minCustomKeyboardTopMarginLandscape;
|
||||
private final int statusBarHeight;
|
||||
private final Rect rect = new Rect();
|
||||
private final Set<OnKeyboardHiddenListener> hiddenListeners = new HashSet<>();
|
||||
private final Set<OnKeyboardShownListener> shownListeners = new HashSet<>();
|
||||
private final DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||
|
||||
private final int minKeyboardSize;
|
||||
private final int minCustomKeyboardSize;
|
||||
private final int defaultCustomKeyboardSize;
|
||||
private final int minCustomKeyboardTopMarginPortrait;
|
||||
private final int minCustomKeyboardTopMarginLandscape;
|
||||
private final int minCustomKeyboardTopMarginLandscapeBubble;
|
||||
private final int statusBarHeight;
|
||||
|
||||
private int viewInset;
|
||||
|
||||
private boolean keyboardOpen = false;
|
||||
private int rotation = -1;
|
||||
private boolean isFullscreen = false;
|
||||
private boolean isBubble = false;
|
||||
|
||||
public KeyboardAwareLinearLayout(Context context) {
|
||||
this(context, null);
|
||||
@@ -71,13 +77,14 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
|
||||
|
||||
public KeyboardAwareLinearLayout(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
minKeyboardSize = getResources().getDimensionPixelSize(R.dimen.min_keyboard_size);
|
||||
minCustomKeyboardSize = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_size);
|
||||
defaultCustomKeyboardSize = getResources().getDimensionPixelSize(R.dimen.default_custom_keyboard_size);
|
||||
minCustomKeyboardTopMarginPortrait = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin_portrait);
|
||||
minCustomKeyboardTopMarginLandscape = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin_portrait);
|
||||
statusBarHeight = ViewUtil.getStatusBarHeight(this);
|
||||
viewInset = getViewInset();
|
||||
minKeyboardSize = getResources().getDimensionPixelSize(R.dimen.min_keyboard_size);
|
||||
minCustomKeyboardSize = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_size);
|
||||
defaultCustomKeyboardSize = getResources().getDimensionPixelSize(R.dimen.default_custom_keyboard_size);
|
||||
minCustomKeyboardTopMarginPortrait = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin_portrait);
|
||||
minCustomKeyboardTopMarginLandscape = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin_portrait);
|
||||
minCustomKeyboardTopMarginLandscapeBubble = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin_landscape_bubble);
|
||||
statusBarHeight = ViewUtil.getStatusBarHeight(this);
|
||||
viewInset = getViewInset();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -87,6 +94,10 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
|
||||
public void setIsBubble(boolean isBubble) {
|
||||
this.isBubble = isBubble;
|
||||
}
|
||||
|
||||
private void updateRotation() {
|
||||
int oldRotation = rotation;
|
||||
rotation = getDeviceRotation();
|
||||
@@ -120,6 +131,25 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
if (Build.VERSION.SDK_INT >= 23 && getRootWindowInsets() != null) {
|
||||
int bottomInset;
|
||||
WindowInsets windowInsets = getRootWindowInsets();
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
bottomInset = windowInsets.getInsets(WindowInsets.Type.navigationBars()).bottom;
|
||||
} else {
|
||||
bottomInset = windowInsets.getStableInsetBottom();
|
||||
}
|
||||
|
||||
if (bottomInset != 0 && (viewInset == 0 || viewInset == statusBarHeight)) {
|
||||
Log.i(TAG, "Updating view inset based on WindowInsets. viewInset: " + viewInset + " windowInset: " + bottomInset);
|
||||
viewInset = bottomInset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(VERSION_CODES.LOLLIPOP)
|
||||
private int getViewInset() {
|
||||
try {
|
||||
@@ -129,7 +159,7 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
|
||||
if (attachInfo != null) {
|
||||
Field stableInsetsField = attachInfo.getClass().getDeclaredField("mStableInsets");
|
||||
stableInsetsField.setAccessible(true);
|
||||
Rect insets = (Rect)stableInsetsField.get(attachInfo);
|
||||
Rect insets = (Rect) stableInsetsField.get(attachInfo);
|
||||
if (insets != null) {
|
||||
return insets.bottom;
|
||||
}
|
||||
@@ -177,28 +207,51 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
|
||||
int rotation = getDeviceRotation();
|
||||
return rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270;
|
||||
}
|
||||
|
||||
private int getDeviceRotation() {
|
||||
return ServiceUtil.getWindowManager(getContext()).getDefaultDisplay().getRotation();
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
getContext().getDisplay().getRealMetrics(displayMetrics);
|
||||
} else {
|
||||
ServiceUtil.getWindowManager(getContext()).getDefaultDisplay().getRealMetrics(displayMetrics);
|
||||
}
|
||||
return displayMetrics.widthPixels > displayMetrics.heightPixels ? Surface.ROTATION_90 : Surface.ROTATION_0;
|
||||
}
|
||||
|
||||
private int getKeyboardLandscapeHeight() {
|
||||
if (isBubble) {
|
||||
return getRootView().getHeight() - minCustomKeyboardTopMarginLandscapeBubble;
|
||||
}
|
||||
|
||||
int keyboardHeight = PreferenceManager.getDefaultSharedPreferences(getContext())
|
||||
.getInt("keyboard_height_landscape", defaultCustomKeyboardSize);
|
||||
return Util.clamp(keyboardHeight, minCustomKeyboardSize, getRootView().getHeight() - minCustomKeyboardTopMarginLandscape);
|
||||
}
|
||||
|
||||
private int getKeyboardPortraitHeight() {
|
||||
if (isBubble) {
|
||||
int height = getRootView().getHeight();
|
||||
return height - (int)(height * 0.45);
|
||||
}
|
||||
|
||||
int keyboardHeight = PreferenceManager.getDefaultSharedPreferences(getContext())
|
||||
.getInt("keyboard_height_portrait", defaultCustomKeyboardSize);
|
||||
return Util.clamp(keyboardHeight, minCustomKeyboardSize, getRootView().getHeight() - minCustomKeyboardTopMarginPortrait);
|
||||
}
|
||||
|
||||
private void setKeyboardPortraitHeight(int height) {
|
||||
if (isBubble) {
|
||||
return;
|
||||
}
|
||||
|
||||
PreferenceManager.getDefaultSharedPreferences(getContext())
|
||||
.edit().putInt("keyboard_height_portrait", height).apply();
|
||||
}
|
||||
|
||||
private void setKeyboardLandscapeHeight(int height) {
|
||||
if (isBubble) {
|
||||
return;
|
||||
}
|
||||
|
||||
PreferenceManager.getDefaultSharedPreferences(getContext())
|
||||
.edit().putInt("keyboard_height_landscape", height).apply();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
class PlaybackSpeedToggleTextView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : AppCompatTextView(context, attrs, defStyleAttr) {
|
||||
|
||||
private val speeds: IntArray = context.resources.getIntArray(R.array.PlaybackSpeedToggleTextView__speeds)
|
||||
private val labels: Array<String> = context.resources.getStringArray(R.array.PlaybackSpeedToggleTextView__speed_labels)
|
||||
private var currentSpeedIndex = 0
|
||||
private var requestedSpeed: Float? = null
|
||||
|
||||
var playbackSpeedListener: PlaybackSpeedListener? = null
|
||||
|
||||
init {
|
||||
text = getCurrentLabel()
|
||||
super.setOnClickListener {
|
||||
currentSpeedIndex = getNextSpeedIndex()
|
||||
text = getCurrentLabel()
|
||||
requestedSpeed = getCurrentSpeed()
|
||||
|
||||
playbackSpeedListener?.onPlaybackSpeedChanged(getCurrentSpeed())
|
||||
}
|
||||
|
||||
isClickable = false
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
||||
if (isClickable) {
|
||||
when (event?.action) {
|
||||
MotionEvent.ACTION_DOWN -> zoomIn()
|
||||
MotionEvent.ACTION_UP -> zoomOut()
|
||||
MotionEvent.ACTION_CANCEL -> zoomOut()
|
||||
}
|
||||
}
|
||||
|
||||
return super.onTouchEvent(event)
|
||||
}
|
||||
|
||||
fun clearRequestedSpeed() {
|
||||
requestedSpeed = null
|
||||
}
|
||||
|
||||
fun setCurrentSpeed(speed: Float) {
|
||||
if (speed == getCurrentSpeed() || (requestedSpeed != null && requestedSpeed != speed)) {
|
||||
if (requestedSpeed == speed) {
|
||||
requestedSpeed = null
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
requestedSpeed = null
|
||||
|
||||
val outOf100 = (speed * 100).toInt()
|
||||
val index = speeds.indexOf(outOf100)
|
||||
|
||||
if (index != -1) {
|
||||
currentSpeedIndex = index
|
||||
text = getCurrentLabel()
|
||||
} else {
|
||||
throw IllegalArgumentException("Invalid Speed $speed")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNextSpeedIndex(): Int = (currentSpeedIndex + 1) % speeds.size
|
||||
|
||||
private fun getCurrentSpeed(): Float = speeds[currentSpeedIndex] / 100f
|
||||
|
||||
private fun getCurrentLabel(): String = labels[currentSpeedIndex]
|
||||
|
||||
private fun zoomIn() {
|
||||
animate()
|
||||
.setInterpolator(DecelerateInterpolator())
|
||||
.setDuration(150L)
|
||||
.scaleX(1.2f)
|
||||
.scaleY(1.2f)
|
||||
}
|
||||
|
||||
private fun zoomOut() {
|
||||
animate()
|
||||
.setInterpolator(DecelerateInterpolator())
|
||||
.setDuration(150L)
|
||||
.scaleX(1f)
|
||||
.scaleY(1f)
|
||||
}
|
||||
|
||||
override fun setOnClickListener(l: OnClickListener?) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
interface PlaybackSpeedListener {
|
||||
fun onPlaybackSpeedChanged(speed: Float)
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
@@ -25,6 +26,7 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
@@ -33,6 +35,7 @@ import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
import java.util.List;
|
||||
@@ -49,7 +52,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
private ViewGroup footerView;
|
||||
private TextView authorView;
|
||||
private TextView bodyView;
|
||||
private ImageView quoteBarView;
|
||||
private View quoteBarView;
|
||||
private ImageView thumbnailView;
|
||||
private View attachmentVideoOverlayView;
|
||||
private ViewGroup attachmentContainerView;
|
||||
@@ -152,7 +155,8 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
@NonNull Recipient author,
|
||||
@Nullable CharSequence body,
|
||||
boolean originalMissing,
|
||||
@NonNull SlideDeck attachments)
|
||||
@NonNull SlideDeck attachments,
|
||||
@Nullable ChatColors chatColors)
|
||||
{
|
||||
if (this.author != null) this.author.removeForeverObserver(this);
|
||||
|
||||
@@ -166,6 +170,12 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
setQuoteText(body, attachments);
|
||||
setQuoteAttachment(glideRequests, attachments);
|
||||
setQuoteMissingFooter(originalMissing);
|
||||
|
||||
if (Build.VERSION.SDK_INT < 21 && messageType == MESSAGE_TYPE_INCOMING && chatColors != null) {
|
||||
this.setBackgroundColor(chatColors.asSingleColor());
|
||||
} else {
|
||||
this.setBackground(null);
|
||||
}
|
||||
}
|
||||
|
||||
public void setTopCornerSizes(boolean topLeftLarge, boolean topRightLarge) {
|
||||
@@ -188,15 +198,23 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
setQuoteAuthor(recipient);
|
||||
}
|
||||
|
||||
public @NonNull Projection getProjection(@NonNull ViewGroup parent) {
|
||||
return Projection.relativeToParent(parent, this, getCorners());
|
||||
}
|
||||
|
||||
public @NonNull Projection.Corners getCorners() {
|
||||
return new Projection.Corners(cornerMask.getRadii());
|
||||
}
|
||||
|
||||
private void setQuoteAuthor(@NonNull Recipient author) {
|
||||
boolean outgoing = messageType != MESSAGE_TYPE_INCOMING;
|
||||
boolean preview = messageType == MESSAGE_TYPE_PREVIEW;
|
||||
|
||||
authorView.setText(author.isSelf() ? getContext().getString(R.string.QuoteView_you)
|
||||
: author.getDisplayName(getContext()));
|
||||
|
||||
// We use the raw color resource because Android 4.x was struggling with tints here
|
||||
quoteBarView.setImageResource(author.getColor().toQuoteBarColorResource(getContext(), outgoing));
|
||||
mainView.setBackgroundColor(author.getColor().toQuoteBackgroundColor(getContext(), outgoing));
|
||||
quoteBarView.setBackgroundColor(ContextCompat.getColor(getContext(), outgoing ? R.color.core_white : android.R.color.transparent));
|
||||
mainView.setBackgroundColor(ContextCompat.getColor(getContext(), preview ? R.color.quote_preview_background : R.color.quote_view_background));
|
||||
}
|
||||
|
||||
private void setQuoteText(@Nullable CharSequence body, @NonNull SlideDeck attachments) {
|
||||
@@ -272,7 +290,11 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
|
||||
private void setQuoteMissingFooter(boolean missing) {
|
||||
footerView.setVisibility(missing ? VISIBLE : GONE);
|
||||
footerView.setBackgroundColor(author.get().getColor().toQuoteFooterColor(getContext(), messageType != MESSAGE_TYPE_INCOMING));
|
||||
footerView.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.quote_view_background));
|
||||
}
|
||||
|
||||
public void setTextSize(int unit, float size) {
|
||||
bodyView.setTextSize(unit, size);
|
||||
}
|
||||
|
||||
public long getQuoteId() {
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.LinearGradient;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Shader;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import kotlin.jvm.functions.Function1;
|
||||
import kotlin.jvm.functions.Function2;
|
||||
|
||||
/**
|
||||
* Drawable which renders a gradient at a specified angle. Note that this drawable does
|
||||
* not implement drawable state, and all the baggage that comes with a normal Drawable
|
||||
* override, so this may not work in every scenario.
|
||||
*
|
||||
* Essentially, this drawable creates a LinearGradient shader using the given colors and
|
||||
* positions, but makes it larger than the bounds, such that it can be rotated and still
|
||||
* fill the bounds with a gradient.
|
||||
*
|
||||
* If you wish to apply clipping to this drawable, it is recommended to either use it with
|
||||
* a CardView or utilize {@link org.thoughtcrime.securesms.util.CustomDrawWrapperKt#customizeOnDraw(Drawable, Function2)}
|
||||
*/
|
||||
public final class RotatableGradientDrawable extends Drawable {
|
||||
|
||||
/**
|
||||
* From investigation into how Gradients are rendered vs how they are rendered in
|
||||
* designs, in order to match spec, we need to rotate gradients by 225 degrees. (180 + 45)
|
||||
*
|
||||
* This puts 0 at the bottom (0, -1) of the surface area.
|
||||
*/
|
||||
private static final float DEGREE_OFFSET = 225f;
|
||||
|
||||
private final float degrees;
|
||||
private final int[] colors;
|
||||
private final float[] positions;
|
||||
|
||||
private final Rect fillRect = new Rect();
|
||||
private final Paint fillPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
|
||||
|
||||
/**
|
||||
* @param degrees Gradient rotation in degrees, relative to a vector pointed from the center to bottom center
|
||||
* @param colors The colors of the gradient
|
||||
* @param positions The positions of the colors. Values should be between 0f and 1f and this array should be the
|
||||
* same length as colors.
|
||||
*/
|
||||
public RotatableGradientDrawable(float degrees, int[] colors, @Nullable float[] positions) {
|
||||
this.degrees = degrees + DEGREE_OFFSET;
|
||||
this.colors = colors;
|
||||
this.positions = positions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBounds(int left, int top, int right, int bottom) {
|
||||
super.setBounds(left, top, right, bottom);
|
||||
|
||||
Point topLeft = new Point(left, top);
|
||||
Point topRight = new Point(right, top);
|
||||
Point bottomLeft = new Point(left, bottom);
|
||||
Point bottomRight = new Point(right, bottom);
|
||||
Point origin = new Point(getBounds().width() / 2, getBounds().height() / 2);
|
||||
|
||||
Point rotationTopLeft = cornerPrime(origin, topLeft, degrees);
|
||||
Point rotationTopRight = cornerPrime(origin, topRight, degrees);
|
||||
Point rotationBottomLeft = cornerPrime(origin, bottomLeft, degrees);
|
||||
Point rotationBottomRight = cornerPrime(origin, bottomRight, degrees);
|
||||
|
||||
fillRect.left = Integer.MAX_VALUE;
|
||||
fillRect.top = Integer.MAX_VALUE;
|
||||
fillRect.right = Integer.MIN_VALUE;
|
||||
fillRect.bottom = Integer.MIN_VALUE;
|
||||
|
||||
for (Point point : Arrays.asList(topLeft, topRight, bottomLeft, bottomRight, rotationTopLeft, rotationTopRight, rotationBottomLeft, rotationBottomRight)) {
|
||||
if (point.x < fillRect.left) {
|
||||
fillRect.left = point.x;
|
||||
}
|
||||
|
||||
if (point.x > fillRect.right) {
|
||||
fillRect.right = point.x;
|
||||
}
|
||||
|
||||
if (point.y < fillRect.top) {
|
||||
fillRect.top = point.y;
|
||||
}
|
||||
|
||||
if (point.y > fillRect.bottom) {
|
||||
fillRect.bottom = point.y;
|
||||
}
|
||||
}
|
||||
|
||||
fillPaint.setShader(new LinearGradient(fillRect.left, fillRect.top, fillRect.right, fillRect.bottom, colors, positions, Shader.TileMode.CLAMP));
|
||||
}
|
||||
|
||||
private static Point cornerPrime(@NonNull Point origin, @NonNull Point corner, float degrees) {
|
||||
return new Point(xPrime(origin, corner, Math.toRadians(degrees)), yPrime(origin, corner, Math.toRadians(degrees)));
|
||||
}
|
||||
|
||||
private static int xPrime(@NonNull Point origin, @NonNull Point corner, double theta) {
|
||||
return (int) Math.ceil(((corner.x - origin.x) * Math.cos(theta)) - ((corner.y - origin.y) * Math.sin(theta)) + origin.x);
|
||||
}
|
||||
|
||||
private static int yPrime(@NonNull Point origin, @NonNull Point corner, double theta) {
|
||||
return (int) Math.ceil(((corner.x - origin.x) * Math.sin(theta)) + ((corner.y - origin.y) * Math.cos(theta)) + origin.y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
int save = canvas.save();
|
||||
canvas.rotate(degrees, getBounds().width() / 2f, getBounds().height() / 2f);
|
||||
canvas.drawRect(fillRect, fillPaint);
|
||||
canvas.restoreToCount(save);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
// Not supported
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable ColorFilter colorFilter) {
|
||||
// Not supported
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return PixelFormat.OPAQUE;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.InputFilter;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiFilter;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
|
||||
/**
|
||||
* Custom styled search view that we can insert into ActionBar menus
|
||||
@@ -23,5 +27,31 @@ public class SearchView extends androidx.appcompat.widget.SearchView {
|
||||
|
||||
public SearchView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
initEmojiFilter();
|
||||
}
|
||||
|
||||
private void initEmojiFilter() {
|
||||
if (!isInEditMode() && !SignalStore.settings().isPreferSystemEmoji()) {
|
||||
TextView searchText = findViewById(androidx.appcompat.R.id.search_src_text);
|
||||
if (searchText != null) {
|
||||
searchText.setFilters(appendEmojiFilter(searchText));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private InputFilter[] appendEmojiFilter(@NonNull TextView view) {
|
||||
InputFilter[] originalFilters = view.getFilters();
|
||||
InputFilter[] result;
|
||||
|
||||
if (originalFilters != null) {
|
||||
result = new InputFilter[originalFilters.length + 1];
|
||||
System.arraycopy(originalFilters, 0, result, 1, originalFilters.length);
|
||||
} else {
|
||||
result = new InputFilter[1];
|
||||
}
|
||||
|
||||
result[0] = new EmojiFilter(view);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@ package org.thoughtcrime.securesms.components;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.drawable.shapes.RoundRectShape;
|
||||
import android.graphics.drawable.shapes.Shape;
|
||||
import android.net.Uri;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
@@ -12,6 +18,7 @@ import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
@@ -25,6 +32,7 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicy;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequest;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
@@ -40,6 +48,7 @@ import org.thoughtcrime.securesms.util.views.Stub;
|
||||
import org.thoughtcrime.securesms.video.VideoPlayer;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
@@ -61,7 +70,6 @@ public class ThumbnailView extends FrameLayout {
|
||||
private ImageView blurhash;
|
||||
private View playOverlay;
|
||||
private View captionIcon;
|
||||
private Stub<VideoPlayer> videoPlayer;
|
||||
private OnClickListener parentClickListener;
|
||||
|
||||
private final int[] dimens = new int[2];
|
||||
@@ -93,7 +101,7 @@ public class ThumbnailView extends FrameLayout {
|
||||
this.blurhash = findViewById(R.id.thumbnail_blurhash);
|
||||
this.playOverlay = findViewById(R.id.play_overlay);
|
||||
this.captionIcon = findViewById(R.id.thumbnail_caption_icon);
|
||||
this.videoPlayer = new Stub<>(findViewById(R.id.thumbnail_player_stub));
|
||||
|
||||
super.setOnClickListener(new ThumbnailClickDispatcher());
|
||||
|
||||
if (attrs != null) {
|
||||
@@ -104,9 +112,18 @@ public class ThumbnailView extends FrameLayout {
|
||||
bounds[MAX_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxHeight, 0);
|
||||
radius = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_thumbnail_radius, getResources().getDimensionPixelSize(R.dimen.thumbnail_default_radius));
|
||||
fit = typedArray.getInt(R.styleable.ThumbnailView_thumbnail_fit, 0) == 1 ? new FitCenter() : new CenterCrop();
|
||||
|
||||
int transparentOverlayColor = typedArray.getColor(R.styleable.ThumbnailView_transparent_overlay_color, -1);
|
||||
if (transparentOverlayColor > 0) {
|
||||
image.setColorFilter(new PorterDuffColorFilter(transparentOverlayColor, PorterDuff.Mode.SRC_ATOP));
|
||||
} else {
|
||||
image.setColorFilter(null);
|
||||
}
|
||||
|
||||
typedArray.recycle();
|
||||
} else {
|
||||
radius = getResources().getDimensionPixelSize(R.dimen.message_corner_collapse_radius);
|
||||
image.setColorFilter(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,8 +41,8 @@ public class ZoomingImageView extends FrameLayout {
|
||||
|
||||
private static final float ZOOM_LEVEL_MIN = 1.0f;
|
||||
|
||||
private static final float LARGE_IMAGES_ZOOM_LEVEL_MID = 1.5f;
|
||||
private static final float LARGE_IMAGES_ZOOM_LEVEL_MAX = 2.0f;
|
||||
private static final float LARGE_IMAGES_ZOOM_LEVEL_MID = 2.0f;
|
||||
private static final float LARGE_IMAGES_ZOOM_LEVEL_MAX = 5.0f;
|
||||
|
||||
private static final float SMALL_IMAGES_ZOOM_LEVEL_MID = 3.0f;
|
||||
private static final float SMALL_IMAGES_ZOOM_LEVEL_MAX = 8.0f;
|
||||
|
||||
@@ -6,19 +6,25 @@ import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class CompositeEmojiPageModel implements EmojiPageModel {
|
||||
@AttrRes private final int iconAttr;
|
||||
@NonNull private final List<EmojiPageModel> models;
|
||||
@AttrRes private final int iconAttr;
|
||||
@NonNull private final List<EmojiPageModel> models;
|
||||
|
||||
public CompositeEmojiPageModel(@AttrRes int iconAttr, @NonNull List<EmojiPageModel> models) {
|
||||
this.iconAttr = iconAttr;
|
||||
this.models = models;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return Util.hasItems(models) ? models.get(0).getKey() : "";
|
||||
}
|
||||
|
||||
public int getIconAttr() {
|
||||
return iconAttr;
|
||||
}
|
||||
|
||||
@@ -22,4 +22,8 @@ public class Emoji {
|
||||
public List<String> getVariations() {
|
||||
return variations;
|
||||
}
|
||||
|
||||
public boolean hasMultipleVariations() {
|
||||
return variations.size() > 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,8 @@ public class EmojiEditText extends AppCompatEditText {
|
||||
boolean forceCustom = a.getBoolean(R.styleable.EmojiTextView_emoji_forceCustom, false);
|
||||
a.recycle();
|
||||
|
||||
if (forceCustom || !SignalStore.settings().isPreferSystemEmoji()) {
|
||||
if (!isInEditMode()) {
|
||||
setFilters(appendEmojiFilter(this.getFilters()));
|
||||
}
|
||||
if (!isInEditMode() && (forceCustom || !SignalStore.settings().isPreferSystemEmoji())) {
|
||||
setFilters(appendEmojiFilter(this.getFilters()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.view.KeyEvent;
|
||||
|
||||
public interface EmojiEventListener {
|
||||
void onEmojiSelected(String emoji);
|
||||
|
||||
void onKeyEvent(KeyEvent keyEvent);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.thoughtcrime.securesms.components.emoji
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.EmojiModel
|
||||
import org.thoughtcrime.securesms.util.InsetItemDecoration
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
|
||||
private val EDGE_LENGTH: Int = ViewUtil.dpToPx(6)
|
||||
private val HORIZONTAL_INSET: Int = ViewUtil.dpToPx(6)
|
||||
private val EMOJI_VERTICAL_INSET: Int = ViewUtil.dpToPx(5)
|
||||
private val HEADER_VERTICAL_INSET: Int = ViewUtil.dpToPx(8)
|
||||
|
||||
/**
|
||||
* Use super class to add insets to the emojis and use the [onDrawOver] to draw the variation
|
||||
* hint if the emoji has more than one variation.
|
||||
*/
|
||||
class EmojiItemDecoration(private val allowVariations: Boolean, private val variationsDrawable: Drawable) : InsetItemDecoration(SetInset()) {
|
||||
|
||||
override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
super.onDrawOver(canvas, parent, state)
|
||||
|
||||
val adapter: EmojiPageViewGridAdapter? = parent.adapter as? EmojiPageViewGridAdapter
|
||||
if (allowVariations && adapter != null) {
|
||||
for (i in 0 until parent.childCount) {
|
||||
val child: View = parent.getChildAt(i)
|
||||
val position: Int = parent.getChildAdapterPosition(child)
|
||||
if (position >= 0 && position <= adapter.itemCount) {
|
||||
val model = adapter.currentList[position]
|
||||
if (model is EmojiModel && model.emoji.hasMultipleVariations()) {
|
||||
variationsDrawable.setBounds(child.right, child.bottom - EDGE_LENGTH, child.right + EDGE_LENGTH, child.bottom)
|
||||
variationsDrawable.draw(canvas)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SetInset : InsetItemDecoration.SetInset() {
|
||||
override fun setInset(outRect: Rect, view: View, parent: RecyclerView) {
|
||||
val isHeader = view.javaClass == AppCompatTextView::class.java
|
||||
|
||||
outRect.left = HORIZONTAL_INSET
|
||||
outRect.right = HORIZONTAL_INSET
|
||||
outRect.top = if (isHeader) HEADER_VERTICAL_INSET else EMOJI_VERTICAL_INSET
|
||||
outRect.bottom = if (isHeader) 0 else EMOJI_VERTICAL_INSET
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.VariationSelectorListener;
|
||||
import org.thoughtcrime.securesms.emoji.EmojiSource;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.util.ResUtil;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A provider to select emoji in the {@link org.thoughtcrime.securesms.components.emoji.MediaKeyboard}.
|
||||
*/
|
||||
public class EmojiKeyboardProvider implements MediaKeyboardProvider,
|
||||
MediaKeyboardProvider.TabIconProvider,
|
||||
MediaKeyboardProvider.BackspaceObserver,
|
||||
VariationSelectorListener
|
||||
{
|
||||
private static final KeyEvent DELETE_KEY_EVENT = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL);
|
||||
|
||||
public static final String RECENT_STORAGE_KEY = "pref_recent_emoji2";
|
||||
|
||||
private final Context context;
|
||||
private final List<EmojiPageModel> models;
|
||||
private final RecentEmojiPageModel recentModel;
|
||||
private final EmojiPagerAdapter emojiPagerAdapter;
|
||||
private final EmojiEventListener emojiEventListener;
|
||||
|
||||
private Controller controller;
|
||||
private int currentPosition;
|
||||
|
||||
public EmojiKeyboardProvider(@NonNull Context context, @Nullable EmojiEventListener emojiEventListener) {
|
||||
this.context = context;
|
||||
this.emojiEventListener = emojiEventListener;
|
||||
this.models = new LinkedList<>();
|
||||
this.recentModel = new RecentEmojiPageModel(context, RECENT_STORAGE_KEY);
|
||||
this.emojiPagerAdapter = new EmojiPagerAdapter(context, models, new EmojiEventListener() {
|
||||
@Override
|
||||
public void onEmojiSelected(String emoji) {
|
||||
recentModel.onCodePointSelected(emoji);
|
||||
SignalStore.emojiValues().setPreferredVariation(emoji);
|
||||
|
||||
if (emojiEventListener != null) {
|
||||
emojiEventListener.onEmojiSelected(emoji);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKeyEvent(KeyEvent keyEvent) {
|
||||
if (emojiEventListener != null) {
|
||||
emojiEventListener.onKeyEvent(keyEvent);
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
|
||||
models.add(recentModel);
|
||||
models.addAll(EmojiSource.getLatest().getDisplayPages());
|
||||
|
||||
currentPosition = recentModel.getEmoji().size() > 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestPresentation(@NonNull Presenter presenter, boolean isSoloProvider) {
|
||||
presenter.present(this, emojiPagerAdapter, this, this, null, null, currentPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCurrentPosition(int currentPosition) {
|
||||
this.currentPosition = currentPosition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setController(@Nullable Controller controller) {
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getProviderIconView(boolean selected) {
|
||||
if (selected) {
|
||||
return R.layout.emoji_keyboard_icon_selected;
|
||||
} else {
|
||||
return R.layout.emoji_keyboard_icon;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadCategoryTabIcon(@NonNull GlideRequests glideRequests, @NonNull ImageView imageView, int index) {
|
||||
Drawable drawable = ResUtil.getDrawable(context, models.get(index).getIconAttr());
|
||||
imageView.setImageDrawable(drawable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackspaceClicked() {
|
||||
if (emojiEventListener != null) {
|
||||
emojiEventListener.onKeyEvent(DELETE_KEY_EVENT);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVariationSelectorStateChanged(boolean open) {
|
||||
if (controller != null) {
|
||||
controller.setViewPagerEnabled(!open);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
return obj instanceof EmojiKeyboardProvider;
|
||||
}
|
||||
|
||||
private static class EmojiPagerAdapter extends PagerAdapter {
|
||||
private Context context;
|
||||
private List<EmojiPageModel> pages;
|
||||
private EmojiEventListener emojiSelectionListener;
|
||||
private VariationSelectorListener variationSelectorListener;
|
||||
|
||||
public EmojiPagerAdapter(@NonNull Context context,
|
||||
@NonNull List<EmojiPageModel> pages,
|
||||
@NonNull EmojiEventListener emojiSelectionListener,
|
||||
@NonNull VariationSelectorListener variationSelectorListener)
|
||||
{
|
||||
super();
|
||||
this.context = context;
|
||||
this.pages = pages;
|
||||
this.emojiSelectionListener = emojiSelectionListener;
|
||||
this.variationSelectorListener = variationSelectorListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return pages.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Object instantiateItem(@NonNull ViewGroup container, int position) {
|
||||
EmojiPageView page = new EmojiPageView(context, emojiSelectionListener, variationSelectorListener, true);
|
||||
page.setModel(pages.get(position));
|
||||
container.addView(page);
|
||||
return page;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(ViewGroup container, int position, Object object) {
|
||||
container.removeView((View)object);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPrimaryItem(ViewGroup container, int position, Object object) {
|
||||
EmojiPageView current = (EmojiPageView) object;
|
||||
current.onSelected();
|
||||
super.setPrimaryItem(container, position, object);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isViewFromObject(View view, Object object) {
|
||||
return view == object;
|
||||
}
|
||||
}
|
||||
|
||||
public interface EmojiEventListener {
|
||||
void onEmojiSelected(String emoji);
|
||||
void onKeyEvent(KeyEvent keyEvent);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import androidx.annotation.Nullable;
|
||||
import java.util.List;
|
||||
|
||||
public interface EmojiPageModel {
|
||||
String getKey();
|
||||
int getIconAttr();
|
||||
List<String> getEmoji();
|
||||
List<Emoji> getDisplayEmoji();
|
||||
|
||||
@@ -1,63 +1,136 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearSmoothScroller;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider.EmojiEventListener;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.EmojiHeader;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.EmojiNoResultsModel;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.VariationSelectorListener;
|
||||
import org.thoughtcrime.securesms.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.util.DrawableUtil;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
public class EmojiPageView extends FrameLayout implements VariationSelectorListener {
|
||||
private static final String TAG = Log.tag(EmojiPageView.class);
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
private EmojiPageModel model;
|
||||
private EmojiPageViewGridAdapter adapter;
|
||||
private RecyclerView recyclerView;
|
||||
private GridLayoutManager layoutManager;
|
||||
public class EmojiPageView extends RecyclerView implements VariationSelectorListener {
|
||||
|
||||
private AdapterFactory adapterFactory;
|
||||
private LinearLayoutManager layoutManager;
|
||||
private RecyclerView.OnItemTouchListener scrollDisabler;
|
||||
private VariationSelectorListener variationSelectorListener;
|
||||
private EmojiVariationSelectorPopup popup;
|
||||
|
||||
public EmojiPageView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public EmojiPageView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public EmojiPageView(@NonNull Context context,
|
||||
@NonNull EmojiEventListener emojiSelectionListener,
|
||||
@NonNull VariationSelectorListener variationSelectorListener,
|
||||
boolean allowVariations)
|
||||
{
|
||||
super(context);
|
||||
final View view = LayoutInflater.from(getContext()).inflate(R.layout.emoji_grid_layout, this, true);
|
||||
initialize(emojiSelectionListener, variationSelectorListener, allowVariations);
|
||||
}
|
||||
|
||||
public EmojiPageView(@NonNull Context context,
|
||||
@NonNull EmojiEventListener emojiSelectionListener,
|
||||
@NonNull VariationSelectorListener variationSelectorListener,
|
||||
boolean allowVariations,
|
||||
@NonNull LinearLayoutManager layoutManager,
|
||||
@LayoutRes int displayEmojiLayoutResId,
|
||||
@LayoutRes int displayEmoticonLayoutResId)
|
||||
{
|
||||
super(context);
|
||||
initialize(emojiSelectionListener, variationSelectorListener, allowVariations, layoutManager, displayEmojiLayoutResId, displayEmoticonLayoutResId);
|
||||
}
|
||||
|
||||
public void initialize(@NonNull EmojiEventListener emojiSelectionListener,
|
||||
@NonNull VariationSelectorListener variationSelectorListener,
|
||||
boolean allowVariations)
|
||||
{
|
||||
initialize(emojiSelectionListener, variationSelectorListener, allowVariations, new GridLayoutManager(getContext(), 8), R.layout.emoji_display_item_grid, R.layout.emoji_text_display_item_grid);
|
||||
}
|
||||
|
||||
public void initialize(@NonNull EmojiEventListener emojiSelectionListener,
|
||||
@NonNull VariationSelectorListener variationSelectorListener,
|
||||
boolean allowVariations,
|
||||
@NonNull LinearLayoutManager layoutManager,
|
||||
@LayoutRes int displayEmojiLayoutResId,
|
||||
@LayoutRes int displayEmoticonLayoutResId)
|
||||
{
|
||||
this.variationSelectorListener = variationSelectorListener;
|
||||
|
||||
recyclerView = view.findViewById(R.id.emoji);
|
||||
layoutManager = new GridLayoutManager(context, 8);
|
||||
scrollDisabler = new ScrollDisabler();
|
||||
popup = new EmojiVariationSelectorPopup(context, emojiSelectionListener);
|
||||
adapter = new EmojiPageViewGridAdapter(popup,
|
||||
emojiSelectionListener,
|
||||
this,
|
||||
allowVariations);
|
||||
this.layoutManager = layoutManager;
|
||||
this.scrollDisabler = new ScrollDisabler();
|
||||
this.popup = new EmojiVariationSelectorPopup(getContext(), emojiSelectionListener);
|
||||
this.adapterFactory = () -> new EmojiPageViewGridAdapter(popup,
|
||||
emojiSelectionListener,
|
||||
this,
|
||||
allowVariations,
|
||||
displayEmojiLayoutResId,
|
||||
displayEmoticonLayoutResId);
|
||||
|
||||
recyclerView.setLayoutManager(layoutManager);
|
||||
recyclerView.setAdapter(adapter);
|
||||
if (this.layoutManager instanceof GridLayoutManager) {
|
||||
GridLayoutManager gridLayout = (GridLayoutManager) this.layoutManager;
|
||||
gridLayout.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
|
||||
@Override
|
||||
public int getSpanSize(int position) {
|
||||
if (getAdapter() != null) {
|
||||
Optional<MappingModel<?>> model = getAdapter().getModel(position);
|
||||
if (model.isPresent() && (model.get() instanceof EmojiHeader || model.get() instanceof EmojiNoResultsModel)) {
|
||||
return gridLayout.getSpanCount();
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setLayoutManager(layoutManager);
|
||||
|
||||
Drawable drawable = DrawableUtil.tint(ContextUtil.requireDrawable(getContext(), R.drawable.triangle_bottom_right_corner), ContextCompat.getColor(getContext(), R.color.signal_button_secondary_text_disabled));
|
||||
addItemDecoration(new EmojiItemDecoration(allowVariations, drawable));
|
||||
}
|
||||
|
||||
public void presentForEmojiKeyboard() {
|
||||
setPadding(getPaddingLeft(),
|
||||
getPaddingTop(),
|
||||
getPaddingRight(),
|
||||
getPaddingBottom() + ViewUtil.dpToPx(56));
|
||||
|
||||
setClipToPadding(false);
|
||||
}
|
||||
|
||||
public void onSelected() {
|
||||
if (model.isDynamic() && adapter != null) {
|
||||
adapter.notifyDataSetChanged();
|
||||
if (getAdapter() != null) {
|
||||
getAdapter().notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void setModel(EmojiPageModel model) {
|
||||
this.model = model;
|
||||
adapter.setEmoji(model.getDisplayEmoji());
|
||||
public void setList(@NonNull List<MappingModel<?>> list, @Nullable Runnable commitCallback) {
|
||||
EmojiPageViewGridAdapter adapter = adapterFactory.create();
|
||||
setAdapter(adapter);
|
||||
adapter.submitList(list, commitCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -69,16 +142,20 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
int idealWidth = getContext().getResources().getDimensionPixelOffset(R.dimen.emoji_drawer_item_width);
|
||||
layoutManager.setSpanCount(Math.max(w / idealWidth, 1));
|
||||
if (layoutManager instanceof GridLayoutManager) {
|
||||
int idealWidth = getContext().getResources().getDimensionPixelOffset(R.dimen.emoji_drawer_item_width);
|
||||
int spanCount = Math.max(w / idealWidth, 1);
|
||||
|
||||
((GridLayoutManager) layoutManager).setSpanCount(spanCount);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVariationSelectorStateChanged(boolean open) {
|
||||
if (open) {
|
||||
recyclerView.addOnItemTouchListener(scrollDisabler);
|
||||
addOnItemTouchListener(scrollDisabler);
|
||||
} else {
|
||||
post(() -> recyclerView.removeOnItemTouchListener(scrollDisabler));
|
||||
post(() -> removeOnItemTouchListener(scrollDisabler));
|
||||
}
|
||||
|
||||
if (variationSelectorListener != null) {
|
||||
@@ -87,7 +164,29 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
|
||||
}
|
||||
|
||||
public void setRecyclerNestedScrollingEnabled(boolean enabled) {
|
||||
recyclerView.setNestedScrollingEnabled(enabled);
|
||||
setNestedScrollingEnabled(enabled);
|
||||
}
|
||||
|
||||
public void smoothScrollToPositionTop(int position) {
|
||||
int currentPosition = layoutManager.findFirstCompletelyVisibleItemPosition();
|
||||
boolean shortTrip = Math.abs(currentPosition - position) < 475;
|
||||
|
||||
if (shortTrip) {
|
||||
RecyclerView.SmoothScroller smoothScroller = new LinearSmoothScroller(getContext()) {
|
||||
@Override
|
||||
protected int getVerticalSnapPreference() {
|
||||
return LinearSmoothScroller.SNAP_TO_START;
|
||||
}
|
||||
};
|
||||
smoothScroller.setTargetPosition(position);
|
||||
layoutManager.startSmoothScroll(smoothScroller);
|
||||
} else {
|
||||
layoutManager.scrollToPositionWithOffset(position, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable EmojiPageViewGridAdapter getAdapter() {
|
||||
return (EmojiPageViewGridAdapter) super.getAdapter();
|
||||
}
|
||||
|
||||
private static class ScrollDisabler implements RecyclerView.OnItemTouchListener {
|
||||
@@ -102,4 +201,8 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
|
||||
@Override
|
||||
public void onRequestDisallowInterceptTouchEvent(boolean b) { }
|
||||
}
|
||||
|
||||
private interface AdapterFactory {
|
||||
EmojiPageViewGridAdapter create();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,95 +1,38 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupWindow;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider.EmojiEventListener;
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
public class EmojiPageViewGridAdapter extends MappingAdapter implements PopupWindow.OnDismissListener {
|
||||
|
||||
public class EmojiPageViewGridAdapter extends RecyclerView.Adapter<EmojiPageViewGridAdapter.EmojiViewHolder> implements PopupWindow.OnDismissListener {
|
||||
|
||||
private final List<Emoji> emojiList;
|
||||
private final EmojiVariationSelectorPopup popup;
|
||||
private final VariationSelectorListener variationSelectorListener;
|
||||
private final EmojiEventListener emojiEventListener;
|
||||
private final boolean allowVariations;
|
||||
private final VariationSelectorListener variationSelectorListener;
|
||||
|
||||
public EmojiPageViewGridAdapter(@NonNull EmojiVariationSelectorPopup popup,
|
||||
@NonNull EmojiEventListener emojiEventListener,
|
||||
@NonNull VariationSelectorListener variationSelectorListener,
|
||||
boolean allowVariations)
|
||||
boolean allowVariations,
|
||||
@LayoutRes int displayEmojiLayoutResId,
|
||||
@LayoutRes int displayEmoticonLayoutResId)
|
||||
{
|
||||
this.emojiList = new ArrayList<>();
|
||||
this.popup = popup;
|
||||
this.emojiEventListener = emojiEventListener;
|
||||
this.variationSelectorListener = variationSelectorListener;
|
||||
this.allowVariations = allowVariations;
|
||||
|
||||
popup.setOnDismissListener(this);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public EmojiViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
|
||||
return new EmojiViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.emoji_display_item, viewGroup, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull EmojiViewHolder viewHolder, int i) {
|
||||
Emoji emoji = emojiList.get(i);
|
||||
|
||||
final Drawable drawable = EmojiProvider.getEmojiDrawable(viewHolder.imageView.getContext(), emoji.getValue());
|
||||
|
||||
if (drawable != null) {
|
||||
viewHolder.textView.setVisibility(View.GONE);
|
||||
viewHolder.imageView.setVisibility(View.VISIBLE);
|
||||
|
||||
viewHolder.imageView.setImageDrawable(drawable);
|
||||
} else {
|
||||
viewHolder.textView.setVisibility(View.VISIBLE);
|
||||
viewHolder.imageView.setVisibility(View.GONE);
|
||||
|
||||
viewHolder.textView.setEmoji(emoji.getValue());
|
||||
}
|
||||
|
||||
viewHolder.itemView.setOnClickListener(v -> {
|
||||
emojiEventListener.onEmojiSelected(emoji.getValue());
|
||||
});
|
||||
|
||||
if (allowVariations && emoji.getVariations().size() > 1) {
|
||||
viewHolder.itemView.setOnLongClickListener(v -> {
|
||||
popup.dismiss();
|
||||
popup.setVariations(emoji.getVariations());
|
||||
popup.showAsDropDown(viewHolder.itemView, 0, -(2 * viewHolder.itemView.getHeight()));
|
||||
variationSelectorListener.onVariationSelectorStateChanged(true);
|
||||
return true;
|
||||
});
|
||||
viewHolder.hintCorner.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
viewHolder.itemView.setOnLongClickListener(null);
|
||||
viewHolder.hintCorner.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return emojiList.size();
|
||||
}
|
||||
|
||||
public void setEmoji(@NonNull List<Emoji> emojiList) {
|
||||
this.emojiList.clear();
|
||||
this.emojiList.addAll(emojiList);
|
||||
notifyDataSetChanged();
|
||||
registerFactory(EmojiHeader.class, new LayoutFactory<>(EmojiHeaderViewHolder::new, R.layout.emoji_grid_header));
|
||||
registerFactory(EmojiModel.class, new LayoutFactory<>(v -> new EmojiViewHolder(v, emojiEventListener, variationSelectorListener, popup, allowVariations), displayEmojiLayoutResId));
|
||||
registerFactory(EmojiTextModel.class, new LayoutFactory<>(v -> new EmojiTextViewHolder(v, emojiEventListener), displayEmoticonLayoutResId));
|
||||
registerFactory(EmojiNoResultsModel.class, new LayoutFactory<>(MappingViewHolder.SimpleViewHolder::new, R.layout.emoji_grid_no_results));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -97,18 +40,196 @@ public class EmojiPageViewGridAdapter extends RecyclerView.Adapter<EmojiPageView
|
||||
variationSelectorListener.onVariationSelectorStateChanged(false);
|
||||
}
|
||||
|
||||
static class EmojiViewHolder extends RecyclerView.ViewHolder {
|
||||
public static class EmojiHeader implements MappingModel<EmojiHeader>, HasKey {
|
||||
|
||||
private final ImageView imageView;
|
||||
private final AsciiEmojiView textView;
|
||||
private final ImageView hintCorner;
|
||||
private final String key;
|
||||
private final int title;
|
||||
|
||||
public EmojiViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
this.imageView = itemView.findViewById(R.id.emoji_image);
|
||||
this.textView = itemView.findViewById(R.id.emoji_text);
|
||||
this.hintCorner = itemView.findViewById(R.id.emoji_variation_hint);
|
||||
public EmojiHeader(@NonNull String key, int title) {
|
||||
this.key = key;
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull EmojiHeader newItem) {
|
||||
return title == newItem.title;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull EmojiHeader newItem) {
|
||||
return areItemsTheSame(newItem);
|
||||
}
|
||||
}
|
||||
|
||||
static class EmojiHeaderViewHolder extends MappingViewHolder<EmojiHeader> {
|
||||
|
||||
private final TextView title;
|
||||
|
||||
public EmojiHeaderViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
title = findViewById(R.id.emoji_grid_header_title);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull EmojiHeader model) {
|
||||
title.setText(model.title);
|
||||
}
|
||||
}
|
||||
|
||||
public static class EmojiModel implements MappingModel<EmojiModel>, HasKey {
|
||||
|
||||
private final String key;
|
||||
private final Emoji emoji;
|
||||
|
||||
public EmojiModel(@NonNull String key, @NonNull Emoji emoji) {
|
||||
this.key = key;
|
||||
this.emoji = emoji;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public @NonNull Emoji getEmoji() {
|
||||
return emoji;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull EmojiModel newItem) {
|
||||
return newItem.emoji.getValue().equals(emoji.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull EmojiModel newItem) {
|
||||
return areItemsTheSame(newItem);
|
||||
}
|
||||
}
|
||||
|
||||
static class EmojiViewHolder extends MappingViewHolder<EmojiModel> {
|
||||
|
||||
private final EmojiVariationSelectorPopup popup;
|
||||
private final VariationSelectorListener variationSelectorListener;
|
||||
private final EmojiEventListener emojiEventListener;
|
||||
private final boolean allowVariations;
|
||||
|
||||
private final ImageView imageView;
|
||||
|
||||
public EmojiViewHolder(@NonNull View itemView,
|
||||
@NonNull EmojiEventListener emojiEventListener,
|
||||
@NonNull VariationSelectorListener variationSelectorListener,
|
||||
@NonNull EmojiVariationSelectorPopup popup,
|
||||
boolean allowVariations)
|
||||
{
|
||||
super(itemView);
|
||||
|
||||
this.popup = popup;
|
||||
this.variationSelectorListener = variationSelectorListener;
|
||||
this.emojiEventListener = emojiEventListener;
|
||||
this.allowVariations = allowVariations;
|
||||
|
||||
this.imageView = itemView.findViewById(R.id.emoji_image);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull EmojiModel model) {
|
||||
final Drawable drawable = EmojiProvider.getEmojiDrawable(imageView.getContext(), model.emoji.getValue());
|
||||
|
||||
if (drawable != null) {
|
||||
imageView.setVisibility(View.VISIBLE);
|
||||
imageView.setImageDrawable(drawable);
|
||||
}
|
||||
|
||||
itemView.setOnClickListener(v -> {
|
||||
emojiEventListener.onEmojiSelected(model.emoji.getValue());
|
||||
});
|
||||
|
||||
if (allowVariations && model.emoji.hasMultipleVariations()) {
|
||||
itemView.setOnLongClickListener(v -> {
|
||||
popup.dismiss();
|
||||
popup.setVariations(model.emoji.getVariations());
|
||||
popup.showAsDropDown(itemView, 0, -(2 * itemView.getHeight()));
|
||||
variationSelectorListener.onVariationSelectorStateChanged(true);
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
itemView.setOnLongClickListener(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class EmojiTextModel implements MappingModel<EmojiTextModel>, HasKey {
|
||||
private final String key;
|
||||
private final Emoji emoji;
|
||||
|
||||
public EmojiTextModel(@NonNull String key, @NonNull Emoji emoji) {
|
||||
this.key = key;
|
||||
this.emoji = emoji;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public @NonNull Emoji getEmoji() {
|
||||
return emoji;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull EmojiTextModel newItem) {
|
||||
return newItem.emoji.getValue().equals(emoji.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull EmojiTextModel newItem) {
|
||||
return areItemsTheSame(newItem);
|
||||
}
|
||||
}
|
||||
|
||||
static class EmojiTextViewHolder extends MappingViewHolder<EmojiTextModel> {
|
||||
|
||||
private final EmojiEventListener emojiEventListener;
|
||||
private final AsciiEmojiView textView;
|
||||
|
||||
public EmojiTextViewHolder(@NonNull View itemView,
|
||||
@NonNull EmojiEventListener emojiEventListener)
|
||||
{
|
||||
super(itemView);
|
||||
|
||||
this.emojiEventListener = emojiEventListener;
|
||||
this.textView = itemView.findViewById(R.id.emoji_text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull EmojiTextModel model) {
|
||||
textView.setEmoji(model.emoji.getValue());
|
||||
|
||||
itemView.setOnClickListener(v -> {
|
||||
emojiEventListener.onEmojiSelected(model.emoji.getValue());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static class EmojiNoResultsModel implements MappingModel<EmojiNoResultsModel> {
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull EmojiNoResultsModel newItem) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull EmojiNoResultsModel newItem) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public interface HasKey {
|
||||
@NonNull String getKey();
|
||||
}
|
||||
|
||||
public interface VariationSelectorListener {
|
||||
|
||||
@@ -24,7 +24,6 @@ import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
@@ -91,7 +90,8 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
super.onDraw(canvas);
|
||||
}
|
||||
|
||||
@Override public void setText(@Nullable CharSequence text, BufferType type) {
|
||||
@Override
|
||||
public void setText(@Nullable CharSequence text, BufferType type) {
|
||||
EmojiParser.CandidateList candidates = isInEditMode() ? null : EmojiProvider.getCandidates(text);
|
||||
|
||||
if (scaleEmojis && candidates != null && candidates.allEmojis) {
|
||||
@@ -118,23 +118,19 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
useSystemEmoji = useSystemEmoji();
|
||||
|
||||
if (useSystemEmoji || candidates == null || candidates.size() == 0) {
|
||||
super.setText(new SpannableStringBuilder(Optional.fromNullable(text).or("")).append(Optional.fromNullable(overflowText).or("")), BufferType.NORMAL);
|
||||
|
||||
if (getEllipsize() == TextUtils.TruncateAt.END && maxLength > 0) {
|
||||
ellipsizeAnyTextForMaxLength();
|
||||
}
|
||||
super.setText(new SpannableStringBuilder(Optional.fromNullable(text).or("")), BufferType.NORMAL);
|
||||
} else {
|
||||
CharSequence emojified = EmojiProvider.emojify(candidates, text, this);
|
||||
super.setText(new SpannableStringBuilder(emojified).append(Optional.fromNullable(overflowText).or("")), BufferType.SPANNABLE);
|
||||
super.setText(new SpannableStringBuilder(emojified), BufferType.SPANNABLE);
|
||||
}
|
||||
|
||||
// Android fails to ellipsize spannable strings. (https://issuetracker.google.com/issues/36991688)
|
||||
// We ellipsize them ourselves by manually truncating the appropriate section.
|
||||
if (getEllipsize() == TextUtils.TruncateAt.END) {
|
||||
if (maxLength > 0) {
|
||||
ellipsizeAnyTextForMaxLength();
|
||||
} else {
|
||||
ellipsizeEmojiTextForMaxLines();
|
||||
}
|
||||
// Android fails to ellipsize spannable strings. (https://issuetracker.google.com/issues/36991688)
|
||||
// We ellipsize them ourselves by manually truncating the appropriate section.
|
||||
if (getText() != null && getText().length() > 0 && getEllipsize() == TextUtils.TruncateAt.END) {
|
||||
if (maxLength > 0) {
|
||||
ellipsizeAnyTextForMaxLength();
|
||||
} else if (getMaxLines() > 0) {
|
||||
ellipsizeEmojiTextForMaxLines();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +188,8 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
if (lineCount > maxLines) {
|
||||
int overflowStart = getLayout().getLineStart(maxLines - 1);
|
||||
CharSequence overflow = getText().subSequence(overflowStart, getText().length());
|
||||
CharSequence ellipsized = TextUtils.ellipsize(overflow, getPaint(), getWidth(), TextUtils.TruncateAt.END);
|
||||
float adjust = overflowText != null ? getPaint().measureText(overflowText, 0, overflowText.length()) : 0f;
|
||||
CharSequence ellipsized = TextUtils.ellipsize(overflow, getPaint(), getWidth() - adjust, TextUtils.TruncateAt.END);
|
||||
|
||||
SpannableStringBuilder newContent = new SpannableStringBuilder();
|
||||
newContent.append(getText().subSequence(0, overflowStart))
|
||||
|
||||
@@ -6,16 +6,17 @@ import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.AppCompatImageButton;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.stickers.StickerKeyboardProvider;
|
||||
import org.thoughtcrime.securesms.keyboard.KeyboardPage;
|
||||
import org.thoughtcrime.securesms.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
public class EmojiToggle extends AppCompatImageButton implements MediaKeyboard.MediaKeyboardListener {
|
||||
|
||||
private Drawable emojiToggle;
|
||||
private Drawable stickerToggle;
|
||||
private Drawable gifToggle;
|
||||
|
||||
private Drawable mediaToggle;
|
||||
private Drawable imeToggle;
|
||||
@@ -45,9 +46,10 @@ public class EmojiToggle extends AppCompatImageButton implements MediaKeyboard.M
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
this.emojiToggle = ContextCompat.getDrawable(getContext(), R.drawable.ic_emoji_smiley_24);
|
||||
this.stickerToggle = ContextCompat.getDrawable(getContext(), R.drawable.ic_sticker_24);
|
||||
this.imeToggle = ContextCompat.getDrawable(getContext(), R.drawable.ic_keyboard_24);
|
||||
this.emojiToggle = ContextUtil.requireDrawable(getContext(), R.drawable.ic_emoji);
|
||||
this.stickerToggle = ContextUtil.requireDrawable(getContext(), R.drawable.ic_sticker_24);
|
||||
this.gifToggle = ContextUtil.requireDrawable(getContext(), R.drawable.ic_gif_24);
|
||||
this.imeToggle = ContextUtil.requireDrawable(getContext(), R.drawable.ic_keyboard_24);
|
||||
this.mediaToggle = emojiToggle;
|
||||
|
||||
setToMedia();
|
||||
@@ -57,8 +59,18 @@ public class EmojiToggle extends AppCompatImageButton implements MediaKeyboard.M
|
||||
drawer.setKeyboardListener(this);
|
||||
}
|
||||
|
||||
public void setStickerMode(boolean stickerMode) {
|
||||
this.mediaToggle = stickerMode ? stickerToggle : emojiToggle;
|
||||
public void setStickerMode(@NonNull KeyboardPage page) {
|
||||
switch (page) {
|
||||
case EMOJI:
|
||||
mediaToggle = emojiToggle;
|
||||
break;
|
||||
case STICKER:
|
||||
mediaToggle = stickerToggle;
|
||||
break;
|
||||
case GIF:
|
||||
mediaToggle = gifToggle;
|
||||
break;
|
||||
}
|
||||
|
||||
if (getDrawable() != imeToggle) {
|
||||
setToMedia();
|
||||
@@ -78,9 +90,18 @@ public class EmojiToggle extends AppCompatImageButton implements MediaKeyboard.M
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKeyboardProviderChanged(@NonNull MediaKeyboardProvider provider) {
|
||||
setStickerMode(provider instanceof StickerKeyboardProvider);
|
||||
TextSecurePreferences.setMediaKeyboardMode(getContext(), (provider instanceof StickerKeyboardProvider) ? TextSecurePreferences.MediaKeyboardMode.STICKER
|
||||
: TextSecurePreferences.MediaKeyboardMode.EMOJI);
|
||||
public void onKeyboardChanged(@NonNull KeyboardPage page) {
|
||||
setStickerMode(page);
|
||||
switch (page) {
|
||||
case EMOJI:
|
||||
TextSecurePreferences.setMediaKeyboardMode(getContext(), TextSecurePreferences.MediaKeyboardMode.EMOJI);
|
||||
break;
|
||||
case STICKER:
|
||||
TextSecurePreferences.setMediaKeyboardMode(getContext(), TextSecurePreferences.MediaKeyboardMode.STICKER);
|
||||
break;
|
||||
case GIF:
|
||||
TextSecurePreferences.setMediaKeyboardMode(getContext(), TextSecurePreferences.MediaKeyboardMode.GIF);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ public final class EmojiUtil {
|
||||
* If the emoji has no skin variations, this function will return the original emoji.
|
||||
*/
|
||||
public static @NonNull String getCanonicalRepresentation(@NonNull String emoji) {
|
||||
String canonical = EmojiSource.getLatest().getVariationMap().get(emoji);
|
||||
String canonical = EmojiSource.getLatest().getVariationsToCanonical().get(emoji);
|
||||
return canonical != null ? canonical : emoji;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider.EmojiEventListener;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -1,50 +1,36 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.InputAwareLayout.InputView;
|
||||
import org.thoughtcrime.securesms.components.RepeatableImageKey;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.keyboard.KeyboardPage;
|
||||
import org.thoughtcrime.securesms.keyboard.KeyboardPagerFragment;
|
||||
import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment;
|
||||
|
||||
import java.util.Arrays;
|
||||
public class MediaKeyboard extends FrameLayout implements InputView {
|
||||
|
||||
public class MediaKeyboard extends FrameLayout implements InputView,
|
||||
MediaKeyboardProvider.Presenter,
|
||||
MediaKeyboardProvider.Controller,
|
||||
MediaKeyboardBottomTabAdapter.EventListener
|
||||
{
|
||||
private static final String TAG = Log.tag(MediaKeyboard.class);
|
||||
private static final String EMOJI_SEARCH = "emoji_search_fragment";
|
||||
|
||||
private static final String TAG = Log.tag(MediaKeyboard.class);
|
||||
|
||||
private RecyclerView categoryTabs;
|
||||
private ViewPager categoryPager;
|
||||
private ViewGroup providerTabs;
|
||||
private RepeatableImageKey backspaceButton;
|
||||
private RepeatableImageKey backspaceButtonBackup;
|
||||
private View searchButton;
|
||||
private View addButton;
|
||||
@Nullable private MediaKeyboardListener keyboardListener;
|
||||
private MediaKeyboardProvider[] providers;
|
||||
private int providerIndex;
|
||||
|
||||
private final boolean tabsAtBottom;
|
||||
|
||||
private MediaKeyboardBottomTabAdapter categoryTabAdapter;
|
||||
@Nullable private MediaKeyboardListener keyboardListener;
|
||||
private boolean isInitialised;
|
||||
private int latestKeyboardHeight;
|
||||
private State keyboardState;
|
||||
private KeyboardPagerFragment keyboardPagerFragment;
|
||||
private FragmentManager fragmentManager;
|
||||
|
||||
public MediaKeyboard(Context context) {
|
||||
this(context, null);
|
||||
@@ -52,23 +38,6 @@ public class MediaKeyboard extends FrameLayout implements InputView,
|
||||
|
||||
public MediaKeyboard(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MediaKeyboard, 0, 0);
|
||||
|
||||
try {
|
||||
tabsAtBottom = typedArray.getInt(R.styleable.MediaKeyboard_tabs_gravity, 0) == 0;
|
||||
} finally {
|
||||
typedArray.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
public void setProviders(int startIndex, MediaKeyboardProvider... providers) {
|
||||
if (!Arrays.equals(this.providers, providers)) {
|
||||
this.providers = providers;
|
||||
this.providerIndex = startIndex;
|
||||
|
||||
requestPresent(providers, providerIndex);
|
||||
}
|
||||
}
|
||||
|
||||
public void setKeyboardListener(@Nullable MediaKeyboardListener listener) {
|
||||
@@ -82,10 +51,12 @@ public class MediaKeyboard extends FrameLayout implements InputView,
|
||||
|
||||
@Override
|
||||
public void show(int height, boolean immediate) {
|
||||
if (this.categoryPager == null) initView();
|
||||
if (!isInitialised) initView();
|
||||
|
||||
latestKeyboardHeight = height;
|
||||
|
||||
ViewGroup.LayoutParams params = getLayoutParams();
|
||||
params.height = height;
|
||||
params.height = (keyboardState == State.NORMAL) ? latestKeyboardHeight : ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
Log.i(TAG, "showing emoji drawer with height " + params.height);
|
||||
setLayoutParams(params);
|
||||
|
||||
@@ -93,205 +64,85 @@ public class MediaKeyboard extends FrameLayout implements InputView,
|
||||
}
|
||||
|
||||
public void show() {
|
||||
if (this.categoryPager == null) initView();
|
||||
if (!isInitialised) initView();
|
||||
|
||||
setVisibility(VISIBLE);
|
||||
if (keyboardListener != null) keyboardListener.onShown();
|
||||
|
||||
requestPresent(providers, providerIndex);
|
||||
keyboardPagerFragment.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hide(boolean immediate) {
|
||||
setVisibility(GONE);
|
||||
onCloseEmojiSearchInternal(false);
|
||||
if (keyboardListener != null) keyboardListener.onHidden();
|
||||
Log.i(TAG, "hide()");
|
||||
keyboardPagerFragment.hide();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void present(@NonNull MediaKeyboardProvider provider,
|
||||
@NonNull PagerAdapter pagerAdapter,
|
||||
@NonNull MediaKeyboardProvider.TabIconProvider tabIconProvider,
|
||||
@Nullable MediaKeyboardProvider.BackspaceObserver backspaceObserver,
|
||||
@Nullable MediaKeyboardProvider.AddObserver addObserver,
|
||||
@Nullable MediaKeyboardProvider.SearchObserver searchObserver,
|
||||
int startingIndex)
|
||||
{
|
||||
if (categoryPager == null) return;
|
||||
if (!provider.equals(providers[providerIndex])) return;
|
||||
if (keyboardListener != null) keyboardListener.onKeyboardProviderChanged(provider);
|
||||
|
||||
boolean isSolo = providers.length == 1;
|
||||
|
||||
presentProviderStrip(isSolo);
|
||||
presentCategoryPager(pagerAdapter, tabIconProvider, startingIndex);
|
||||
presentProviderTabs(providers, providerIndex);
|
||||
presentSearchButton(searchObserver);
|
||||
presentBackspaceButton(backspaceObserver, isSolo);
|
||||
presentAddButton(addObserver);
|
||||
public void onCloseEmojiSearch() {
|
||||
onCloseEmojiSearchInternal(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCurrentPosition() {
|
||||
return categoryPager != null ? categoryPager.getCurrentItem() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestDismissal() {
|
||||
hide(true);
|
||||
providerIndex = 0;
|
||||
if (keyboardListener != null) keyboardListener.onKeyboardProviderChanged(providers[providerIndex]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVisible() {
|
||||
return getVisibility() == View.VISIBLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabSelected(int index) {
|
||||
if (categoryPager != null) {
|
||||
categoryPager.setCurrentItem(index);
|
||||
categoryTabs.smoothScrollToPosition(index);
|
||||
private void onCloseEmojiSearchInternal(boolean showAfterCommit) {
|
||||
if (keyboardState == State.NORMAL) {
|
||||
return;
|
||||
}
|
||||
|
||||
keyboardState = State.NORMAL;
|
||||
|
||||
Fragment emojiSearch = fragmentManager.findFragmentByTag(EMOJI_SEARCH);
|
||||
if (emojiSearch == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
FragmentTransaction transaction = fragmentManager.beginTransaction()
|
||||
.remove(emojiSearch)
|
||||
.show(keyboardPagerFragment)
|
||||
.setCustomAnimations(R.anim.fade_in, R.anim.fade_out);
|
||||
|
||||
if (showAfterCommit) {
|
||||
transaction.runOnCommit(() -> show(latestKeyboardHeight, false));
|
||||
}
|
||||
|
||||
transaction.commitAllowingStateLoss();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setViewPagerEnabled(boolean enabled) {
|
||||
if (categoryPager != null) {
|
||||
categoryPager.setEnabled(enabled);
|
||||
public void onOpenEmojiSearch() {
|
||||
if (keyboardState == State.EMOJI_SEARCH) {
|
||||
return;
|
||||
}
|
||||
|
||||
keyboardState = State.EMOJI_SEARCH;
|
||||
|
||||
fragmentManager.beginTransaction()
|
||||
.hide(keyboardPagerFragment)
|
||||
.add(R.id.media_keyboard_fragment_container, new EmojiSearchFragment(), EMOJI_SEARCH)
|
||||
.runOnCommit(() -> show(latestKeyboardHeight, true))
|
||||
.setCustomAnimations(R.anim.fade_in, R.anim.fade_out)
|
||||
.commitAllowingStateLoss();
|
||||
}
|
||||
|
||||
private void initView() {
|
||||
final View view = LayoutInflater.from(getContext()).inflate(R.layout.media_keyboard, this, true);
|
||||
if (!isInitialised) {
|
||||
LayoutInflater.from(getContext()).inflate(R.layout.media_keyboard, this, true);
|
||||
|
||||
RecyclerView categoryTabsTop = view.findViewById(R.id.media_keyboard_tabs_top);
|
||||
RecyclerView categoryTabsBottom = view.findViewById(R.id.media_keyboard_tabs);
|
||||
|
||||
this.categoryTabs = tabsAtBottom ? categoryTabsBottom : categoryTabsTop;
|
||||
this.categoryPager = view.findViewById(R.id.media_keyboard_pager);
|
||||
this.providerTabs = view.findViewById(R.id.media_keyboard_provider_tabs);
|
||||
this.backspaceButton = view.findViewById(R.id.media_keyboard_backspace);
|
||||
this.backspaceButtonBackup = view.findViewById(R.id.media_keyboard_backspace_backup);
|
||||
this.searchButton = view.findViewById(R.id.media_keyboard_search);
|
||||
this.addButton = view.findViewById(R.id.media_keyboard_add);
|
||||
|
||||
this.categoryTabAdapter = new MediaKeyboardBottomTabAdapter(GlideApp.with(this), this, tabsAtBottom);
|
||||
|
||||
categoryTabs.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false));
|
||||
categoryTabs.setAdapter(categoryTabAdapter);
|
||||
categoryTabs.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
private void requestPresent(@NonNull MediaKeyboardProvider[] providers, int newIndex) {
|
||||
providers[providerIndex].setController(null);
|
||||
providerIndex = newIndex;
|
||||
|
||||
providers[providerIndex].setController(this);
|
||||
providers[providerIndex].requestPresentation(this, providers.length == 1);
|
||||
}
|
||||
|
||||
|
||||
private void presentCategoryPager(@NonNull PagerAdapter pagerAdapter,
|
||||
@NonNull MediaKeyboardProvider.TabIconProvider iconProvider,
|
||||
int startingIndex) {
|
||||
if (categoryPager.getAdapter() != pagerAdapter) {
|
||||
categoryPager.setAdapter(pagerAdapter);
|
||||
keyboardState = State.NORMAL;
|
||||
latestKeyboardHeight = -1;
|
||||
isInitialised = true;
|
||||
fragmentManager = ((FragmentActivity) getContext()).getSupportFragmentManager();
|
||||
keyboardPagerFragment = (KeyboardPagerFragment) fragmentManager.findFragmentById(R.id.media_keyboard_fragment_container);
|
||||
}
|
||||
|
||||
categoryPager.setCurrentItem(startingIndex);
|
||||
|
||||
categoryPager.clearOnPageChangeListeners();
|
||||
categoryPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
|
||||
@Override
|
||||
public void onPageScrolled(int i, float v, int i1) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageSelected(int i) {
|
||||
categoryTabAdapter.setActivePosition(i);
|
||||
categoryTabs.smoothScrollToPosition(i);
|
||||
providers[providerIndex].setCurrentPosition(i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int i) {
|
||||
}
|
||||
});
|
||||
|
||||
categoryTabAdapter.setTabIconProvider(iconProvider, pagerAdapter.getCount());
|
||||
categoryTabAdapter.setActivePosition(startingIndex);
|
||||
}
|
||||
|
||||
private void presentProviderTabs(@NonNull MediaKeyboardProvider[] providers, int selected) {
|
||||
providerTabs.removeAllViews();
|
||||
|
||||
LayoutInflater inflater = LayoutInflater.from(getContext());
|
||||
|
||||
for (int i = 0; i < providers.length; i++) {
|
||||
MediaKeyboardProvider provider = providers[i];
|
||||
View view = inflater.inflate(provider.getProviderIconView(i == selected), providerTabs, false);
|
||||
|
||||
view.setTag(provider);
|
||||
|
||||
final int index = i;
|
||||
view.setOnClickListener(v -> {
|
||||
requestPresent(providers, index);
|
||||
});
|
||||
|
||||
providerTabs.addView(view);
|
||||
}
|
||||
}
|
||||
|
||||
private void presentBackspaceButton(@Nullable MediaKeyboardProvider.BackspaceObserver backspaceObserver,
|
||||
boolean useBackupPosition)
|
||||
{
|
||||
if (backspaceObserver != null) {
|
||||
if (useBackupPosition) {
|
||||
backspaceButton.setVisibility(INVISIBLE);
|
||||
backspaceButton.setOnKeyEventListener(null);
|
||||
backspaceButtonBackup.setVisibility(VISIBLE);
|
||||
backspaceButtonBackup.setOnKeyEventListener(backspaceObserver::onBackspaceClicked);
|
||||
} else {
|
||||
backspaceButton.setVisibility(VISIBLE);
|
||||
backspaceButton.setOnKeyEventListener(backspaceObserver::onBackspaceClicked);
|
||||
backspaceButtonBackup.setVisibility(GONE);
|
||||
backspaceButtonBackup.setOnKeyEventListener(null);
|
||||
}
|
||||
} else {
|
||||
backspaceButton.setVisibility(INVISIBLE);
|
||||
backspaceButton.setOnKeyEventListener(null);
|
||||
backspaceButtonBackup.setVisibility(GONE);
|
||||
backspaceButton.setOnKeyEventListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void presentAddButton(@Nullable MediaKeyboardProvider.AddObserver addObserver) {
|
||||
if (addObserver != null) {
|
||||
addButton.setVisibility(VISIBLE);
|
||||
addButton.setOnClickListener(v -> addObserver.onAddClicked());
|
||||
} else {
|
||||
addButton.setVisibility(GONE);
|
||||
addButton.setOnClickListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void presentSearchButton(@Nullable MediaKeyboardProvider.SearchObserver searchObserver) {
|
||||
searchButton.setVisibility(searchObserver != null ? VISIBLE : INVISIBLE);
|
||||
}
|
||||
|
||||
private void presentProviderStrip(boolean isSolo) {
|
||||
int visibility = isSolo ? View.GONE : View.VISIBLE;
|
||||
|
||||
searchButton.setVisibility(visibility);
|
||||
backspaceButton.setVisibility(visibility);
|
||||
providerTabs.setVisibility(visibility);
|
||||
}
|
||||
|
||||
public interface MediaKeyboardListener {
|
||||
void onShown();
|
||||
void onHidden();
|
||||
void onKeyboardProviderChanged(@NonNull MediaKeyboardProvider provider);
|
||||
void onKeyboardChanged(@NonNull KeyboardPage page);
|
||||
}
|
||||
|
||||
private enum State {
|
||||
NORMAL,
|
||||
EMOJI_SEARCH
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboardProvider.TabIconProvider;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
public class MediaKeyboardBottomTabAdapter extends RecyclerView.Adapter<MediaKeyboardBottomTabAdapter.MediaKeyboardBottomTabViewHolder> {
|
||||
|
||||
private final GlideRequests glideRequests;
|
||||
private final EventListener eventListener;
|
||||
private final boolean highlightTop;
|
||||
|
||||
private TabIconProvider tabIconProvider;
|
||||
private int activePosition;
|
||||
private int count;
|
||||
|
||||
public MediaKeyboardBottomTabAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener, boolean highlightTop) {
|
||||
this.glideRequests = glideRequests;
|
||||
this.eventListener = eventListener;
|
||||
this.highlightTop = highlightTop;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MediaKeyboardBottomTabViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
|
||||
return new MediaKeyboardBottomTabViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.media_keyboard_bottom_tab_item, viewGroup, false),
|
||||
highlightTop);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull MediaKeyboardBottomTabViewHolder viewHolder, int i) {
|
||||
viewHolder.bind(glideRequests, eventListener, tabIconProvider, i, i == activePosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull MediaKeyboardBottomTabViewHolder holder) {
|
||||
holder.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return count;
|
||||
}
|
||||
|
||||
public void setTabIconProvider(@NonNull TabIconProvider iconProvider, int count) {
|
||||
this.tabIconProvider = iconProvider;
|
||||
this.count = count;
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setActivePosition(int position) {
|
||||
this.activePosition = position;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
static class MediaKeyboardBottomTabViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final ImageView image;
|
||||
private final View indicator;
|
||||
|
||||
public MediaKeyboardBottomTabViewHolder(@NonNull View itemView, boolean highlightTop) {
|
||||
super(itemView);
|
||||
|
||||
View indicatorTop = itemView.findViewById(R.id.media_keyboard_top_tab_indicator);
|
||||
View indicatorBottom = itemView.findViewById(R.id.media_keyboard_bottom_tab_indicator);
|
||||
|
||||
this.image = itemView.findViewById(R.id.media_keyboard_bottom_tab_image);
|
||||
this.indicator = highlightTop ? indicatorTop : indicatorBottom;
|
||||
|
||||
this.indicator.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
void bind(@NonNull GlideRequests glideRequests,
|
||||
@NonNull EventListener eventListener,
|
||||
@NonNull TabIconProvider tabIconProvider,
|
||||
int index,
|
||||
boolean selected)
|
||||
{
|
||||
tabIconProvider.loadCategoryTabIcon(glideRequests, image, index);
|
||||
image.setAlpha(selected ? 1 : 0.5f);
|
||||
image.setSelected(selected);
|
||||
|
||||
indicator.setVisibility(selected ? View.VISIBLE : View.INVISIBLE);
|
||||
|
||||
itemView.setOnClickListener(v -> eventListener.onTabSelected(index));
|
||||
}
|
||||
|
||||
void recycle() {
|
||||
itemView.setOnClickListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
interface EventListener {
|
||||
void onTabSelected(int index);
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
public interface MediaKeyboardProvider {
|
||||
@LayoutRes int getProviderIconView(boolean selected);
|
||||
/** @return True if the click was handled with provider-specific logic, otherwise false */
|
||||
void requestPresentation(@NonNull Presenter presenter, boolean isSoloProvider);
|
||||
void setController(@Nullable Controller controller);
|
||||
void setCurrentPosition(int currentPosition);
|
||||
|
||||
interface BackspaceObserver {
|
||||
void onBackspaceClicked();
|
||||
}
|
||||
|
||||
interface AddObserver {
|
||||
void onAddClicked();
|
||||
}
|
||||
|
||||
interface SearchObserver {
|
||||
void onSearchOpened();
|
||||
void onSearchClosed();
|
||||
void onSearchChanged(@NonNull String query);
|
||||
}
|
||||
|
||||
interface Controller {
|
||||
void setViewPagerEnabled(boolean enabled);
|
||||
}
|
||||
|
||||
interface Presenter {
|
||||
void present(@NonNull MediaKeyboardProvider provider,
|
||||
@NonNull PagerAdapter pagerAdapter,
|
||||
@NonNull TabIconProvider iconProvider,
|
||||
@Nullable BackspaceObserver backspaceObserver,
|
||||
@Nullable AddObserver addObserver,
|
||||
@Nullable SearchObserver searchObserver,
|
||||
int startingIndex);
|
||||
int getCurrentPosition();
|
||||
void requestDismissal();
|
||||
boolean isVisible();
|
||||
}
|
||||
|
||||
interface TabIconProvider {
|
||||
void loadCategoryTabIcon(@NonNull GlideRequests glideRequests, @NonNull ImageView imageView, int index);
|
||||
}
|
||||
}
|
||||
@@ -28,11 +28,16 @@ import java.util.List;
|
||||
public class RecentEmojiPageModel implements EmojiPageModel {
|
||||
private static final String TAG = Log.tag(RecentEmojiPageModel.class);
|
||||
private static final int EMOJI_LRU_SIZE = 50;
|
||||
public static final String KEY = "Recents";
|
||||
|
||||
private final SharedPreferences prefs;
|
||||
private final String preferenceName;
|
||||
private final LinkedHashSet<String> recentlyUsed;
|
||||
|
||||
public static boolean hasRecents(Context context, @NonNull String preferenceName) {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context).contains(preferenceName);
|
||||
}
|
||||
|
||||
public RecentEmojiPageModel(Context context, @NonNull String preferenceName) {
|
||||
this.prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
this.preferenceName = preferenceName;
|
||||
@@ -51,6 +56,11 @@ public class RecentEmojiPageModel implements EmojiPageModel {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override public int getIconAttr() {
|
||||
return R.attr.emoji_category_recent;
|
||||
}
|
||||
@@ -96,13 +106,4 @@ public class RecentEmojiPageModel implements EmojiPageModel {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String[] toReversePrimitiveArray(@NonNull LinkedHashSet<String> emojiSet) {
|
||||
String[] emojis = new String[emojiSet.size()];
|
||||
int i = emojiSet.size() - 1;
|
||||
for (String emoji : emojiSet) {
|
||||
emojis[i--] = emoji;
|
||||
}
|
||||
return emojis;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,39 +2,39 @@ package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import org.thoughtcrime.securesms.emoji.EmojiCategory;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class StaticEmojiPageModel implements EmojiPageModel {
|
||||
@AttrRes private final int iconAttr;
|
||||
@NonNull private final List<Emoji> emoji;
|
||||
@Nullable private final Uri sprite;
|
||||
private final @NonNull EmojiCategory category;
|
||||
private final @NonNull List<Emoji> emoji;
|
||||
private final @Nullable Uri sprite;
|
||||
|
||||
public StaticEmojiPageModel(@AttrRes int iconAttr, @NonNull String[] strings, @Nullable Uri sprite) {
|
||||
List<Emoji> emoji = new ArrayList<>(strings.length);
|
||||
for (String s : strings) {
|
||||
emoji.add(new Emoji(Collections.singletonList(s)));
|
||||
}
|
||||
public StaticEmojiPageModel(@NonNull EmojiCategory category, @NonNull String[] strings, @Nullable Uri sprite) {
|
||||
this(category, Arrays.stream(strings).map(s -> new Emoji(Collections.singletonList(s))).collect(Collectors.toList()), sprite);
|
||||
}
|
||||
|
||||
this.iconAttr = iconAttr;
|
||||
this.emoji = emoji;
|
||||
public StaticEmojiPageModel(@NonNull EmojiCategory category, @NonNull List<Emoji> emoji, @Nullable Uri sprite) {
|
||||
this.category = category;
|
||||
this.emoji = Collections.unmodifiableList(emoji);
|
||||
this.sprite = sprite;
|
||||
}
|
||||
|
||||
public StaticEmojiPageModel(@AttrRes int iconAttr, @NonNull List<Emoji> emoji, @Nullable Uri sprite) {
|
||||
this.iconAttr = iconAttr;
|
||||
this.emoji = Collections.unmodifiableList(emoji);
|
||||
this.sprite = sprite;
|
||||
@Override
|
||||
public String getKey() {
|
||||
return category.getKey();
|
||||
}
|
||||
|
||||
public int getIconAttr() {
|
||||
return iconAttr;
|
||||
return category.getIcon();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -8,7 +8,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSessionLock;
|
||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
@@ -43,7 +43,7 @@ public class UntrustedSendDialog extends AlertDialog.Builder implements DialogIn
|
||||
final IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(getContext());
|
||||
|
||||
SimpleTask.run(() -> {
|
||||
try(SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) {
|
||||
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
for (IdentityRecord identityRecord : untrustedRecords) {
|
||||
identityDatabase.setApproval(identityRecord.getRecipientId(), true);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSessionLock;
|
||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
@@ -44,7 +44,7 @@ public class UnverifiedSendDialog extends AlertDialog.Builder implements DialogI
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
try(SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) {
|
||||
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
for (IdentityRecord identityRecord : untrustedRecords) {
|
||||
identityDatabase.setVerified(identityRecord.getRecipientId(),
|
||||
identityRecord.getIdentityKey(),
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package org.thoughtcrime.securesms.components.recyclerview
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
/**
|
||||
* Allows implementor to trigger an animation when the attached recyclerview is
|
||||
* scrolled.
|
||||
*/
|
||||
abstract class OnScrollAnimationHelper : RecyclerView.OnScrollListener() {
|
||||
private var lastAnimationState = AnimationState.NONE
|
||||
|
||||
protected open val duration: Long = 250L
|
||||
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
val newAnimationState = getAnimationState(recyclerView)
|
||||
|
||||
if (newAnimationState == lastAnimationState) {
|
||||
return
|
||||
}
|
||||
|
||||
if (lastAnimationState == AnimationState.NONE) {
|
||||
setImmediateState(recyclerView)
|
||||
return
|
||||
}
|
||||
|
||||
when (newAnimationState) {
|
||||
AnimationState.NONE -> throw AssertionError()
|
||||
AnimationState.HIDE -> hide(duration)
|
||||
AnimationState.SHOW -> show(duration)
|
||||
}
|
||||
|
||||
lastAnimationState = newAnimationState
|
||||
}
|
||||
|
||||
fun setImmediateState(recyclerView: RecyclerView) {
|
||||
val newAnimationState = getAnimationState(recyclerView)
|
||||
|
||||
when (newAnimationState) {
|
||||
AnimationState.NONE -> throw AssertionError()
|
||||
AnimationState.HIDE -> hide(0L)
|
||||
AnimationState.SHOW -> show(0L)
|
||||
}
|
||||
|
||||
lastAnimationState = newAnimationState
|
||||
}
|
||||
|
||||
protected open fun getAnimationState(recyclerView: RecyclerView): AnimationState {
|
||||
return if (recyclerView.canScrollVertically(-1)) AnimationState.SHOW else AnimationState.HIDE
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired when the RecyclerView is able to be scrolled up
|
||||
*/
|
||||
protected abstract fun show(duration: Long)
|
||||
|
||||
/**
|
||||
* Fired when the RecyclerView is not able to be scrolled up
|
||||
*/
|
||||
protected abstract fun hide(duration: Long)
|
||||
|
||||
enum class AnimationState {
|
||||
NONE,
|
||||
HIDE,
|
||||
SHOW
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.thoughtcrime.securesms.components.recyclerview
|
||||
|
||||
import android.view.View
|
||||
|
||||
/**
|
||||
* Animates in and out a given view. This is intended to be used to show and hide a toolbar shadow,
|
||||
* but makes no restrictions in this manner.
|
||||
*/
|
||||
open class ToolbarShadowAnimationHelper(private val toolbarShadow: View) : OnScrollAnimationHelper() {
|
||||
|
||||
override fun show(duration: Long) {
|
||||
toolbarShadow.animate()
|
||||
.setDuration(duration)
|
||||
.alpha(1f)
|
||||
}
|
||||
|
||||
override fun hide(duration: Long) {
|
||||
toolbarShadow.animate()
|
||||
.setDuration(duration)
|
||||
.alpha(0f)
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.reminder;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
/**
|
||||
* Shows a reminder to upgrade a group to GV2.
|
||||
*/
|
||||
public class GroupsV1MigrationInitiationReminder extends Reminder {
|
||||
|
||||
public GroupsV1MigrationInitiationReminder(@NonNull Context context) {
|
||||
super(null, context.getString(R.string.GroupsV1MigrationInitiationReminder_to_access_new_features_like_mentions));
|
||||
addAction(new Action(context.getString(R.string.GroupsV1MigrationInitiationReminder_upgrade_group), R.id.reminder_action_gv1_initiation_update_group));
|
||||
addAction(new Action(context.getResources().getString(R.string.GroupsV1MigrationInitiationReminder_not_now), R.id.reminder_action_gv1_initiation_not_now));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDismissable() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ public class OutdatedBuildReminder extends Reminder {
|
||||
}
|
||||
|
||||
private static CharSequence getPluralsText(final Context context) {
|
||||
int days = getDaysUntilExpiry() - 1;
|
||||
int days = getDaysUntilExpiry();
|
||||
|
||||
if (days == 0) {
|
||||
return context.getString(R.string.OutdatedBuildReminder_your_version_of_signal_will_expire_today);
|
||||
|
||||
@@ -31,7 +31,7 @@ public final class DeviceOrientationMonitor implements DefaultLifecycleObserver
|
||||
private final float[] rotationMatrix = new float[9];
|
||||
private final float[] orientationAngles = new float[3];
|
||||
|
||||
private final MutableLiveData<Orientation> orientation = new MutableLiveData<>();
|
||||
private final MutableLiveData<Orientation> orientation = new MutableLiveData<>(Orientation.PORTRAIT_BOTTOM_EDGE);
|
||||
|
||||
public DeviceOrientationMonitor(@NonNull Context context) {
|
||||
this.sensorManager = ServiceUtil.getSensorManager(context);
|
||||
|
||||
@@ -8,10 +8,11 @@ import androidx.navigation.fragment.NavHostFragment
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
|
||||
open class DSLSettingsActivity : PassphraseRequiredActivity() {
|
||||
|
||||
private val dynamicTheme = DynamicNoActionBarTheme()
|
||||
protected open val dynamicTheme: DynamicTheme = DynamicNoActionBarTheme()
|
||||
|
||||
protected lateinit var navController: NavController
|
||||
private set
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
class DSLSettingsAdapter : MappingAdapter() {
|
||||
init {
|
||||
@@ -42,13 +43,9 @@ abstract class PreferenceViewHolder<T : PreferenceModel<T>>(itemView: View) : Ma
|
||||
it.isEnabled = model.isEnabled
|
||||
}
|
||||
|
||||
if (model.iconId != -1) {
|
||||
iconView.setImageResource(model.iconId)
|
||||
iconView.visibility = View.VISIBLE
|
||||
} else {
|
||||
iconView.setImageDrawable(null)
|
||||
iconView.visibility = View.GONE
|
||||
}
|
||||
val icon = model.icon?.resolve(context)
|
||||
iconView.setImageDrawable(icon)
|
||||
iconView.visible = icon != null
|
||||
|
||||
val title = model.title?.resolve(context)
|
||||
if (title != null) {
|
||||
@@ -93,13 +90,31 @@ class RadioListPreferenceViewHolder(itemView: View) : PreferenceViewHolder<Radio
|
||||
summaryView.text = model.listItems[model.selected]
|
||||
|
||||
itemView.setOnClickListener {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(model.title.resolve(context))
|
||||
var selection = -1
|
||||
val builder = MaterialAlertDialogBuilder(context)
|
||||
.setTitle(model.dialogTitle.resolve(context))
|
||||
.setSingleChoiceItems(model.listItems, model.selected) { dialog, which ->
|
||||
model.onSelected(which)
|
||||
dialog.dismiss()
|
||||
if (model.confirmAction) {
|
||||
selection = which
|
||||
} else {
|
||||
model.onSelected(which)
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
.show()
|
||||
|
||||
if (model.confirmAction) {
|
||||
builder
|
||||
.setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||
model.onSelected(selection)
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
} else {
|
||||
builder.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,21 +12,25 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.recyclerview.OnScrollAnimationHelper
|
||||
import org.thoughtcrime.securesms.components.recyclerview.ToolbarShadowAnimationHelper
|
||||
|
||||
abstract class DSLSettingsFragment(
|
||||
@StringRes private val titleId: Int,
|
||||
@StringRes private val titleId: Int = -1,
|
||||
@MenuRes private val menuId: Int = -1,
|
||||
@LayoutRes layoutId: Int = R.layout.dsl_settings_fragment
|
||||
) : Fragment(layoutId) {
|
||||
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
private lateinit var toolbarShadowHelper: ToolbarShadowHelper
|
||||
private lateinit var scrollAnimationHelper: OnScrollAnimationHelper
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
val toolbarShadow: View = view.findViewById(R.id.toolbar_shadow)
|
||||
|
||||
toolbar.setTitle(titleId)
|
||||
if (titleId != -1) {
|
||||
toolbar.setTitle(titleId)
|
||||
}
|
||||
|
||||
toolbar.setNavigationOnClickListener {
|
||||
requireActivity().onBackPressed()
|
||||
@@ -39,18 +43,17 @@ abstract class DSLSettingsFragment(
|
||||
|
||||
recyclerView = view.findViewById(R.id.recycler)
|
||||
recyclerView.edgeEffectFactory = EdgeEffectFactory()
|
||||
toolbarShadowHelper = ToolbarShadowHelper(toolbarShadow)
|
||||
scrollAnimationHelper = getOnScrollAnimationHelper(toolbarShadow)
|
||||
val adapter = DSLSettingsAdapter()
|
||||
|
||||
recyclerView.adapter = adapter
|
||||
recyclerView.addOnScrollListener(toolbarShadowHelper)
|
||||
recyclerView.addOnScrollListener(scrollAnimationHelper)
|
||||
|
||||
bindAdapter(adapter)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
toolbarShadowHelper.onScrolled(recyclerView, 0, 0)
|
||||
protected open fun getOnScrollAnimationHelper(toolbarShadow: View): OnScrollAnimationHelper {
|
||||
return ToolbarShadowAnimationHelper(toolbarShadow)
|
||||
}
|
||||
|
||||
abstract fun bindAdapter(adapter: DSLSettingsAdapter)
|
||||
@@ -65,32 +68,4 @@ abstract class DSLSettingsFragment(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarShadowHelper(private val toolbarShadow: View) : RecyclerView.OnScrollListener() {
|
||||
|
||||
private var lastAnimationState = ToolbarAnimationState.NONE
|
||||
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
val newAnimationState =
|
||||
if (recyclerView.canScrollVertically(-1)) ToolbarAnimationState.SHOW else ToolbarAnimationState.HIDE
|
||||
|
||||
if (newAnimationState == lastAnimationState) {
|
||||
return
|
||||
}
|
||||
|
||||
when (newAnimationState) {
|
||||
ToolbarAnimationState.NONE -> throw AssertionError()
|
||||
ToolbarAnimationState.HIDE -> toolbarShadow.animate().alpha(0f)
|
||||
ToolbarAnimationState.SHOW -> toolbarShadow.animate().alpha(1f)
|
||||
}
|
||||
|
||||
lastAnimationState = newAnimationState
|
||||
}
|
||||
}
|
||||
|
||||
private enum class ToolbarAnimationState {
|
||||
NONE,
|
||||
HIDE,
|
||||
SHOW
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.thoughtcrime.securesms.components.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
const val NO_TINT = -1
|
||||
|
||||
sealed class DSLSettingsIcon {
|
||||
|
||||
private data class FromResource(
|
||||
@DrawableRes private val iconId: Int,
|
||||
@ColorRes private val iconTintId: Int
|
||||
) : DSLSettingsIcon() {
|
||||
override fun resolve(context: Context) = requireNotNull(ContextCompat.getDrawable(context, iconId)).apply {
|
||||
if (iconTintId != NO_TINT) {
|
||||
colorFilter = PorterDuffColorFilter(ContextCompat.getColor(context, iconTintId), PorterDuff.Mode.SRC_IN)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class FromDrawable(
|
||||
private val drawable: Drawable
|
||||
) : DSLSettingsIcon() {
|
||||
override fun resolve(context: Context): Drawable = drawable
|
||||
}
|
||||
|
||||
abstract fun resolve(context: Context): Drawable
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun from(@DrawableRes iconId: Int, @ColorRes iconTintId: Int = R.color.signal_icon_tint_primary): DSLSettingsIcon = FromResource(iconId, iconTintId)
|
||||
|
||||
@JvmStatic
|
||||
fun from(drawable: Drawable): DSLSettingsIcon = FromDrawable(drawable)
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,7 @@ class AppSettingsActivity : DSLSettingsActivity() {
|
||||
|
||||
@JvmStatic
|
||||
fun help(context: Context, startCategoryIndex: Int = 0): Intent {
|
||||
return getIntentForStartLocation(context, StartLocation.HOME)
|
||||
return getIntentForStartLocation(context, StartLocation.HELP)
|
||||
.putExtra(HelpFragment.START_CATEGORY_INDEX, startCategoryIndex)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceViewHolder
|
||||
@@ -44,7 +45,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.AccountSettingsFragment__account),
|
||||
iconId = R.drawable.ic_profile_circle_24,
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_profile_circle_24),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_accountSettingsFragment)
|
||||
}
|
||||
@@ -52,7 +53,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__linked_devices),
|
||||
iconId = R.drawable.ic_linked_devices_24,
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_linked_devices_24),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_deviceActivity)
|
||||
}
|
||||
@@ -72,7 +73,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__appearance),
|
||||
iconId = R.drawable.ic_appearance_24,
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_appearance_24),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_appearanceSettingsFragment)
|
||||
}
|
||||
@@ -80,7 +81,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences_chats__chats),
|
||||
iconId = R.drawable.ic_message_tinted_bitmap_24,
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_message_tinted_bitmap_24),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_chatsSettingsFragment)
|
||||
}
|
||||
@@ -88,7 +89,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__notifications),
|
||||
iconId = R.drawable.ic_bell_24,
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_bell_24),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_notificationsSettingsFragment)
|
||||
}
|
||||
@@ -96,7 +97,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__privacy),
|
||||
iconId = R.drawable.ic_lock_24,
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_lock_24),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_privacySettingsFragment)
|
||||
}
|
||||
@@ -104,7 +105,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__data_and_storage),
|
||||
iconId = R.drawable.ic_archive_24dp,
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_archive_24dp),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_dataAndStorageSettingsFragment)
|
||||
}
|
||||
@@ -114,7 +115,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__help),
|
||||
iconId = R.drawable.ic_help_24,
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_help_24),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_helpSettingsFragment)
|
||||
}
|
||||
@@ -122,7 +123,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.AppSettingsFragment__invite_your_friends),
|
||||
iconId = R.drawable.ic_invite_24,
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_invite_24),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_inviteActivity)
|
||||
}
|
||||
@@ -130,7 +131,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
|
||||
externalLinkPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__donate_to_signal),
|
||||
iconId = R.drawable.ic_heart_24,
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_heart_24),
|
||||
linkId = R.string.donate_url
|
||||
)
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ class AppearanceSettingsFragment : DSLSettingsFragment(R.string.preferences__app
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__chat_wallpaper),
|
||||
title = DSLSettingsText.from(R.string.preferences__chat_color_and_wallpaper),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appearanceSettings_to_wallpaperActivity)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.settings.app.appearance
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
@@ -28,6 +29,7 @@ class AppearanceSettingsViewModel : ViewModel() {
|
||||
fun setLanguage(language: String) {
|
||||
store.update { it.copy(language = language) }
|
||||
SignalStore.settings().language = language
|
||||
EmojiSearchIndexDownloadJob.scheduleImmediately()
|
||||
}
|
||||
|
||||
fun setMessageFontSize(size: Int) {
|
||||
|
||||
@@ -8,12 +8,14 @@ import android.widget.Toast
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob
|
||||
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob
|
||||
@@ -174,7 +176,7 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
summary = DSLSettingsText.from(R.string.preferences__internal_force_censorship_description),
|
||||
isChecked = state.forceCensorship,
|
||||
onClick = {
|
||||
viewModel.setDisableAutoMigrationNotification(!state.forceCensorship)
|
||||
viewModel.setForceCensorship(!state.forceCensorship)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -212,6 +214,69 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
viewModel.setDisableAutoMigrationNotification(!state.useBuiltInEmojiSet)
|
||||
}
|
||||
)
|
||||
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.preferences__internal_sender_key)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_clear_all_state),
|
||||
summary = DSLSettingsText.from(R.string.preferences__internal_click_to_delete_all_sender_key_state),
|
||||
onClick = {
|
||||
clearAllSenderKeyState()
|
||||
}
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_clear_shared_state),
|
||||
summary = DSLSettingsText.from(R.string.preferences__internal_click_to_delete_all_sharing_state),
|
||||
onClick = {
|
||||
clearAllSenderKeySharedState()
|
||||
}
|
||||
)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_remove_two_person_minimum),
|
||||
summary = DSLSettingsText.from(R.string.preferences__internal_remove_the_requirement_that_you_need),
|
||||
isChecked = state.removeSenderKeyMinimium,
|
||||
onClick = {
|
||||
viewModel.setRemoveSenderKeyMinimum(!state.removeSenderKeyMinimium)
|
||||
}
|
||||
)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_delay_resends),
|
||||
summary = DSLSettingsText.from(R.string.preferences__internal_delay_resending_messages_in_response_to_retry_receipts),
|
||||
isChecked = state.delayResends,
|
||||
onClick = {
|
||||
viewModel.setDelayResends(!state.delayResends)
|
||||
}
|
||||
)
|
||||
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.preferences__internal_calling)
|
||||
|
||||
radioPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_calling_default),
|
||||
summary = DSLSettingsText.from(BuildConfig.SIGNAL_SFU_URL),
|
||||
isChecked = state.callingServer == BuildConfig.SIGNAL_SFU_URL,
|
||||
onClick = {
|
||||
viewModel.setInternalGroupCallingServer(null)
|
||||
}
|
||||
)
|
||||
|
||||
BuildConfig.SIGNAL_SFU_INTERNAL_NAMES.zip(BuildConfig.SIGNAL_SFU_INTERNAL_URLS)
|
||||
.forEach { (name, server) ->
|
||||
radioPref(
|
||||
title = DSLSettingsText.from(requireContext().getString(R.string.preferences__internal_calling_s_server, name)),
|
||||
summary = DSLSettingsText.from(server),
|
||||
isChecked = state.callingServer == server,
|
||||
onClick = {
|
||||
viewModel.setInternalGroupCallingServer(server)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,4 +343,15 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
ConversationUtil.clearAllShortcuts(requireContext())
|
||||
Toast.makeText(context, "Deleted all dynamic shortcuts.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun clearAllSenderKeyState() {
|
||||
DatabaseFactory.getSenderKeyDatabase(requireContext()).deleteAll()
|
||||
DatabaseFactory.getSenderKeySharedDatabase(requireContext()).deleteAll()
|
||||
Toast.makeText(context, "Deleted all sender key state.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun clearAllSenderKeySharedState() {
|
||||
DatabaseFactory.getSenderKeySharedDatabase(requireContext()).deleteAll()
|
||||
Toast.makeText(context, "Deleted all sender key shared state.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ data class InternalSettingsState(
|
||||
val disableAutoMigrationInitiation: Boolean,
|
||||
val disableAutoMigrationNotification: Boolean,
|
||||
val forceCensorship: Boolean,
|
||||
val callingServer: String,
|
||||
val useBuiltInEmojiSet: Boolean,
|
||||
val emojiVersion: EmojiFiles.Version?
|
||||
val emojiVersion: EmojiFiles.Version?,
|
||||
val removeSenderKeyMinimium: Boolean,
|
||||
val delayResends: Boolean,
|
||||
)
|
||||
|
||||
@@ -65,6 +65,21 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setRemoveSenderKeyMinimum(enabled: Boolean) {
|
||||
preferenceDataStore.putBoolean(InternalValues.REMOVE_SENDER_KEY_MINIMUM, enabled)
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setDelayResends(enabled: Boolean) {
|
||||
preferenceDataStore.putBoolean(InternalValues.DELAY_RESENDS, enabled)
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setInternalGroupCallingServer(server: String?) {
|
||||
preferenceDataStore.putString(InternalValues.CALLING_SERVER, server)
|
||||
refresh()
|
||||
}
|
||||
|
||||
private fun refresh() {
|
||||
store.update { getState().copy(emojiVersion = it.emojiVersion) }
|
||||
}
|
||||
@@ -78,8 +93,11 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
disableAutoMigrationInitiation = SignalStore.internalValues().disableGv1AutoMigrateInitiation(),
|
||||
disableAutoMigrationNotification = SignalStore.internalValues().disableGv1AutoMigrateNotification(),
|
||||
forceCensorship = SignalStore.internalValues().forcedCensorship(),
|
||||
callingServer = SignalStore.internalValues().groupCallingServer(),
|
||||
useBuiltInEmojiSet = SignalStore.internalValues().forceBuiltInEmoji(),
|
||||
emojiVersion = null
|
||||
emojiVersion = null,
|
||||
removeSenderKeyMinimium = SignalStore.internalValues().removeSenderKeyMinimum(),
|
||||
delayResends = SignalStore.internalValues().delayResends()
|
||||
)
|
||||
|
||||
class Factory(private val repository: InternalSettingsRepository) : ViewModelProvider.Factory {
|
||||
|
||||
@@ -234,7 +234,7 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
|
||||
} else {
|
||||
val tone = RingtoneUtil.getRingtone(requireContext(), uri)
|
||||
if (tone != null) {
|
||||
tone.getTitle(requireContext())
|
||||
tone.getTitle(requireContext()) ?: getString(R.string.NotificationsSettingsFragment__unknown_ringtone)
|
||||
} else {
|
||||
getString(R.string.preferences__default)
|
||||
}
|
||||
@@ -289,7 +289,7 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
|
||||
val radioListPreference: RadioListPreference
|
||||
) : PreferenceModel<LedColorPreference>(
|
||||
title = radioListPreference.title,
|
||||
iconId = radioListPreference.iconId,
|
||||
icon = radioListPreference.icon,
|
||||
summary = radioListPreference.summary
|
||||
) {
|
||||
override fun areContentsTheSame(newItem: LedColorPreference): Boolean {
|
||||
|
||||
@@ -430,7 +430,7 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
|
||||
) : PreferenceModel<ValueClickPreference>(
|
||||
title = clickPreference.title,
|
||||
summary = clickPreference.summary,
|
||||
iconId = clickPreference.iconId,
|
||||
icon = clickPreference.icon,
|
||||
isEnabled = clickPreference.isEnabled
|
||||
) {
|
||||
override fun areContentsTheSame(newItem: ValueClickPreference): Boolean {
|
||||
|
||||
@@ -3,9 +3,12 @@ package org.thoughtcrime.securesms.components.settings.app.privacy.advanced
|
||||
import android.app.ProgressDialog
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
@@ -98,16 +101,28 @@ class AdvancedPrivacySettingsFragment : DSLSettingsFragment(R.string.preferences
|
||||
isChecked = state.isPushEnabled
|
||||
) {
|
||||
if (state.isPushEnabled) {
|
||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setIcon(R.drawable.ic_info_outline)
|
||||
setTitle(R.string.ApplicationPreferencesActivity_disable_signal_messages_and_calls)
|
||||
val builder = MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setMessage(R.string.ApplicationPreferencesActivity_disable_signal_messages_and_calls_by_unregistering)
|
||||
setNegativeButton(android.R.string.cancel, null)
|
||||
setPositiveButton(
|
||||
android.R.string.ok
|
||||
) { _, _ -> viewModel.disablePushMessages() }
|
||||
show()
|
||||
}
|
||||
|
||||
val icon: Drawable = requireNotNull(ContextCompat.getDrawable(builder.context, R.drawable.ic_info_outline))
|
||||
icon.setBounds(0, 0, ViewUtil.dpToPx(32), ViewUtil.dpToPx(32))
|
||||
|
||||
val title = TextView(builder.context)
|
||||
val padding = ViewUtil.dpToPx(16)
|
||||
title.setText(R.string.ApplicationPreferencesActivity_disable_signal_messages_and_calls)
|
||||
title.setPadding(padding, padding, padding, padding)
|
||||
title.compoundDrawablePadding = padding / 2
|
||||
TextViewCompat.setTextAppearance(title, R.style.TextAppearance_Signal_Title2_MaterialDialog)
|
||||
TextViewCompat.setCompoundDrawablesRelative(title, icon, null, null, null)
|
||||
|
||||
builder
|
||||
.setCustomTitle(title)
|
||||
.show()
|
||||
} else {
|
||||
startActivity(RegistrationNavigationActivity.newIntentForReRegistration(requireContext()))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.privacy.advanced
|
||||
|
||||
import android.content.Context
|
||||
import com.google.firebase.iid.FirebaseInstanceId
|
||||
import com.google.android.gms.tasks.Tasks
|
||||
import com.google.firebase.installations.FirebaseInstallations
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
@@ -14,6 +15,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.ExecutionException
|
||||
|
||||
private val TAG = Log.tag(AdvancedPrivacySettingsRepository::class.java)
|
||||
|
||||
@@ -29,12 +31,18 @@ class AdvancedPrivacySettingsRepository(private val context: Context) {
|
||||
Log.w(TAG, e)
|
||||
}
|
||||
if (!TextSecurePreferences.isFcmDisabled(context)) {
|
||||
FirebaseInstanceId.getInstance().deleteInstanceId()
|
||||
Tasks.await(FirebaseInstallations.getInstance().delete())
|
||||
}
|
||||
DisablePushMessagesResult.SUCCESS
|
||||
} catch (ioe: IOException) {
|
||||
Log.w(TAG, ioe)
|
||||
DisablePushMessagesResult.NETWORK_ERROR
|
||||
} catch (e: InterruptedException) {
|
||||
Log.w(TAG, "Interrupted while deleting", e)
|
||||
DisablePushMessagesResult.NETWORK_ERROR
|
||||
} catch (e: ExecutionException) {
|
||||
Log.w(TAG, "Error deleting", e.cause)
|
||||
DisablePushMessagesResult.NETWORK_ERROR
|
||||
}
|
||||
|
||||
consumer(result)
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.util.Pair
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.ParcelableGroupId
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.DynamicConversationSettingsTheme
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
|
||||
class ConversationSettingsActivity : DSLSettingsActivity(), ConversationSettingsFragment.Callback {
|
||||
|
||||
override val dynamicTheme: DynamicTheme = DynamicConversationSettingsTheme()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
ActivityCompat.postponeEnterTransition(this)
|
||||
super.onCreate(savedInstanceState, ready)
|
||||
}
|
||||
|
||||
override fun onContentWillRender() {
|
||||
ActivityCompat.startPostponedEnterTransition(this)
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
super.finish()
|
||||
overridePendingTransition(0, R.anim.slide_fade_to_bottom)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun createTransitionBundle(context: Context, avatar: View, windowContent: View): Bundle? {
|
||||
return if (context is Activity) {
|
||||
ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||
context,
|
||||
Pair.create(avatar, "avatar"),
|
||||
Pair.create(windowContent, "window_content")
|
||||
).toBundle()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun createTransitionBundle(context: Context, avatar: View): Bundle? {
|
||||
return if (context is Activity) {
|
||||
ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||
context,
|
||||
avatar,
|
||||
"avatar",
|
||||
).toBundle()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun forGroup(context: Context, groupId: GroupId): Intent {
|
||||
val startBundle = ConversationSettingsFragmentArgs.Builder(null, ParcelableGroupId.from(groupId))
|
||||
.build()
|
||||
.toBundle()
|
||||
|
||||
return getIntent(context)
|
||||
.putExtra(ARG_START_BUNDLE, startBundle)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun forRecipient(context: Context, recipientId: RecipientId): Intent {
|
||||
val startBundle = ConversationSettingsFragmentArgs.Builder(recipientId, null)
|
||||
.build()
|
||||
.toBundle()
|
||||
|
||||
return getIntent(context)
|
||||
.putExtra(ARG_START_BUNDLE, startBundle)
|
||||
}
|
||||
|
||||
private fun getIntent(context: Context): Intent {
|
||||
return Intent(context, ConversationSettingsActivity::class.java)
|
||||
.putExtra(ARG_NAV_GRAPH, R.navigation.conversation_settings)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation
|
||||
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
sealed class ConversationSettingsEvent {
|
||||
class AddToAGroup(
|
||||
val recipientId: RecipientId,
|
||||
val groupMembership: List<RecipientId>
|
||||
) : ConversationSettingsEvent()
|
||||
|
||||
class AddMembersToGroup(
|
||||
val groupId: GroupId,
|
||||
val selectionWarning: Int,
|
||||
val selectionLimit: Int,
|
||||
val groupMembersWithoutSelf: List<RecipientId>
|
||||
) : ConversationSettingsEvent()
|
||||
|
||||
object ShowGroupHardLimitDialog : ConversationSettingsEvent()
|
||||
|
||||
class ShowAddMembersToGroupError(
|
||||
val failureReason: GroupChangeFailureReason
|
||||
) : ConversationSettingsEvent()
|
||||
|
||||
class ShowGroupInvitesSentDialog(
|
||||
val invitesSentTo: List<Recipient>
|
||||
) : ConversationSettingsEvent()
|
||||
|
||||
class ShowMembersAdded(
|
||||
val membersAddedCount: Int
|
||||
) : ConversationSettingsEvent()
|
||||
|
||||
class InitiateGroupMigration(
|
||||
val recipientId: RecipientId
|
||||
) : ConversationSettingsEvent()
|
||||
}
|
||||
@@ -0,0 +1,770 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.doOnPreDraw
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import app.cash.exhaustive.Exhaustive
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.thoughtcrime.securesms.AvatarPreviewActivity
|
||||
import org.thoughtcrime.securesms.BlockUnblockDialog
|
||||
import org.thoughtcrime.securesms.InviteActivity
|
||||
import org.thoughtcrime.securesms.MediaPreviewActivity
|
||||
import org.thoughtcrime.securesms.MuteDialog
|
||||
import org.thoughtcrime.securesms.PushContactSelectionActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.VerifyIdentityActivity
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.components.recyclerview.OnScrollAnimationHelper
|
||||
import org.thoughtcrime.securesms.components.recyclerview.ToolbarShadowAnimationHelper
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.NO_TINT
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.AvatarPreference
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.BioTextPreference
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.GroupDescriptionPreference
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.InternalPreference
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.LargeIconClickPreference
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.LegacyGroupPreference
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.RecipientPreference
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.SharedMediaPreference
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.Utils.formatMutedUntil
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents
|
||||
import org.thoughtcrime.securesms.groups.ParcelableGroupId
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupErrors
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog
|
||||
import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog
|
||||
import org.thoughtcrime.securesms.groups.ui.addmembers.AddMembersActivity
|
||||
import org.thoughtcrime.securesms.groups.ui.addtogroup.AddToGroupsActivity
|
||||
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupDescriptionDialog
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupInviteSentDialog
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupsLearnMoreBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInitiationBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity
|
||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientExporter
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.recipients.ui.sharablegrouplink.ShareableGroupLinkDialogFragment
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.ContextUtil
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperActivity
|
||||
|
||||
private const val REQUEST_CODE_VIEW_CONTACT = 1
|
||||
private const val REQUEST_CODE_ADD_CONTACT = 2
|
||||
private const val REQUEST_CODE_ADD_MEMBERS_TO_GROUP = 3
|
||||
private const val REQUEST_CODE_RETURN_FROM_MEDIA = 4
|
||||
|
||||
class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
layoutId = R.layout.conversation_settings_fragment,
|
||||
menuId = R.menu.conversation_settings
|
||||
) {
|
||||
|
||||
private val alertTint by lazy { ContextCompat.getColor(requireContext(), R.color.signal_alert_primary) }
|
||||
private val blockIcon by lazy {
|
||||
ContextUtil.requireDrawable(requireContext(), R.drawable.ic_block_tinted_24).apply {
|
||||
colorFilter = PorterDuffColorFilter(alertTint, PorterDuff.Mode.SRC_IN)
|
||||
}
|
||||
}
|
||||
|
||||
private val unblockIcon by lazy {
|
||||
ContextUtil.requireDrawable(requireContext(), R.drawable.ic_block_tinted_24)
|
||||
}
|
||||
|
||||
private val leaveIcon by lazy {
|
||||
ContextUtil.requireDrawable(requireContext(), R.drawable.ic_leave_tinted_24).apply {
|
||||
colorFilter = PorterDuffColorFilter(alertTint, PorterDuff.Mode.SRC_IN)
|
||||
}
|
||||
}
|
||||
|
||||
private val viewModel by viewModels<ConversationSettingsViewModel>(
|
||||
factoryProducer = {
|
||||
val args = ConversationSettingsFragmentArgs.fromBundle(requireArguments())
|
||||
val groupId = args.groupId as? ParcelableGroupId
|
||||
|
||||
ConversationSettingsViewModel.Factory(
|
||||
recipientId = args.recipientId,
|
||||
groupId = ParcelableGroupId.get(groupId),
|
||||
repository = ConversationSettingsRepository(requireContext())
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
private lateinit var callback: Callback
|
||||
|
||||
private lateinit var toolbar: Toolbar
|
||||
private lateinit var toolbarAvatar: AvatarImageView
|
||||
private lateinit var toolbarTitle: TextView
|
||||
private lateinit var toolbarBackground: View
|
||||
|
||||
private val navController get() = Navigation.findNavController(requireView())
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
|
||||
callback = context as Callback
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbarAvatar = view.findViewById(R.id.toolbar_avatar)
|
||||
toolbarTitle = view.findViewById(R.id.toolbar_title)
|
||||
toolbarBackground = view.findViewById(R.id.toolbar_background)
|
||||
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when (requestCode) {
|
||||
REQUEST_CODE_ADD_MEMBERS_TO_GROUP -> if (data != null) {
|
||||
val selected: List<RecipientId> = requireNotNull(data.getParcelableArrayListExtra(PushContactSelectionActivity.KEY_SELECTED_RECIPIENTS))
|
||||
val progress: SimpleProgressDialog.DismissibleDialog = SimpleProgressDialog.showDelayed(requireContext())
|
||||
|
||||
viewModel.onAddToGroupComplete(selected) {
|
||||
progress.dismiss()
|
||||
}
|
||||
}
|
||||
REQUEST_CODE_RETURN_FROM_MEDIA -> viewModel.refreshSharedMedia()
|
||||
REQUEST_CODE_ADD_CONTACT -> viewModel.refreshRecipient()
|
||||
REQUEST_CODE_VIEW_CONTACT -> viewModel.refreshRecipient()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getOnScrollAnimationHelper(toolbarShadow: View): OnScrollAnimationHelper {
|
||||
return ConversationSettingsOnUserScrolledAnimationHelper(toolbarAvatar, toolbarTitle, toolbarBackground, toolbarShadow)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return if (item.itemId == R.id.action_edit) {
|
||||
val args = ConversationSettingsFragmentArgs.fromBundle(requireArguments())
|
||||
val groupId = args.groupId as ParcelableGroupId
|
||||
|
||||
startActivity(EditProfileActivity.getIntentForGroupProfile(requireActivity(), requireNotNull(ParcelableGroupId.get(groupId))))
|
||||
true
|
||||
} else {
|
||||
super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
BioTextPreference.register(adapter)
|
||||
AvatarPreference.register(adapter)
|
||||
ButtonStripPreference.register(adapter)
|
||||
LargeIconClickPreference.register(adapter)
|
||||
SharedMediaPreference.register(adapter)
|
||||
RecipientPreference.register(adapter)
|
||||
InternalPreference.register(adapter)
|
||||
GroupDescriptionPreference.register(adapter)
|
||||
LegacyGroupPreference.register(adapter)
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
|
||||
if (state.recipient != Recipient.UNKNOWN) {
|
||||
toolbarAvatar.buildOptions()
|
||||
.withQuickContactEnabled(false)
|
||||
.withUseSelfProfileAvatar(false)
|
||||
.withFixedSize(ViewUtil.dpToPx(80))
|
||||
.load(state.recipient)
|
||||
|
||||
state.withRecipientSettingsState {
|
||||
toolbarTitle.text = state.recipient.getDisplayName(requireContext())
|
||||
}
|
||||
|
||||
state.withGroupSettingsState {
|
||||
toolbarTitle.text = it.groupTitle
|
||||
toolbar.menu.findItem(R.id.action_edit).isVisible = it.canEditGroupAttributes
|
||||
}
|
||||
}
|
||||
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList()) {
|
||||
if (state.isLoaded) {
|
||||
(view?.parent as? ViewGroup)?.doOnPreDraw {
|
||||
callback.onContentWillRender()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.events.observe(viewLifecycleOwner) { event ->
|
||||
@Exhaustive
|
||||
when (event) {
|
||||
is ConversationSettingsEvent.AddToAGroup -> handleAddToAGroup(event)
|
||||
is ConversationSettingsEvent.AddMembersToGroup -> handleAddMembersToGroup(event)
|
||||
ConversationSettingsEvent.ShowGroupHardLimitDialog -> showGroupHardLimitDialog()
|
||||
is ConversationSettingsEvent.ShowAddMembersToGroupError -> showAddMembersToGroupError(event)
|
||||
is ConversationSettingsEvent.ShowGroupInvitesSentDialog -> showGroupInvitesSentDialog(event)
|
||||
is ConversationSettingsEvent.ShowMembersAdded -> showMembersAdded(event)
|
||||
is ConversationSettingsEvent.InitiateGroupMigration -> GroupsV1MigrationInitiationBottomSheetDialogFragment.showForInitiation(parentFragmentManager, event.recipientId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: ConversationSettingsState): DSLConfiguration {
|
||||
return configure {
|
||||
if (state.recipient == Recipient.UNKNOWN) {
|
||||
return@configure
|
||||
}
|
||||
|
||||
customPref(
|
||||
AvatarPreference.Model(
|
||||
recipient = state.recipient,
|
||||
onAvatarClick = { avatar ->
|
||||
requireActivity().apply {
|
||||
startActivity(
|
||||
AvatarPreviewActivity.intentFromRecipientId(this, state.recipient.id),
|
||||
AvatarPreviewActivity.createTransitionBundle(this, avatar)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
state.withRecipientSettingsState {
|
||||
customPref(BioTextPreference.RecipientModel(recipient = state.recipient))
|
||||
}
|
||||
|
||||
state.withGroupSettingsState { groupState ->
|
||||
|
||||
val groupMembershipDescription = if (groupState.groupId.isV1) {
|
||||
String.format("%s · %s", groupState.membershipCountDescription, getString(R.string.ManageGroupActivity_legacy_group))
|
||||
} else if (!groupState.canEditGroupAttributes && groupState.groupDescription.isNullOrEmpty()) {
|
||||
groupState.membershipCountDescription
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
customPref(
|
||||
BioTextPreference.GroupModel(
|
||||
groupTitle = groupState.groupTitle,
|
||||
groupMembershipDescription = groupMembershipDescription
|
||||
)
|
||||
)
|
||||
|
||||
if (groupState.groupId.isV2) {
|
||||
customPref(
|
||||
GroupDescriptionPreference.Model(
|
||||
groupId = groupState.groupId,
|
||||
groupDescription = groupState.groupDescription,
|
||||
descriptionShouldLinkify = groupState.groupDescriptionShouldLinkify,
|
||||
canEditGroupAttributes = groupState.canEditGroupAttributes,
|
||||
onEditGroupDescription = {
|
||||
startActivity(EditProfileActivity.getIntentForGroupProfile(requireActivity(), groupState.groupId))
|
||||
},
|
||||
onViewGroupDescription = {
|
||||
GroupDescriptionDialog.show(childFragmentManager, groupState.groupId, null, groupState.groupDescriptionShouldLinkify)
|
||||
}
|
||||
)
|
||||
)
|
||||
} else if (groupState.legacyGroupState != LegacyGroupPreference.State.NONE) {
|
||||
customPref(
|
||||
LegacyGroupPreference.Model(
|
||||
state = groupState.legacyGroupState,
|
||||
onLearnMoreClick = { GroupsLearnMoreBottomSheetDialogFragment.show(parentFragmentManager) },
|
||||
onUpgradeClick = { viewModel.initiateGroupUpgrade() },
|
||||
onMmsWarningClick = { startActivity(Intent(requireContext(), InviteActivity::class.java)) }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
state.withRecipientSettingsState { recipientState ->
|
||||
if (recipientState.displayInternalRecipientDetails) {
|
||||
customPref(
|
||||
InternalPreference.Model(
|
||||
recipient = state.recipient,
|
||||
onDisableProfileSharingClick = {
|
||||
viewModel.disableProfileSharing()
|
||||
},
|
||||
onDeleteSessionClick = {
|
||||
viewModel.deleteSession()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
customPref(
|
||||
ButtonStripPreference.Model(
|
||||
state = state.buttonStripState,
|
||||
onVideoClick = {
|
||||
CommunicationActions.startVideoCall(requireActivity(), state.recipient)
|
||||
},
|
||||
onAudioClick = {
|
||||
CommunicationActions.startVoiceCall(requireActivity(), state.recipient)
|
||||
},
|
||||
onMuteClick = {
|
||||
if (!state.buttonStripState.isMuted) {
|
||||
MuteDialog.show(requireContext(), viewModel::setMuteUntil)
|
||||
} else {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(state.recipient.muteUntil.formatMutedUntil(requireContext()))
|
||||
.setPositiveButton(R.string.ConversationSettingsFragment__unmute) { dialog, _ ->
|
||||
viewModel.unmute()
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.dismiss() }
|
||||
.show()
|
||||
}
|
||||
},
|
||||
onSearchClick = {
|
||||
val intent = ConversationIntents.createBuilder(requireContext(), state.recipient.id, state.threadId)
|
||||
.withSearchOpen(true)
|
||||
.build()
|
||||
|
||||
startActivity(intent)
|
||||
requireActivity().finish()
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
dividerPref()
|
||||
|
||||
val summary = DSLSettingsText.from(formatDisappearingMessagesLifespan(state.disappearingMessagesLifespan))
|
||||
val icon = if (state.disappearingMessagesLifespan <= 0) {
|
||||
R.drawable.ic_update_timer_disabled_16
|
||||
} else {
|
||||
R.drawable.ic_update_timer_16
|
||||
}
|
||||
|
||||
var enabled = true
|
||||
state.withGroupSettingsState {
|
||||
enabled = it.canEditGroupAttributes
|
||||
}
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__disappearing_messages),
|
||||
summary = summary,
|
||||
icon = DSLSettingsIcon.from(icon),
|
||||
isEnabled = enabled,
|
||||
onClick = {
|
||||
val action = ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToAppSettingsExpireTimer()
|
||||
.setInitialValue(state.disappearingMessagesLifespan)
|
||||
.setRecipientId(state.recipient.id)
|
||||
.setForResultMode(false)
|
||||
|
||||
navController.navigate(action)
|
||||
}
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__chat_color_and_wallpaper),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_color_24),
|
||||
onClick = {
|
||||
startActivity(ChatWallpaperActivity.createIntent(requireContext(), state.recipient.id))
|
||||
}
|
||||
)
|
||||
|
||||
if (!state.recipient.isSelf) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__sounds_and_notifications),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_speaker_24),
|
||||
onClick = {
|
||||
val action = ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToSoundsAndNotificationsSettingsFragment(state.recipient.id)
|
||||
|
||||
navController.navigate(action)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
state.withRecipientSettingsState { recipientState ->
|
||||
when (recipientState.contactLinkState) {
|
||||
ContactLinkState.OPEN -> {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__contact_details),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_profile_circle_24),
|
||||
onClick = {
|
||||
startActivityForResult(Intent(Intent.ACTION_VIEW, state.recipient.contactUri), REQUEST_CODE_VIEW_CONTACT)
|
||||
}
|
||||
)
|
||||
}
|
||||
ContactLinkState.ADD -> {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__add_as_a_contact),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_plus_24),
|
||||
onClick = {
|
||||
startActivityForResult(RecipientExporter.export(state.recipient).asAddContactIntent(), REQUEST_CODE_ADD_CONTACT)
|
||||
}
|
||||
)
|
||||
}
|
||||
ContactLinkState.NONE -> {
|
||||
}
|
||||
}
|
||||
|
||||
if (recipientState.identityRecord != null) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__view_safety_number),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_safety_number_24),
|
||||
onClick = {
|
||||
startActivity(VerifyIdentityActivity.newIntent(requireActivity(), recipientState.identityRecord))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.sharedMedia != null && state.sharedMedia.count > 0) {
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.recipient_preference_activity__shared_media)
|
||||
|
||||
customPref(
|
||||
SharedMediaPreference.Model(
|
||||
mediaCursor = state.sharedMedia,
|
||||
mediaIds = state.sharedMediaIds,
|
||||
onMediaRecordClick = { mediaRecord, isLtr ->
|
||||
startActivityForResult(
|
||||
MediaPreviewActivity.intentFromMediaRecord(requireContext(), mediaRecord, isLtr),
|
||||
REQUEST_CODE_RETURN_FROM_MEDIA
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__see_all),
|
||||
onClick = {
|
||||
startActivity(MediaOverviewActivity.forThread(requireContext(), state.threadId))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
state.withRecipientSettingsState { groupState ->
|
||||
if (groupState.selfHasGroups) {
|
||||
|
||||
dividerPref()
|
||||
|
||||
val groupsInCommonCount = groupState.allGroupsInCommon.size
|
||||
sectionHeaderPref(
|
||||
DSLSettingsText.from(
|
||||
if (groupsInCommonCount == 0) {
|
||||
getString(R.string.ManageRecipientActivity_no_groups_in_common)
|
||||
} else {
|
||||
resources.getQuantityString(
|
||||
R.plurals.ManageRecipientActivity_d_groups_in_common,
|
||||
groupsInCommonCount,
|
||||
groupsInCommonCount
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
customPref(
|
||||
LargeIconClickPreference.Model(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__add_to_a_group),
|
||||
icon = DSLSettingsIcon.from(R.drawable.add_to_a_group, NO_TINT),
|
||||
onClick = {
|
||||
viewModel.onAddToGroup()
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
for (group in groupState.groupsInCommon) {
|
||||
customPref(
|
||||
RecipientPreference.Model(
|
||||
recipient = group,
|
||||
onClick = {
|
||||
CommunicationActions.startConversation(requireActivity(), group, null)
|
||||
requireActivity().finish()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (groupState.canShowMoreGroupsInCommon) {
|
||||
customPref(
|
||||
LargeIconClickPreference.Model(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__see_all),
|
||||
icon = DSLSettingsIcon.from(R.drawable.show_more, NO_TINT),
|
||||
onClick = {
|
||||
viewModel.revealAllMembers()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.withGroupSettingsState { groupState ->
|
||||
val memberCount = groupState.allMembers.size
|
||||
|
||||
if (groupState.canAddToGroup || memberCount > 0) {
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(DSLSettingsText.from(resources.getQuantityString(R.plurals.ContactSelectionListFragment_d_members, memberCount, memberCount)))
|
||||
}
|
||||
|
||||
if (groupState.canAddToGroup) {
|
||||
customPref(
|
||||
LargeIconClickPreference.Model(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__add_members),
|
||||
icon = DSLSettingsIcon.from(R.drawable.add_to_a_group, NO_TINT),
|
||||
onClick = {
|
||||
viewModel.onAddToGroup()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
for (member in groupState.members) {
|
||||
customPref(
|
||||
RecipientPreference.Model(
|
||||
recipient = member.member,
|
||||
isAdmin = member.isAdmin,
|
||||
onClick = {
|
||||
RecipientBottomSheetDialogFragment.create(member.member.id, groupState.groupId).show(parentFragmentManager, "BOTTOM")
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (groupState.canShowMoreGroupMembers) {
|
||||
customPref(
|
||||
LargeIconClickPreference.Model(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__see_all),
|
||||
icon = DSLSettingsIcon.from(R.drawable.show_more, NO_TINT),
|
||||
onClick = {
|
||||
viewModel.revealAllMembers()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (state.recipient.isPushV2Group) {
|
||||
dividerPref()
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__group_link),
|
||||
summary = DSLSettingsText.from(if (groupState.groupLinkEnabled) R.string.preferences_on else R.string.preferences_off),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_link_16),
|
||||
onClick = {
|
||||
ShareableGroupLinkDialogFragment.create(groupState.groupId.requireV2()).show(parentFragmentManager, "DIALOG")
|
||||
}
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__requests_and_invites),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_update_group_add_16),
|
||||
onClick = {
|
||||
startActivity(ManagePendingAndRequestingMembersActivity.newIntent(requireContext(), groupState.groupId.requireV2()))
|
||||
}
|
||||
)
|
||||
|
||||
if (groupState.isSelfAdmin) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__permissions),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_lock_24),
|
||||
onClick = {
|
||||
val action = ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToPermissionsSettingsFragment(ParcelableGroupId.from(groupState.groupId))
|
||||
navController.navigate(action)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (groupState.canLeave) {
|
||||
dividerPref()
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.conversation__menu_leave_group, alertTint),
|
||||
icon = DSLSettingsIcon.from(leaveIcon),
|
||||
onClick = {
|
||||
LeaveGroupDialog.handleLeavePushGroup(requireActivity(), groupState.groupId.requirePush(), null)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.canModifyBlockedState) {
|
||||
state.withRecipientSettingsState {
|
||||
dividerPref()
|
||||
}
|
||||
|
||||
state.withGroupSettingsState {
|
||||
if (!it.canLeave) {
|
||||
dividerPref()
|
||||
}
|
||||
}
|
||||
|
||||
val isBlocked = state.recipient.isBlocked
|
||||
val isGroup = state.recipient.isPushGroup
|
||||
|
||||
val title = when {
|
||||
isBlocked && isGroup -> R.string.ConversationSettingsFragment__unblock_group
|
||||
isBlocked -> R.string.ConversationSettingsFragment__unblock
|
||||
isGroup -> R.string.ConversationSettingsFragment__block_group
|
||||
else -> R.string.ConversationSettingsFragment__block
|
||||
}
|
||||
|
||||
val titleTint = if (isBlocked) null else alertTint
|
||||
val blockUnblockIcon = if (isBlocked) unblockIcon else blockIcon
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(title, titleTint),
|
||||
icon = DSLSettingsIcon.from(blockUnblockIcon),
|
||||
onClick = {
|
||||
if (state.recipient.isBlocked) {
|
||||
BlockUnblockDialog.showUnblockFor(requireContext(), viewLifecycleOwner.lifecycle, state.recipient) {
|
||||
viewModel.unblock()
|
||||
}
|
||||
} else {
|
||||
BlockUnblockDialog.showBlockFor(requireContext(), viewLifecycleOwner.lifecycle, state.recipient) {
|
||||
viewModel.block()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatDisappearingMessagesLifespan(disappearingMessagesLifespan: Int): String {
|
||||
return if (disappearingMessagesLifespan <= 0) {
|
||||
getString(R.string.preferences_off)
|
||||
} else {
|
||||
ExpirationUtil.getExpirationDisplayValue(requireContext(), disappearingMessagesLifespan)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAddToAGroup(addToAGroup: ConversationSettingsEvent.AddToAGroup) {
|
||||
startActivity(AddToGroupsActivity.newIntent(requireContext(), addToAGroup.recipientId, addToAGroup.groupMembership))
|
||||
}
|
||||
|
||||
private fun handleAddMembersToGroup(addMembersToGroup: ConversationSettingsEvent.AddMembersToGroup) {
|
||||
startActivityForResult(
|
||||
AddMembersActivity.createIntent(
|
||||
requireContext(),
|
||||
addMembersToGroup.groupId,
|
||||
ContactsCursorLoader.DisplayMode.FLAG_PUSH,
|
||||
addMembersToGroup.selectionWarning,
|
||||
addMembersToGroup.selectionLimit,
|
||||
addMembersToGroup.groupMembersWithoutSelf
|
||||
),
|
||||
REQUEST_CODE_ADD_MEMBERS_TO_GROUP
|
||||
)
|
||||
}
|
||||
|
||||
private fun showGroupHardLimitDialog() {
|
||||
GroupLimitDialog.showHardLimitMessage(requireContext())
|
||||
}
|
||||
|
||||
private fun showAddMembersToGroupError(showAddMembersToGroupError: ConversationSettingsEvent.ShowAddMembersToGroupError) {
|
||||
Toast.makeText(requireContext(), GroupErrors.getUserDisplayMessage(showAddMembersToGroupError.failureReason), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
private fun showGroupInvitesSentDialog(showGroupInvitesSentDialog: ConversationSettingsEvent.ShowGroupInvitesSentDialog) {
|
||||
GroupInviteSentDialog.showInvitesSent(requireContext(), showGroupInvitesSentDialog.invitesSentTo)
|
||||
}
|
||||
|
||||
private fun showMembersAdded(showMembersAdded: ConversationSettingsEvent.ShowMembersAdded) {
|
||||
val string = resources.getQuantityString(
|
||||
R.plurals.ManageGroupActivity_added,
|
||||
showMembersAdded.membersAddedCount,
|
||||
showMembersAdded.membersAddedCount
|
||||
)
|
||||
|
||||
Snackbar.make(requireView(), string, Snackbar.LENGTH_SHORT).setTextColor(Color.WHITE).show()
|
||||
}
|
||||
|
||||
private class ConversationSettingsOnUserScrolledAnimationHelper(
|
||||
private val toolbarAvatar: View,
|
||||
private val toolbarTitle: View,
|
||||
private val toolbarBackground: View,
|
||||
toolbarShadow: View
|
||||
) : ToolbarShadowAnimationHelper(toolbarShadow) {
|
||||
|
||||
override val duration: Long = 200L
|
||||
|
||||
private val actionBarSize = ThemeUtil.getThemedDimen(toolbarShadow.context, R.attr.actionBarSize)
|
||||
private val rect = Rect()
|
||||
|
||||
override fun getAnimationState(recyclerView: RecyclerView): AnimationState {
|
||||
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
|
||||
|
||||
return if (layoutManager.findFirstVisibleItemPosition() == 0) {
|
||||
val firstChild = requireNotNull(layoutManager.getChildAt(0))
|
||||
firstChild.getLocalVisibleRect(rect)
|
||||
|
||||
if (rect.height() <= actionBarSize) {
|
||||
AnimationState.SHOW
|
||||
} else {
|
||||
AnimationState.HIDE
|
||||
}
|
||||
} else {
|
||||
AnimationState.SHOW
|
||||
}
|
||||
}
|
||||
|
||||
override fun show(duration: Long) {
|
||||
super.show(duration)
|
||||
|
||||
toolbarAvatar
|
||||
.animate()
|
||||
.setDuration(duration)
|
||||
.translationY(0f)
|
||||
.alpha(1f)
|
||||
|
||||
toolbarTitle
|
||||
.animate()
|
||||
.setDuration(duration)
|
||||
.translationY(0f)
|
||||
.alpha(1f)
|
||||
|
||||
toolbarBackground
|
||||
.animate()
|
||||
.setDuration(duration)
|
||||
.alpha(1f)
|
||||
}
|
||||
|
||||
override fun hide(duration: Long) {
|
||||
super.hide(duration)
|
||||
|
||||
toolbarAvatar
|
||||
.animate()
|
||||
.setDuration(duration)
|
||||
.translationY(ViewUtil.dpToPx(56).toFloat())
|
||||
.alpha(0f)
|
||||
|
||||
toolbarTitle
|
||||
.animate()
|
||||
.setDuration(duration)
|
||||
.translationY(ViewUtil.dpToPx(56).toFloat())
|
||||
.alpha(0f)
|
||||
|
||||
toolbarBackground
|
||||
.animate()
|
||||
.setDuration(duration)
|
||||
.alpha(0f)
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onContentWillRender()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.lifecycle.LiveData
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
import org.thoughtcrime.securesms.groups.GroupProtoUtil
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import org.whispersystems.libsignal.util.guava.Preconditions
|
||||
import java.io.IOException
|
||||
|
||||
private val TAG = Log.tag(ConversationSettingsRepository::class.java)
|
||||
|
||||
class ConversationSettingsRepository(
|
||||
private val context: Context
|
||||
) {
|
||||
|
||||
@WorkerThread
|
||||
fun getThreadMedia(threadId: Long): Optional<Cursor> {
|
||||
return if (threadId <= 0) {
|
||||
Optional.absent()
|
||||
} else {
|
||||
Optional.of(DatabaseFactory.getMediaDatabase(context).getGalleryMediaForThread(threadId, MediaDatabase.Sorting.Newest))
|
||||
}
|
||||
}
|
||||
|
||||
fun getThreadId(recipientId: RecipientId, consumer: (Long) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
consumer(DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipientId))
|
||||
}
|
||||
}
|
||||
|
||||
fun getThreadId(groupId: GroupId, consumer: (Long) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val recipientId = Recipient.externalGroupExact(context, groupId).id
|
||||
consumer(DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipientId))
|
||||
}
|
||||
}
|
||||
|
||||
fun isInternalRecipientDetailsEnabled(): Boolean = SignalStore.internalValues().recipientDetails()
|
||||
|
||||
fun hasGroups(consumer: (Boolean) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute { consumer(DatabaseFactory.getGroupDatabase(context).activeGroupCount > 0) }
|
||||
}
|
||||
|
||||
fun getIdentity(recipientId: RecipientId, consumer: (IdentityDatabase.IdentityRecord?) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
consumer(
|
||||
DatabaseFactory.getIdentityDatabase(context)
|
||||
.getIdentity(recipientId)
|
||||
.orNull()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getGroupsInCommon(recipientId: RecipientId, consumer: (List<Recipient>) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
consumer(
|
||||
DatabaseFactory
|
||||
.getGroupDatabase(context)
|
||||
.getPushGroupsContainingMember(recipientId)
|
||||
.asSequence()
|
||||
.filter { it.members.contains(Recipient.self().id) }
|
||||
.map(GroupDatabase.GroupRecord::getRecipientId)
|
||||
.map(Recipient::resolved)
|
||||
.sortedBy { gr -> gr.getDisplayName(context) }
|
||||
.toList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getGroupMembership(recipientId: RecipientId, consumer: (List<RecipientId>) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val groupDatabase = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupRecords = groupDatabase.getPushGroupsContainingMember(recipientId)
|
||||
val groupRecipients = ArrayList<RecipientId>(groupRecords.size)
|
||||
for (groupRecord in groupRecords) {
|
||||
groupRecipients.add(groupRecord.recipientId)
|
||||
}
|
||||
consumer(groupRecipients)
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshRecipient(recipientId: RecipientId) {
|
||||
SignalExecutors.UNBOUNDED.execute {
|
||||
try {
|
||||
DirectoryHelper.refreshDirectoryFor(context, Recipient.resolved(recipientId), false)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Failed to refresh user after adding to contacts.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setMuteUntil(recipientId: RecipientId, until: Long) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
DatabaseFactory.getRecipientDatabase(context).setMuted(recipientId, until)
|
||||
}
|
||||
}
|
||||
|
||||
fun getGroupCapacity(groupId: GroupId, consumer: (GroupCapacityResult) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val groupRecord: GroupDatabase.GroupRecord = DatabaseFactory.getGroupDatabase(context).getGroup(groupId).get()
|
||||
consumer(
|
||||
if (groupRecord.isV2Group) {
|
||||
val decryptedGroup: DecryptedGroup = groupRecord.requireV2GroupProperties().decryptedGroup
|
||||
val pendingMembers: List<RecipientId> = decryptedGroup.pendingMembersList
|
||||
.map(DecryptedPendingMember::getUuid)
|
||||
.map(GroupProtoUtil::uuidByteStringToRecipientId)
|
||||
|
||||
val members = mutableListOf<RecipientId>()
|
||||
|
||||
members.addAll(groupRecord.members)
|
||||
members.addAll(pendingMembers)
|
||||
|
||||
GroupCapacityResult(Recipient.self().id, members, FeatureFlags.groupLimits())
|
||||
} else {
|
||||
GroupCapacityResult(Recipient.self().id, groupRecord.members, FeatureFlags.groupLimits())
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun addMembers(groupId: GroupId, selected: List<RecipientId>, consumer: (GroupAddMembersResult) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
consumer(
|
||||
try {
|
||||
val groupActionResult = GroupManager.addMembers(context, groupId.requirePush(), selected)
|
||||
GroupAddMembersResult.Success(groupActionResult.addedMemberCount, Recipient.resolvedList(groupActionResult.invitedMembers))
|
||||
} catch (e: Exception) {
|
||||
GroupAddMembersResult.Failure(GroupChangeFailureReason.fromException(e))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setMuteUntil(groupId: GroupId, until: Long) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val recipientId = Recipient.externalGroupExact(context, groupId).id
|
||||
DatabaseFactory.getRecipientDatabase(context).setMuted(recipientId, until)
|
||||
}
|
||||
}
|
||||
|
||||
fun block(recipientId: RecipientId) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
RecipientUtil.blockNonGroup(context, recipient)
|
||||
}
|
||||
}
|
||||
|
||||
fun unblock(recipientId: RecipientId) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
RecipientUtil.unblock(context, recipient)
|
||||
}
|
||||
}
|
||||
|
||||
fun block(groupId: GroupId) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val recipient = Recipient.externalGroupExact(context, groupId)
|
||||
RecipientUtil.block(context, recipient)
|
||||
}
|
||||
}
|
||||
|
||||
fun unblock(groupId: GroupId) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val recipient = Recipient.externalGroupExact(context, groupId)
|
||||
RecipientUtil.unblock(context, recipient)
|
||||
}
|
||||
}
|
||||
|
||||
fun disableProfileSharingForInternalUser(recipientId: RecipientId) {
|
||||
Preconditions.checkArgument(FeatureFlags.internalUser(), "Internal users only!")
|
||||
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipientId, false)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteSessionForInternalUser(recipientId: RecipientId) {
|
||||
Preconditions.checkArgument(FeatureFlags.internalUser(), "Internal users only!")
|
||||
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
DatabaseFactory.getSessionDatabase(context).deleteAllFor(recipientId)
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun isMessageRequestAccepted(recipient: Recipient): Boolean {
|
||||
return RecipientUtil.isMessageRequestAccepted(context, recipient)
|
||||
}
|
||||
|
||||
fun getMembershipCountDescription(liveGroup: LiveGroup): LiveData<String> {
|
||||
return liveGroup.getMembershipCountDescription(context.resources)
|
||||
}
|
||||
|
||||
fun getExternalPossiblyMigratedGroupRecipientId(groupId: GroupId, consumer: (RecipientId) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
consumer(Recipient.externalPossiblyMigratedGroup(context, groupId).id)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation
|
||||
|
||||
import android.database.Cursor
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.LegacyGroupPreference
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
data class ConversationSettingsState(
|
||||
val threadId: Long = -1,
|
||||
val recipient: Recipient = Recipient.UNKNOWN,
|
||||
val buttonStripState: ButtonStripPreference.State = ButtonStripPreference.State(),
|
||||
val disappearingMessagesLifespan: Int = 0,
|
||||
val canModifyBlockedState: Boolean = false,
|
||||
val sharedMedia: Cursor? = null,
|
||||
val sharedMediaIds: List<Long> = listOf(),
|
||||
private val sharedMediaLoaded: Boolean = false,
|
||||
private val specificSettingsState: SpecificSettingsState,
|
||||
) {
|
||||
|
||||
val isLoaded: Boolean = recipient != Recipient.UNKNOWN && sharedMediaLoaded && specificSettingsState.isLoaded
|
||||
|
||||
fun withRecipientSettingsState(consumer: (SpecificSettingsState.RecipientSettingsState) -> Unit) {
|
||||
if (specificSettingsState is SpecificSettingsState.RecipientSettingsState) {
|
||||
consumer(specificSettingsState)
|
||||
}
|
||||
}
|
||||
|
||||
fun withGroupSettingsState(consumer: (SpecificSettingsState.GroupSettingsState) -> Unit) {
|
||||
if (specificSettingsState is SpecificSettingsState.GroupSettingsState) {
|
||||
consumer(specificSettingsState)
|
||||
}
|
||||
}
|
||||
|
||||
fun requireRecipientSettingsState(): SpecificSettingsState.RecipientSettingsState = specificSettingsState.requireRecipientSettingsState()
|
||||
fun requireGroupSettingsState(): SpecificSettingsState.GroupSettingsState = specificSettingsState.requireGroupSettingsState()
|
||||
}
|
||||
|
||||
sealed class SpecificSettingsState {
|
||||
|
||||
abstract val isLoaded: Boolean
|
||||
|
||||
data class RecipientSettingsState(
|
||||
val identityRecord: IdentityDatabase.IdentityRecord? = null,
|
||||
val allGroupsInCommon: List<Recipient> = listOf(),
|
||||
val groupsInCommon: List<Recipient> = listOf(),
|
||||
val selfHasGroups: Boolean = false,
|
||||
val canShowMoreGroupsInCommon: Boolean = false,
|
||||
val groupsInCommonExpanded: Boolean = false,
|
||||
val contactLinkState: ContactLinkState = ContactLinkState.NONE,
|
||||
val displayInternalRecipientDetails: Boolean
|
||||
) : SpecificSettingsState() {
|
||||
|
||||
override val isLoaded: Boolean = true
|
||||
|
||||
override fun requireRecipientSettingsState() = this
|
||||
}
|
||||
|
||||
data class GroupSettingsState(
|
||||
val groupId: GroupId,
|
||||
val allMembers: List<GroupMemberEntry.FullMember> = listOf(),
|
||||
val members: List<GroupMemberEntry.FullMember> = listOf(),
|
||||
val isSelfAdmin: Boolean = false,
|
||||
val canAddToGroup: Boolean = false,
|
||||
val canEditGroupAttributes: Boolean = false,
|
||||
val canLeave: Boolean = false,
|
||||
val canShowMoreGroupMembers: Boolean = false,
|
||||
val groupMembersExpanded: Boolean = false,
|
||||
val groupTitle: String = "",
|
||||
private val groupTitleLoaded: Boolean = false,
|
||||
val groupDescription: String? = null,
|
||||
val groupDescriptionShouldLinkify: Boolean = false,
|
||||
private val groupDescriptionLoaded: Boolean = false,
|
||||
val groupLinkEnabled: Boolean = false,
|
||||
val membershipCountDescription: String = "",
|
||||
val legacyGroupState: LegacyGroupPreference.State = LegacyGroupPreference.State.NONE
|
||||
) : SpecificSettingsState() {
|
||||
|
||||
override val isLoaded: Boolean = groupTitleLoaded && groupDescriptionLoaded
|
||||
|
||||
override fun requireGroupSettingsState(): GroupSettingsState = this
|
||||
}
|
||||
|
||||
open fun requireRecipientSettingsState(): RecipientSettingsState = error("Not a recipient settings state")
|
||||
open fun requireGroupSettingsState(): GroupSettingsState = error("Not a group settings state")
|
||||
}
|
||||
|
||||
enum class ContactLinkState {
|
||||
OPEN,
|
||||
ADD,
|
||||
NONE
|
||||
}
|
||||
@@ -0,0 +1,480 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation
|
||||
|
||||
import android.database.Cursor
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.LegacyGroupPreference
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil
|
||||
import org.thoughtcrime.securesms.util.CursorUtil
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
|
||||
sealed class ConversationSettingsViewModel(
|
||||
private val repository: ConversationSettingsRepository,
|
||||
specificSettingsState: SpecificSettingsState,
|
||||
) : ViewModel() {
|
||||
|
||||
private val openedMediaCursors = HashSet<Cursor>()
|
||||
|
||||
@Volatile
|
||||
private var cleared = false
|
||||
|
||||
protected val store = Store(
|
||||
ConversationSettingsState(
|
||||
specificSettingsState = specificSettingsState
|
||||
)
|
||||
)
|
||||
protected val internalEvents = SingleLiveEvent<ConversationSettingsEvent>()
|
||||
|
||||
private val sharedMediaUpdateTrigger = MutableLiveData(Unit)
|
||||
|
||||
val state: LiveData<ConversationSettingsState> = store.stateLiveData
|
||||
val events: LiveData<ConversationSettingsEvent> = internalEvents
|
||||
|
||||
init {
|
||||
val threadId: LiveData<Long> = Transformations.distinctUntilChanged(Transformations.map(state) { it.threadId })
|
||||
val updater: LiveData<Long> = LiveDataUtil.combineLatest(threadId, sharedMediaUpdateTrigger) { tId, _ -> tId }
|
||||
|
||||
val sharedMedia: LiveData<Optional<Cursor>> = LiveDataUtil.mapAsync(SignalExecutors.BOUNDED, updater) { tId ->
|
||||
repository.getThreadMedia(tId)
|
||||
}
|
||||
|
||||
store.update(sharedMedia) { cursor, state ->
|
||||
if (!cleared) {
|
||||
if (cursor.isPresent) {
|
||||
openedMediaCursors.add(cursor.get())
|
||||
}
|
||||
|
||||
val ids: List<Long> = cursor.transform<List<Long>> {
|
||||
val result = mutableListOf<Long>()
|
||||
while (it.moveToNext()) {
|
||||
result.add(CursorUtil.requireLong(it, AttachmentDatabase.ROW_ID))
|
||||
}
|
||||
result
|
||||
}.or(listOf())
|
||||
|
||||
state.copy(
|
||||
sharedMedia = cursor.orNull(),
|
||||
sharedMediaIds = ids,
|
||||
sharedMediaLoaded = true
|
||||
)
|
||||
} else {
|
||||
cursor.orNull().ensureClosed()
|
||||
state.copy(sharedMedia = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshSharedMedia() {
|
||||
sharedMediaUpdateTrigger.postValue(Unit)
|
||||
}
|
||||
|
||||
open fun refreshRecipient(): Unit = error("This ViewModel does not support this interaction")
|
||||
|
||||
abstract fun setMuteUntil(muteUntil: Long)
|
||||
|
||||
abstract fun unmute()
|
||||
|
||||
abstract fun block()
|
||||
|
||||
abstract fun unblock()
|
||||
|
||||
abstract fun onAddToGroup()
|
||||
|
||||
abstract fun onAddToGroupComplete(selected: List<RecipientId>, onComplete: () -> Unit)
|
||||
|
||||
abstract fun revealAllMembers()
|
||||
|
||||
override fun onCleared() {
|
||||
cleared = true
|
||||
store.update { state ->
|
||||
openedMediaCursors.forEach { it.ensureClosed() }
|
||||
state.copy(sharedMedia = null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Cursor?.ensureClosed() {
|
||||
if (this != null && !this.isClosed) {
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
|
||||
open fun disableProfileSharing(): Unit = error("This ViewModel does not support this interaction")
|
||||
|
||||
open fun deleteSession(): Unit = error("This ViewModel does not support this interaction")
|
||||
|
||||
open fun initiateGroupUpgrade(): Unit = error("This ViewModel does not support this interaction")
|
||||
|
||||
private class RecipientSettingsViewModel(
|
||||
private val recipientId: RecipientId,
|
||||
private val repository: ConversationSettingsRepository
|
||||
) : ConversationSettingsViewModel(
|
||||
repository,
|
||||
SpecificSettingsState.RecipientSettingsState(
|
||||
displayInternalRecipientDetails = repository.isInternalRecipientDetailsEnabled()
|
||||
)
|
||||
) {
|
||||
|
||||
private val liveRecipient = Recipient.live(recipientId)
|
||||
|
||||
init {
|
||||
store.update(liveRecipient.liveData) { recipient, state ->
|
||||
state.copy(
|
||||
recipient = recipient,
|
||||
buttonStripState = ButtonStripPreference.State(
|
||||
isVideoAvailable = recipient.registered == RecipientDatabase.RegisteredState.REGISTERED && !recipient.isSelf,
|
||||
isAudioAvailable = !recipient.isGroup && !recipient.isSelf,
|
||||
isAudioSecure = recipient.registered == RecipientDatabase.RegisteredState.REGISTERED,
|
||||
isMuted = recipient.isMuted,
|
||||
isMuteAvailable = !recipient.isSelf,
|
||||
isSearchAvailable = true
|
||||
),
|
||||
disappearingMessagesLifespan = recipient.expireMessages,
|
||||
canModifyBlockedState = !recipient.isSelf,
|
||||
specificSettingsState = state.requireRecipientSettingsState().copy(
|
||||
contactLinkState = when {
|
||||
recipient.isSelf -> ContactLinkState.NONE
|
||||
recipient.isSystemContact -> ContactLinkState.OPEN
|
||||
else -> ContactLinkState.ADD
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
repository.getThreadId(recipientId) { threadId ->
|
||||
store.update { state ->
|
||||
state.copy(threadId = threadId)
|
||||
}
|
||||
}
|
||||
|
||||
if (recipientId != Recipient.self().id) {
|
||||
repository.getGroupsInCommon(recipientId) { groupsInCommon ->
|
||||
store.update { state ->
|
||||
val recipientSettings = state.requireRecipientSettingsState()
|
||||
val canShowMore = !recipientSettings.groupsInCommonExpanded && groupsInCommon.size > 6
|
||||
|
||||
state.copy(
|
||||
specificSettingsState = recipientSettings.copy(
|
||||
allGroupsInCommon = groupsInCommon,
|
||||
groupsInCommon = if (!canShowMore) groupsInCommon else groupsInCommon.take(5),
|
||||
canShowMoreGroupsInCommon = canShowMore
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
repository.hasGroups { hasGroups ->
|
||||
store.update { state ->
|
||||
val recipientSettings = state.requireRecipientSettingsState()
|
||||
state.copy(
|
||||
specificSettingsState = recipientSettings.copy(
|
||||
selfHasGroups = hasGroups
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
repository.getIdentity(recipientId) { identityRecord ->
|
||||
store.update { state ->
|
||||
state.copy(specificSettingsState = state.requireRecipientSettingsState().copy(identityRecord = identityRecord))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddToGroup() {
|
||||
repository.getGroupMembership(recipientId) {
|
||||
internalEvents.postValue(ConversationSettingsEvent.AddToAGroup(recipientId, it))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddToGroupComplete(selected: List<RecipientId>, onComplete: () -> Unit) {
|
||||
}
|
||||
|
||||
override fun revealAllMembers() {
|
||||
store.update { state ->
|
||||
state.copy(
|
||||
specificSettingsState = state.requireRecipientSettingsState().copy(
|
||||
groupsInCommon = state.requireRecipientSettingsState().allGroupsInCommon,
|
||||
groupsInCommonExpanded = true,
|
||||
canShowMoreGroupsInCommon = false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun refreshRecipient() {
|
||||
repository.refreshRecipient(recipientId)
|
||||
}
|
||||
|
||||
override fun setMuteUntil(muteUntil: Long) {
|
||||
repository.setMuteUntil(recipientId, muteUntil)
|
||||
}
|
||||
|
||||
override fun unmute() {
|
||||
repository.setMuteUntil(recipientId, 0)
|
||||
}
|
||||
|
||||
override fun block() {
|
||||
repository.block(recipientId)
|
||||
}
|
||||
|
||||
override fun unblock() {
|
||||
repository.unblock(recipientId)
|
||||
}
|
||||
|
||||
override fun disableProfileSharing() {
|
||||
repository.disableProfileSharingForInternalUser(recipientId)
|
||||
}
|
||||
|
||||
override fun deleteSession() {
|
||||
repository.deleteSessionForInternalUser(recipientId)
|
||||
}
|
||||
}
|
||||
|
||||
private class GroupSettingsViewModel(
|
||||
private val groupId: GroupId,
|
||||
private val repository: ConversationSettingsRepository
|
||||
) : ConversationSettingsViewModel(repository, SpecificSettingsState.GroupSettingsState(groupId)) {
|
||||
|
||||
private val liveGroup = LiveGroup(groupId)
|
||||
|
||||
init {
|
||||
store.update(liveGroup.groupRecipient) { recipient, state ->
|
||||
state.copy(
|
||||
recipient = recipient,
|
||||
buttonStripState = ButtonStripPreference.State(
|
||||
isVideoAvailable = recipient.isPushV2Group,
|
||||
isAudioAvailable = false,
|
||||
isAudioSecure = recipient.isPushV2Group,
|
||||
isMuted = recipient.isMuted,
|
||||
isMuteAvailable = true,
|
||||
isSearchAvailable = true
|
||||
),
|
||||
canModifyBlockedState = RecipientUtil.isBlockable(recipient),
|
||||
specificSettingsState = state.requireGroupSettingsState().copy(
|
||||
legacyGroupState = getLegacyGroupState(recipient)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
repository.getThreadId(groupId) { threadId ->
|
||||
store.update { state ->
|
||||
state.copy(threadId = threadId)
|
||||
}
|
||||
}
|
||||
|
||||
store.update(liveGroup.selfCanEditGroupAttributes()) { selfCanEditGroupAttributes, state ->
|
||||
state.copy(
|
||||
specificSettingsState = state.requireGroupSettingsState().copy(
|
||||
canEditGroupAttributes = selfCanEditGroupAttributes
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
store.update(liveGroup.isSelfAdmin) { isSelfAdmin, state ->
|
||||
state.copy(
|
||||
specificSettingsState = state.requireGroupSettingsState().copy(
|
||||
isSelfAdmin = isSelfAdmin
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
store.update(liveGroup.expireMessages) { expireMessages, state ->
|
||||
state.copy(
|
||||
disappearingMessagesLifespan = expireMessages
|
||||
)
|
||||
}
|
||||
|
||||
store.update(liveGroup.selfCanAddMembers()) { canAddMembers, state ->
|
||||
state.copy(
|
||||
specificSettingsState = state.requireGroupSettingsState().copy(
|
||||
canAddToGroup = canAddMembers
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
store.update(liveGroup.fullMembers) { fullMembers, state ->
|
||||
val groupState = state.requireGroupSettingsState()
|
||||
val canShowMore = !groupState.groupMembersExpanded && fullMembers.size > 6
|
||||
|
||||
state.copy(
|
||||
specificSettingsState = groupState.copy(
|
||||
allMembers = fullMembers,
|
||||
members = if (!canShowMore) fullMembers else fullMembers.take(5),
|
||||
canShowMoreGroupMembers = canShowMore
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val isMessageRequestAccepted: LiveData<Boolean> = LiveDataUtil.mapAsync(liveGroup.groupRecipient) { r -> repository.isMessageRequestAccepted(r) }
|
||||
val descriptionState: LiveData<DescriptionState> = LiveDataUtil.combineLatest(liveGroup.description, isMessageRequestAccepted, ::DescriptionState)
|
||||
|
||||
store.update(descriptionState) { d, state ->
|
||||
state.copy(
|
||||
specificSettingsState = state.requireGroupSettingsState().copy(
|
||||
groupDescription = d.description,
|
||||
groupDescriptionShouldLinkify = d.canLinkify,
|
||||
groupDescriptionLoaded = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
store.update(liveGroup.isActive) { isActive, state ->
|
||||
state.copy(
|
||||
specificSettingsState = state.requireGroupSettingsState().copy(
|
||||
canLeave = isActive && groupId.isPush
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
store.update(liveGroup.title) { title, state ->
|
||||
state.copy(
|
||||
specificSettingsState = state.requireGroupSettingsState().copy(
|
||||
groupTitle = title,
|
||||
groupTitleLoaded = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
store.update(liveGroup.groupLink) { groupLink, state ->
|
||||
state.copy(
|
||||
specificSettingsState = state.requireGroupSettingsState().copy(
|
||||
groupLinkEnabled = groupLink.isEnabled
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
store.update(repository.getMembershipCountDescription(liveGroup)) { description, state ->
|
||||
state.copy(
|
||||
specificSettingsState = state.requireGroupSettingsState().copy(
|
||||
membershipCountDescription = description
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLegacyGroupState(recipient: Recipient): LegacyGroupPreference.State {
|
||||
val showLegacyInfo = recipient.requireGroupId().isV1
|
||||
|
||||
return if (showLegacyInfo && recipient.participants.size > FeatureFlags.groupLimits().hardLimit) {
|
||||
LegacyGroupPreference.State.TOO_LARGE
|
||||
} else if (showLegacyInfo) {
|
||||
LegacyGroupPreference.State.UPGRADE
|
||||
} else if (groupId.isMms) {
|
||||
LegacyGroupPreference.State.MMS_WARNING
|
||||
} else {
|
||||
LegacyGroupPreference.State.NONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddToGroup() {
|
||||
repository.getGroupCapacity(groupId) { capacityResult ->
|
||||
if (capacityResult.getRemainingCapacity() > 0) {
|
||||
internalEvents.postValue(
|
||||
ConversationSettingsEvent.AddMembersToGroup(
|
||||
groupId,
|
||||
capacityResult.getSelectionWarning(),
|
||||
capacityResult.getSelectionLimit(),
|
||||
capacityResult.getMembersWithoutSelf()
|
||||
)
|
||||
)
|
||||
} else {
|
||||
internalEvents.postValue(ConversationSettingsEvent.ShowGroupHardLimitDialog)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddToGroupComplete(selected: List<RecipientId>, onComplete: () -> Unit) {
|
||||
repository.addMembers(groupId, selected) {
|
||||
ThreadUtil.runOnMain { onComplete() }
|
||||
|
||||
when (it) {
|
||||
is GroupAddMembersResult.Success -> {
|
||||
if (it.newMembersInvited.isNotEmpty()) {
|
||||
internalEvents.postValue(ConversationSettingsEvent.ShowGroupInvitesSentDialog(it.newMembersInvited))
|
||||
}
|
||||
|
||||
if (it.numberOfMembersAdded > 0) {
|
||||
internalEvents.postValue(ConversationSettingsEvent.ShowMembersAdded(it.numberOfMembersAdded))
|
||||
}
|
||||
}
|
||||
is GroupAddMembersResult.Failure -> internalEvents.postValue(ConversationSettingsEvent.ShowAddMembersToGroupError(it.reason))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun revealAllMembers() {
|
||||
store.update { state ->
|
||||
state.copy(
|
||||
specificSettingsState = state.requireGroupSettingsState().copy(
|
||||
members = state.requireGroupSettingsState().allMembers,
|
||||
groupMembersExpanded = true,
|
||||
canShowMoreGroupMembers = false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setMuteUntil(muteUntil: Long) {
|
||||
repository.setMuteUntil(groupId, muteUntil)
|
||||
}
|
||||
|
||||
override fun unmute() {
|
||||
repository.setMuteUntil(groupId, 0)
|
||||
}
|
||||
|
||||
override fun block() {
|
||||
repository.block(groupId)
|
||||
}
|
||||
|
||||
override fun unblock() {
|
||||
repository.unblock(groupId)
|
||||
}
|
||||
|
||||
override fun initiateGroupUpgrade() {
|
||||
repository.getExternalPossiblyMigratedGroupRecipientId(groupId) {
|
||||
internalEvents.postValue(ConversationSettingsEvent.InitiateGroupMigration(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val recipientId: RecipientId? = null,
|
||||
private val groupId: GroupId? = null,
|
||||
private val repository: ConversationSettingsRepository,
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(
|
||||
modelClass.cast(
|
||||
when {
|
||||
recipientId != null -> RecipientSettingsViewModel(recipientId, repository)
|
||||
groupId != null -> GroupSettingsViewModel(groupId, repository)
|
||||
else -> error("One of RecipientId or GroupId required.")
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class DescriptionState(
|
||||
val description: String?,
|
||||
val canLinkify: Boolean
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation
|
||||
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
sealed class GroupAddMembersResult {
|
||||
class Success(
|
||||
val numberOfMembersAdded: Int,
|
||||
val newMembersInvited: List<Recipient>
|
||||
) : GroupAddMembersResult()
|
||||
|
||||
class Failure(
|
||||
val reason: GroupChangeFailureReason
|
||||
) : GroupAddMembersResult()
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation
|
||||
|
||||
import org.thoughtcrime.securesms.ContactSelectionListFragment
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
class GroupCapacityResult(
|
||||
private val selfId: RecipientId,
|
||||
private val members: List<RecipientId>,
|
||||
private val selectionLimits: SelectionLimits
|
||||
) {
|
||||
fun getMembers(): List<RecipientId?> {
|
||||
return members
|
||||
}
|
||||
|
||||
fun getSelectionLimit(): Int {
|
||||
if (!selectionLimits.hasHardLimit()) {
|
||||
return ContactSelectionListFragment.NO_LIMIT
|
||||
}
|
||||
val containsSelf = members.indexOf(selfId) != -1
|
||||
return selectionLimits.hardLimit - if (containsSelf) 1 else 0
|
||||
}
|
||||
|
||||
fun getSelectionWarning(): Int {
|
||||
if (!selectionLimits.hasRecommendedLimit()) {
|
||||
return ContactSelectionListFragment.NO_LIMIT
|
||||
}
|
||||
|
||||
val containsSelf = members.indexOf(selfId) != -1
|
||||
return selectionLimits.recommendedLimit - if (containsSelf) 1 else 0
|
||||
}
|
||||
|
||||
fun getRemainingCapacity(): Int {
|
||||
return selectionLimits.hardLimit - members.size
|
||||
}
|
||||
|
||||
fun getMembersWithoutSelf(): List<RecipientId> {
|
||||
val recipientIds = ArrayList<RecipientId>(members.size)
|
||||
for (recipientId in members) {
|
||||
if (recipientId != selfId) {
|
||||
recipientIds.add(recipientId)
|
||||
}
|
||||
}
|
||||
return recipientIds
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.permissions
|
||||
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
|
||||
|
||||
sealed class PermissionsSettingsEvents {
|
||||
class GroupChangeError(val reason: GroupChangeFailureReason) : PermissionsSettingsEvents()
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.permissions
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.fragment.app.viewModels
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.groups.ParcelableGroupId
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupErrors
|
||||
|
||||
class PermissionsSettingsFragment : DSLSettingsFragment(
|
||||
titleId = R.string.ConversationSettingsFragment__permissions
|
||||
) {
|
||||
|
||||
private val permissionsOptions: Array<String> by lazy {
|
||||
resources.getStringArray(R.array.PermissionsSettingsFragment__editor_labels)
|
||||
}
|
||||
|
||||
private val viewModel: PermissionsSettingsViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
val args = PermissionsSettingsFragmentArgs.fromBundle(requireArguments())
|
||||
val groupId = requireNotNull(ParcelableGroupId.get(args.groupId as ParcelableGroupId))
|
||||
val repository = PermissionsSettingsRepository(requireContext())
|
||||
|
||||
PermissionsSettingsViewModel.Factory(groupId, repository)
|
||||
}
|
||||
)
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
}
|
||||
|
||||
viewModel.events.observe(viewLifecycleOwner) { event ->
|
||||
when (event) {
|
||||
is PermissionsSettingsEvents.GroupChangeError -> handleGroupChangeError(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleGroupChangeError(groupChangeError: PermissionsSettingsEvents.GroupChangeError) {
|
||||
Toast.makeText(context, GroupErrors.getUserDisplayMessage(groupChangeError.reason), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: PermissionsSettingsState): DSLConfiguration {
|
||||
return configure {
|
||||
|
||||
radioListPref(
|
||||
title = DSLSettingsText.from(R.string.PermissionsSettingsFragment__add_members),
|
||||
isEnabled = state.selfCanEditSettings,
|
||||
listItems = permissionsOptions,
|
||||
dialogTitle = DSLSettingsText.from(R.string.PermissionsSettingsFragment__who_can_add_new_members),
|
||||
selected = getSelected(state.nonAdminCanAddMembers),
|
||||
confirmAction = true,
|
||||
onSelected = {
|
||||
viewModel.setNonAdminCanAddMembers(it == 1)
|
||||
}
|
||||
)
|
||||
|
||||
radioListPref(
|
||||
title = DSLSettingsText.from(R.string.PermissionsSettingsFragment__edit_group_info),
|
||||
isEnabled = state.selfCanEditSettings,
|
||||
listItems = permissionsOptions,
|
||||
dialogTitle = DSLSettingsText.from(R.string.PermissionsSettingsFragment__who_can_edit_this_groups_info),
|
||||
selected = getSelected(state.nonAdminCanEditGroupInfo),
|
||||
confirmAction = true,
|
||||
onSelected = {
|
||||
viewModel.setNonAdminCanEditGroupInfo(it == 1)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@StringRes
|
||||
private fun getSelected(isNonAdminAllowed: Boolean): Int {
|
||||
return if (isNonAdminAllowed) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.permissions
|
||||
|
||||
import android.content.Context
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.groups.GroupAccessControl
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeException
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeErrorCallback
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
|
||||
import java.io.IOException
|
||||
|
||||
private val TAG = Log.tag(PermissionsSettingsRepository::class.java)
|
||||
|
||||
class PermissionsSettingsRepository(private val context: Context) {
|
||||
|
||||
fun applyMembershipRightsChange(groupId: GroupId, newRights: GroupAccessControl, error: GroupChangeErrorCallback) {
|
||||
SignalExecutors.UNBOUNDED.execute {
|
||||
try {
|
||||
GroupManager.applyMembershipAdditionRightsChange(context, groupId.requireV2(), newRights)
|
||||
} catch (e: GroupChangeException) {
|
||||
Log.w(TAG, e)
|
||||
error.onError(GroupChangeFailureReason.fromException(e))
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, e)
|
||||
error.onError(GroupChangeFailureReason.fromException(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun applyAttributesRightsChange(groupId: GroupId, newRights: GroupAccessControl, error: GroupChangeErrorCallback) {
|
||||
SignalExecutors.UNBOUNDED.execute {
|
||||
try {
|
||||
GroupManager.applyAttributesRightsChange(context, groupId.requireV2(), newRights)
|
||||
} catch (e: GroupChangeException) {
|
||||
Log.w(TAG, e)
|
||||
error.onError(GroupChangeFailureReason.fromException(e))
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, e)
|
||||
error.onError(GroupChangeFailureReason.fromException(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user