mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-09 09:40:14 +01:00
Compare commits
82 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 439760e773 | |||
| 7560896e2d | |||
| fe18def67e | |||
| 413962a093 | |||
| e518eca9a1 | |||
| b70322b5a6 | |||
| 047516c80b | |||
| 0a45b9b5e3 | |||
| 99b0061127 | |||
| 7b11cc1676 | |||
| 663e0a616e | |||
| d05338cee0 | |||
| ce294dbc0b | |||
| d0efd8d4b0 | |||
| c8875b5ad1 | |||
| 188458f772 | |||
| ed7fd10749 | |||
| 2ffbf09b1b | |||
| 799e57dbe9 | |||
| 572c11ee6d | |||
| 4dd5a4ee53 | |||
| 370fca3c89 | |||
| d91f130238 | |||
| bb20432417 | |||
| 8138ea5f8f | |||
| f235aa0599 | |||
| c7d719e983 | |||
| cf71d43a2f | |||
| 1e70e825a3 | |||
| cce1979716 | |||
| ad7e9c0fd7 | |||
| bd3e1e8059 | |||
| adb9e2173f | |||
| 958c6f451f | |||
| ab090236a1 | |||
| 23698dbc28 | |||
| 0542262c49 | |||
| e2d4ca9a4c | |||
| e54f3f501a | |||
| 638d4997d1 | |||
| cbd05c4dff | |||
| ef396b5758 | |||
| 1d36ecafe1 | |||
| 07329c5b0d | |||
| 7fc4ec3006 | |||
| 9e7477bbeb | |||
| c83054906b | |||
| 011dc3495f | |||
| 41b833e788 | |||
| e11f7225d3 | |||
| bb261a3d85 | |||
| 116f702be6 | |||
| 4d09776277 | |||
| f32184c27e | |||
| 5fc037b324 | |||
| fc9d3e11e8 | |||
| a951c7edfe | |||
| 9d1714d452 | |||
| 9c2825f202 | |||
| a8969b34a4 | |||
| 1f59f3c2c4 | |||
| c6d91dce6e | |||
| 40c4633d41 | |||
| edfe89683b | |||
| cc3bedd154 | |||
| 56803a8850 | |||
| 2fdb712b38 | |||
| 3d39045d1b | |||
| 90385b4e1c | |||
| a02b66601c | |||
| a83c57ff73 | |||
| 3d063b38be | |||
| 03d20cb46a | |||
| 561186df90 | |||
| fdcd21132c | |||
| 1043851423 | |||
| 9bcbacc3d8 | |||
| c2d7ee6926 | |||
| ceecacb47e | |||
| f4986273e4 | |||
| 5f60adbe69 | |||
| db6efeaf3d |
+143
-101
@@ -1,7 +1,10 @@
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
import com.android.build.api.dsl.ManagedVirtualDevice
|
||||
import com.android.build.api.artifact.ArtifactTransformationRequest
|
||||
import com.android.build.api.artifact.SingleArtifact
|
||||
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
@@ -10,7 +13,6 @@ import java.util.Properties
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.jetbrains.kotlin.android)
|
||||
alias(libs.plugins.ktlint)
|
||||
alias(libs.plugins.compose.compiler)
|
||||
alias(libs.plugins.kotlinx.serialization)
|
||||
@@ -22,21 +24,22 @@ plugins {
|
||||
id("licenses")
|
||||
}
|
||||
|
||||
apply(from = "static-ips.gradle.kts")
|
||||
val staticIps = Properties().apply { file("static-ips.properties").reader().use { load(it) } }
|
||||
staticIps.stringPropertyNames().forEach { rootProject.extra[it] = staticIps.getProperty(it) }
|
||||
|
||||
val canonicalVersionCode = 1683
|
||||
val canonicalVersionName = "8.9.0"
|
||||
val canonicalVersionCode = 1686
|
||||
val canonicalVersionName = "8.10.1"
|
||||
val currentHotfixVersion = 0
|
||||
val maxHotfixVersions = 100
|
||||
|
||||
// We don't want versions to ever end in 0 so that they don't conflict with nightly versions
|
||||
val possibleHotfixVersions = (0 until maxHotfixVersions).toList().filter { it % 10 != 0 }
|
||||
|
||||
val debugKeystorePropertiesProvider = providers.of(PropertiesFileValueSource::class.java) {
|
||||
val debugKeystorePropertiesProvider: Provider<Properties> = providers.of(PropertiesFileValueSource::class.java) {
|
||||
parameters.file.set(rootProject.layout.projectDirectory.file("keystore.debug.properties"))
|
||||
}
|
||||
|
||||
val languagesProvider = providers.of(LanguageListValueSource::class.java) {
|
||||
val languagesProvider: Provider<List<String>> = providers.of(LanguageListValueSource::class.java) {
|
||||
parameters.resDir.set(layout.projectDirectory.dir("src/main/res"))
|
||||
}
|
||||
|
||||
@@ -81,6 +84,24 @@ val selectableVariants = listOf(
|
||||
"githubProdRelease"
|
||||
)
|
||||
|
||||
// Wire 5.x iterates Android source sets and expects matching Kotlin source sets.
|
||||
// AGP 9.0's built-in Kotlin doesn't create all source sets automatically.
|
||||
val kotlinExt = extensions.getByName("kotlin") as KotlinAndroidProjectExtension
|
||||
android.sourceSets.all {
|
||||
kotlinExt.sourceSets.findByName(name) ?: kotlinExt.sourceSets.create(name)
|
||||
}
|
||||
// AGP 9.0's built-in Kotlin doesn't pick up extra java.srcDir entries from Android
|
||||
// source sets, so add shared dirs directly to the relevant Kotlin compile tasks.
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java).configureEach {
|
||||
val isTestTask = name.contains("UnitTest") || name.contains("AndroidTest")
|
||||
if (isTestTask) {
|
||||
source("$projectDir/src/testShared")
|
||||
}
|
||||
if (!isTestTask && (name.contains("Mocked") || name.contains("Benchmark"))) {
|
||||
source("$projectDir/src/benchmarkShared/java")
|
||||
}
|
||||
}
|
||||
|
||||
wire {
|
||||
kotlin {
|
||||
javaInterop = true
|
||||
@@ -94,8 +115,6 @@ wire {
|
||||
srcDir("${project.rootDir}/lib/libsignal-service/src/main/protowire")
|
||||
srcDir("${project.rootDir}/lib/archive/src/main/protowire")
|
||||
}
|
||||
// Handled by libsignal
|
||||
prune("signalservice.DecryptionErrorMessage")
|
||||
}
|
||||
|
||||
ktlint {
|
||||
@@ -106,7 +125,7 @@ android {
|
||||
namespace = "org.thoughtcrime.securesms"
|
||||
|
||||
buildToolsVersion = libs.versions.buildTools.get()
|
||||
compileSdkVersion = libs.versions.compileSdk.get()
|
||||
compileSdkVersion(libs.versions.compileSdk.get())
|
||||
ndkVersion = libs.versions.ndk.get()
|
||||
|
||||
flavorDimensions += listOf("distribution", "environment")
|
||||
@@ -114,13 +133,7 @@ android {
|
||||
|
||||
android.bundle.language.enableSplit = false
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = libs.versions.kotlinJvmTarget.get()
|
||||
freeCompilerArgs = listOf("-Xjvm-default=all")
|
||||
suppressWarnings = true
|
||||
}
|
||||
|
||||
debugKeystorePropertiesProvider.orNull?.let { properties ->
|
||||
debugKeystorePropertiesProvider.get().takeIf { it.isNotEmpty() }?.let { properties ->
|
||||
signingConfigs.getByName("debug").apply {
|
||||
storeFile = file("${project.rootDir}/${properties.getProperty("storeFile")}")
|
||||
storePassword = properties.getProperty("storePassword")
|
||||
@@ -137,8 +150,8 @@ android {
|
||||
}
|
||||
|
||||
managedDevices {
|
||||
devices {
|
||||
create<ManagedVirtualDevice>("pixel3api30") {
|
||||
localDevices {
|
||||
create("pixel3api30") {
|
||||
device = "Pixel 3"
|
||||
apiLevel = 30
|
||||
systemImageSource = "google-atd"
|
||||
@@ -195,10 +208,6 @@ android {
|
||||
compose = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.4"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
if (currentHotfixVersion >= maxHotfixVersions) {
|
||||
throw AssertionError("Hotfix version offset is too large!")
|
||||
@@ -291,7 +300,7 @@ android {
|
||||
isDefault = true
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android.txt"),
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard/proguard-firebase-messaging.pro",
|
||||
"proguard/proguard-google-play-services.pro",
|
||||
"proguard/proguard-jackson.pro",
|
||||
@@ -308,6 +317,7 @@ android {
|
||||
"proguard/proguard-retrolambda.pro",
|
||||
"proguard/proguard-okhttp.pro",
|
||||
"proguard/proguard-ez-vcard.pro",
|
||||
"proguard/proguard-dnsjava.pro",
|
||||
"proguard/proguard.cfg"
|
||||
)
|
||||
testProguardFiles(
|
||||
@@ -480,70 +490,6 @@ android {
|
||||
lintConfig = rootProject.file("lint.xml")
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
beforeVariants { variant ->
|
||||
variant.enable = variant.name in selectableVariants
|
||||
}
|
||||
onVariants(selector().all()) { variant: com.android.build.api.variant.ApplicationVariant ->
|
||||
// Include the test-only library on debug builds.
|
||||
if (variant.buildType != "instrumentation") {
|
||||
variant.packaging.jniLibs.excludes.add("**/libsignal_jni_testing.so")
|
||||
}
|
||||
|
||||
// Starting with minSdk 23, Android leaves native libraries uncompressed, which is fine for the Play Store, but not for our self-distributed APKs.
|
||||
// This reverts it to the legacy behavior, compressing the native libraries, and drastically reducing the APK file size.
|
||||
if (variant.name.contains("website", ignoreCase = true) || variant.name.contains("github", ignoreCase = true)) {
|
||||
variant.packaging.jniLibs.useLegacyPackaging.set(true)
|
||||
}
|
||||
|
||||
// Version overrides
|
||||
if (variant.name.contains("nightly", ignoreCase = true)) {
|
||||
var tag = getNightlyTagForCurrentCommit()
|
||||
if (!tag.isNullOrEmpty()) {
|
||||
if (tag.startsWith("v")) {
|
||||
tag = tag.substring(1)
|
||||
}
|
||||
|
||||
// We add a multiple of maxHotfixVersions to nightlies to ensure we're always at least that many versions ahead
|
||||
val nightlyBuffer = (5 * maxHotfixVersions)
|
||||
val nightlyVersionCode = (canonicalVersionCode * maxHotfixVersions) + (getNightlyBuildNumber(tag) * 10) + nightlyBuffer
|
||||
|
||||
variant.outputs.forEach { output ->
|
||||
output.versionName.set("$tag | ${getLastCommitDateTimeUtc()}")
|
||||
output.versionCode.set(nightlyVersionCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onVariants(selector().withBuildType("quickstart")) { variant ->
|
||||
val environment = variant.flavorName?.let { name ->
|
||||
when {
|
||||
name.contains("staging", ignoreCase = true) -> "staging"
|
||||
name.contains("prod", ignoreCase = true) -> "prod"
|
||||
else -> "prod"
|
||||
}
|
||||
} ?: "prod"
|
||||
|
||||
val taskProvider = tasks.register<CopyQuickstartCredentialsTask>("copyQuickstartCredentials${variant.name.capitalize()}") {
|
||||
if (quickstartCredentialsDir != null) {
|
||||
inputDir.set(File(quickstartCredentialsDir))
|
||||
}
|
||||
filePrefix.set("${environment}_")
|
||||
}
|
||||
variant.sources.assets?.addGeneratedSourceDirectory(taskProvider) { it.outputDir }
|
||||
}
|
||||
|
||||
onVariants(selector().withBuildType("benchmark")) { variant ->
|
||||
val taskProvider = tasks.register<CopyBenchmarkBackupTask>("copyBenchmarkBackup${variant.name.capitalize()}") {
|
||||
if (benchmarkBackupFile != null) {
|
||||
inputFile.set(File(benchmarkBackupFile))
|
||||
}
|
||||
}
|
||||
variant.sources.assets?.addGeneratedSourceDirectory(taskProvider) { it.outputDir }
|
||||
}
|
||||
}
|
||||
|
||||
val releaseDir = "$projectDir/src/release/java"
|
||||
val debugDir = "$projectDir/src/debug/java"
|
||||
|
||||
@@ -565,15 +511,79 @@ android {
|
||||
manifest.srcFile("$projectDir/src/benchmarkShared/AndroidManifest.xml")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
applicationVariants.configureEach {
|
||||
outputs.configureEach {
|
||||
if (this is com.android.build.gradle.internal.api.BaseVariantOutputImpl) {
|
||||
val fileVersionName = versionName.substringBefore(" |")
|
||||
outputFileName = outputFileName.replace(".apk", "-$fileVersionName.apk")
|
||||
androidComponents {
|
||||
beforeVariants { variant ->
|
||||
variant.enable = variant.name in selectableVariants
|
||||
}
|
||||
onVariants(selector().all()) { variant: com.android.build.api.variant.ApplicationVariant ->
|
||||
// Rename APK to include version name
|
||||
val renameTask = tasks.register<RenameApkTask>("renameApk${variant.name.replaceFirstChar { it.uppercase() }}")
|
||||
val renameRequest = variant.artifacts.use(renameTask)
|
||||
.wiredWithDirectories(RenameApkTask::apkFolder, RenameApkTask::outFolder)
|
||||
.toTransformMany(SingleArtifact.APK)
|
||||
renameTask.configure {
|
||||
transformationRequest.set(renameRequest)
|
||||
}
|
||||
|
||||
// Include the test-only library on debug builds.
|
||||
if (variant.buildType != "instrumentation") {
|
||||
variant.packaging.jniLibs.excludes.add("**/libsignal_jni_testing.so")
|
||||
}
|
||||
|
||||
// Starting with minSdk 23, Android leaves native libraries uncompressed, which is fine for the Play Store, but not for our self-distributed APKs.
|
||||
// This reverts it to the legacy behavior, compressing the native libraries, and drastically reducing the APK file size.
|
||||
if (variant.name.contains("website", ignoreCase = true) || variant.name.contains("github", ignoreCase = true)) {
|
||||
variant.packaging.jniLibs.useLegacyPackaging.set(true)
|
||||
}
|
||||
|
||||
// Version overrides
|
||||
if (variant.name.contains("nightly", ignoreCase = true)) {
|
||||
var tag = getNightlyTagForCurrentCommit()
|
||||
if (!tag.isNullOrEmpty()) {
|
||||
if (tag.startsWith("v")) {
|
||||
tag = tag.substring(1)
|
||||
}
|
||||
|
||||
// We add a multiple of maxHotfixVersions to nightlies to ensure we're always at least that many versions ahead
|
||||
val nightlyBuffer = (5 * maxHotfixVersions)
|
||||
val nightlyVersionCode = (canonicalVersionCode * maxHotfixVersions) + (getNightlyBuildNumber(tag) * 10) + nightlyBuffer
|
||||
|
||||
variant.outputs.forEach { output ->
|
||||
output.versionName.set("$tag | ${getLastCommitDateTimeUtc()}")
|
||||
output.versionCode.set(nightlyVersionCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onVariants(selector().withBuildType("quickstart")) { variant ->
|
||||
val environment = variant.flavorName?.let { name ->
|
||||
when {
|
||||
name.contains("staging", ignoreCase = true) -> "staging"
|
||||
name.contains("prod", ignoreCase = true) -> "prod"
|
||||
else -> "prod"
|
||||
}
|
||||
} ?: "prod"
|
||||
|
||||
val taskProvider = tasks.register<CopyQuickstartCredentialsTask>("copyQuickstartCredentials${variant.name.capitalize()}") {
|
||||
if (quickstartCredentialsDir != null) {
|
||||
inputDir.set(File(quickstartCredentialsDir))
|
||||
}
|
||||
filePrefix.set("${environment}_")
|
||||
}
|
||||
variant.sources.assets?.addGeneratedSourceDirectory(taskProvider) { it.outputDir }
|
||||
}
|
||||
|
||||
onVariants(selector().withBuildType("benchmark")) { variant ->
|
||||
val taskProvider = tasks.register<CopyBenchmarkBackupTask>("copyBenchmarkBackup${variant.name.capitalize()}") {
|
||||
if (benchmarkBackupFile != null) {
|
||||
inputFile.set(File(benchmarkBackupFile))
|
||||
}
|
||||
}
|
||||
variant.sources.assets?.addGeneratedSourceDirectory(taskProvider) { it.outputDir }
|
||||
}
|
||||
}
|
||||
|
||||
baselineProfile {
|
||||
@@ -590,6 +600,14 @@ baselineProfile {
|
||||
dexLayoutOptimization = false
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.fromTarget(libs.versions.kotlinJvmTarget.get())
|
||||
freeCompilerArgs.addAll("-Xjvm-default=all")
|
||||
suppressWarnings = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
lintChecks(project(":lintchecks"))
|
||||
ktlintRuleset(libs.ktlint.twitter.compose)
|
||||
@@ -619,11 +637,7 @@ dependencies {
|
||||
implementation(project(":lib:apng"))
|
||||
|
||||
implementation(libs.androidx.fragment.ktx)
|
||||
implementation(libs.androidx.appcompat) {
|
||||
version {
|
||||
strictly("1.6.1")
|
||||
}
|
||||
}
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.window.window)
|
||||
implementation(libs.androidx.window.java)
|
||||
implementation(libs.androidx.recyclerview)
|
||||
@@ -743,6 +757,7 @@ dependencies {
|
||||
}
|
||||
testImplementation(testLibs.conscrypt.openjdk.uber)
|
||||
testImplementation(testLibs.mockk)
|
||||
testImplementation(testFixtures(project(":core:ui")))
|
||||
testImplementation(testFixtures(project(":lib:libsignal-service")))
|
||||
testImplementation(testLibs.espresso.core)
|
||||
testImplementation(testLibs.kotlinx.coroutines.test)
|
||||
@@ -857,7 +872,7 @@ abstract class LanguageListValueSource : ValueSource<List<String>, LanguageListV
|
||||
}
|
||||
}
|
||||
|
||||
abstract class PropertiesFileValueSource : ValueSource<Properties?, PropertiesFileValueSource.Params> {
|
||||
abstract class PropertiesFileValueSource : ValueSource<Properties, PropertiesFileValueSource.Params> {
|
||||
interface Params : ValueSourceParameters {
|
||||
@get:InputFile
|
||||
@get:Optional
|
||||
@@ -865,9 +880,9 @@ abstract class PropertiesFileValueSource : ValueSource<Properties?, PropertiesFi
|
||||
val file: RegularFileProperty
|
||||
}
|
||||
|
||||
override fun obtain(): Properties? {
|
||||
override fun obtain(): Properties {
|
||||
val f: File = parameters.file.asFile.get()
|
||||
if (!f.exists()) return null
|
||||
if (!f.exists()) return Properties()
|
||||
|
||||
return Properties().apply {
|
||||
f.inputStream().use { load(it) }
|
||||
@@ -937,3 +952,30 @@ abstract class CopyBenchmarkBackupTask : DefaultTask() {
|
||||
backupFile.copyTo(dest.resolve("backup.binproto"), overwrite = true)
|
||||
}
|
||||
}
|
||||
|
||||
abstract class RenameApkTask : DefaultTask() {
|
||||
@get:InputFiles
|
||||
abstract val apkFolder: DirectoryProperty
|
||||
|
||||
@get:OutputDirectory
|
||||
abstract val outFolder: DirectoryProperty
|
||||
|
||||
@get:Internal
|
||||
abstract val transformationRequest: Property<ArtifactTransformationRequest<RenameApkTask>>
|
||||
|
||||
@TaskAction
|
||||
fun rename() {
|
||||
transformationRequest.get().submit(this) { artifact ->
|
||||
val originalFile = File(artifact.outputFile)
|
||||
val versionName = artifact.versionName?.substringBefore(" |")
|
||||
val newName = if (!versionName.isNullOrEmpty()) {
|
||||
originalFile.name.replace(".apk", "-$versionName.apk")
|
||||
} else {
|
||||
originalFile.name
|
||||
}
|
||||
val newFile = File(outFolder.get().asFile, newName)
|
||||
originalFile.copyTo(newFile, overwrite = true)
|
||||
newFile
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# dnsjava references desktop/server-only classes that are absent on Android.
|
||||
-dontwarn com.sun.jna.**
|
||||
-dontwarn javax.naming.**
|
||||
-dontwarn lombok.Generated
|
||||
@@ -74,7 +74,7 @@ class OtherClient(val serviceId: ServiceId, val e164: String, val identityKeyPai
|
||||
val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, null)
|
||||
|
||||
if (!aciStore.containsSession(getAliceProtocolAddress())) {
|
||||
val sessionBuilder = SignalSessionBuilder(sessionLock, SessionBuilder(aciStore, getAliceProtocolAddress()))
|
||||
val sessionBuilder = SignalSessionBuilder(sessionLock, SessionBuilder(aciStore, getAliceProtocolAddress(), SignalProtocolAddress(serviceAddress.identifier, 1)))
|
||||
sessionBuilder.process(getAlicePreKeyBundle())
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -321,7 +321,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
|
||||
* This is so we can capture ANR's that happen on boot before the foreground event.
|
||||
*/
|
||||
private void startAnrDetector() {
|
||||
AnrDetector.start(TimeUnit.SECONDS.toMillis(5), RemoteConfig::internalUser, (dumps) -> {
|
||||
AnrDetector.start(TimeUnit.SECONDS.toMillis(5), () -> RemoteConfig.internalUser() && SignalStore.internal().getAnrDetectionCrashes(), (dumps) -> {
|
||||
LogDatabase.getInstance(this).anrs().save(System.currentTimeMillis(), dumps);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
|
||||
@@ -85,7 +85,6 @@ import kotlinx.coroutines.withContext
|
||||
import org.signal.core.ui.BottomSheetUtil
|
||||
import org.signal.core.ui.compose.Snackbars
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.signal.core.ui.isSplitPane
|
||||
import org.signal.core.ui.permissions.Permissions
|
||||
import org.signal.core.ui.rememberIsSplitPane
|
||||
import org.signal.core.util.Util
|
||||
@@ -521,15 +520,14 @@ class MainActivity :
|
||||
}.navigateToDetailLocation(location)
|
||||
}
|
||||
|
||||
is MainNavigationDetailLocation.Chats -> {
|
||||
if (location is MainNavigationDetailLocation.Chats.Conversation) {
|
||||
chatNavGraphState.writeGraphicsLayerToBitmap()
|
||||
}
|
||||
is MainNavigationDetailLocation.Conversation -> {
|
||||
chatNavGraphState.writeGraphicsLayerToBitmap()
|
||||
chatsNavHostController.navigateToDetailLocation(location)
|
||||
}
|
||||
|
||||
is MainNavigationDetailLocation.Chats -> chatsNavHostController.navigateToDetailLocation(location)
|
||||
is MainNavigationDetailLocation.CallLinkDetails -> callsNavHostController.navigateToDetailLocation(location)
|
||||
is MainNavigationDetailLocation.Calls -> callsNavHostController.navigateToDetailLocation(location)
|
||||
|
||||
is MainNavigationDetailLocation.Stories -> storiesNavHostController.navigateToDetailLocation(location)
|
||||
}
|
||||
}
|
||||
@@ -848,7 +846,7 @@ class MainActivity :
|
||||
|
||||
val detailLocation = extras.getParcelableCompat(KEY_DETAIL_LOCATION, MainNavigationDetailLocation::class.java)
|
||||
if (detailLocation != null) {
|
||||
mainNavigationViewModel.goTo(detailLocation)
|
||||
goTo(detailLocation)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1034,7 +1032,7 @@ class MainActivity :
|
||||
private fun handleConversationIntent(intent: Intent) {
|
||||
if (ConversationIntents.isConversationIntent(intent)) {
|
||||
mainNavigationViewModel.goTo(MainNavigationListLocation.CHATS)
|
||||
mainNavigationViewModel.goTo(MainNavigationDetailLocation.Chats.Conversation(ConversationIntents.readArgsFromBundle(intent.extras!!)))
|
||||
mainNavigationViewModel.goTo(MainNavigationDetailLocation.Conversation(ConversationIntents.readArgsFromBundle(intent.extras!!)))
|
||||
intent.action = null
|
||||
setIntent(intent)
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ public class MainNavigator {
|
||||
.withStartingPosition(startingPosition)
|
||||
.asIncognito(incognito)
|
||||
.toConversationArgs())
|
||||
.subscribe(args -> viewModel.goTo(new MainNavigationDetailLocation.Chats.Conversation(args)));
|
||||
.subscribe(args -> viewModel.goTo(new MainNavigationDetailLocation.Conversation(args)));
|
||||
|
||||
lifecycleDisposable.add(disposable);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.app.KeyguardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -49,7 +48,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||
import org.thoughtcrime.securesms.components.AnimatingToggle;
|
||||
import org.thoughtcrime.securesms.crypto.InvalidPassphraseException;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
@@ -389,13 +387,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
Log.i(TAG, "onAuthenticationSucceeded");
|
||||
|
||||
lockScreenButton.setOnClickListener(null);
|
||||
unlockView.addAnimatorListener(new AnimationCompleteListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
handleAuthenticated();
|
||||
}
|
||||
});
|
||||
unlockView.playAnimation();
|
||||
handleAuthenticated();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -16,8 +16,8 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.media.AudioManager
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.safeUnregisterReceiver
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.util.safeUnregisterReceiver
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioHandler
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.update
|
||||
import org.signal.core.util.bytes
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.safeUnregisterReceiver
|
||||
import org.signal.core.util.throttleLatest
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
@@ -31,7 +32,6 @@ import org.thoughtcrime.securesms.jobmanager.impl.DiskSpaceNotLowConstraint
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.WifiConstraint
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.safeUnregisterReceiver
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
@@ -203,6 +203,7 @@ object ArchiveRestoreProgress {
|
||||
state.hasActivelyRestoredThisRun -> ArchiveRestoreProgressState.RestoreStatus.FINISHED
|
||||
else -> ArchiveRestoreProgressState.RestoreStatus.NONE
|
||||
}
|
||||
|
||||
else -> {
|
||||
val availableBytes = SignalStore.backup.spaceAvailableOnDiskBytes
|
||||
|
||||
|
||||
+1
-1
@@ -322,7 +322,7 @@ class ChatItemArchiveExporter(
|
||||
}
|
||||
|
||||
MessageTypes.isSessionSwitchoverType(record.type) -> {
|
||||
builder.updateMessage = record.toRemoteSessionSwitchoverUpdate(record.dateSent) ?: continue
|
||||
builder.updateMessage = record.toRemoteSessionSwitchoverUpdate(record.dateSent)?.takeIf { builder.authorIsAciContact(exportState) } ?: continue
|
||||
transformTimer.emit("sse")
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -886,8 +886,8 @@ class ChatItemArchiveImporter(
|
||||
SimpleChatUpdate.Type.UNKNOWN -> typeWithoutBase
|
||||
SimpleChatUpdate.Type.JOINED_SIGNAL -> MessageTypes.JOINED_TYPE or typeWithoutBase
|
||||
SimpleChatUpdate.Type.IDENTITY_UPDATE -> MessageTypes.KEY_EXCHANGE_IDENTITY_UPDATE_BIT or typeWithoutBase
|
||||
SimpleChatUpdate.Type.IDENTITY_VERIFIED -> MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT or typeWithoutBase
|
||||
SimpleChatUpdate.Type.IDENTITY_DEFAULT -> MessageTypes.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT or typeWithoutBase
|
||||
SimpleChatUpdate.Type.IDENTITY_VERIFIED -> MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT or typeWithoutBase or MessageTypes.BASE_SENT_TYPE
|
||||
SimpleChatUpdate.Type.IDENTITY_DEFAULT -> MessageTypes.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT or typeWithoutBase or MessageTypes.BASE_SENT_TYPE
|
||||
SimpleChatUpdate.Type.CHANGE_NUMBER -> MessageTypes.CHANGE_NUMBER_TYPE
|
||||
SimpleChatUpdate.Type.RELEASE_CHANNEL_DONATION_REQUEST -> MessageTypes.RELEASE_CHANNEL_DONATION_REQUEST_TYPE
|
||||
SimpleChatUpdate.Type.END_SESSION -> MessageTypes.END_SESSION_BIT or typeWithoutBase
|
||||
|
||||
@@ -333,9 +333,10 @@ private fun BackupFailedBody() {
|
||||
append(stringResource(id = R.string.BackupAlertBottomSheet__an_error_occurred))
|
||||
append(" ")
|
||||
|
||||
val link = stringResource(R.string.remote_backup_support_url)
|
||||
withLink(
|
||||
LinkAnnotation.Clickable(tag = "learn-more") {
|
||||
CommunicationActions.openBrowserLink(context, context.getString(R.string.remote_backup_support_url))
|
||||
CommunicationActions.openBrowserLink(context, link)
|
||||
}
|
||||
) {
|
||||
withStyle(SpanStyle(color = MaterialTheme.colorScheme.primary)) {
|
||||
|
||||
+2
-1
@@ -31,10 +31,11 @@ class NoRemoteStorageSpaceAvailableBottomSheet : ComposeBottomSheetDialogFragmen
|
||||
@Composable
|
||||
override fun SheetContent() {
|
||||
val context = LocalContext.current
|
||||
val supportUrl = stringResource(R.string.remote_backup_support_url)
|
||||
|
||||
NoRemoteStorageSpaceAvailableBottomSheetContent(
|
||||
onLearnMoreClick = {
|
||||
CommunicationActions.openBrowserLink(context, context.getString(R.string.remote_backup_support_url))
|
||||
CommunicationActions.openBrowserLink(context, supportUrl)
|
||||
},
|
||||
onContactSupportClick = {
|
||||
ContactSupportDialogFragment.create(
|
||||
|
||||
+4
-4
@@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLocale
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString.Builder
|
||||
@@ -37,7 +38,6 @@ import org.signal.core.ui.compose.Previews
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.keyvalue.BackupValues
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import java.util.Locale
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
@@ -53,7 +53,7 @@ fun BackupCreateErrorRow(
|
||||
onLearnMoreClick: () -> Unit = {}
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val locale = Locale.getDefault()
|
||||
val locale = LocalLocale.current
|
||||
|
||||
when (error) {
|
||||
BackupValues.BackupCreationError.TRANSIENT -> {
|
||||
@@ -82,7 +82,7 @@ fun BackupCreateErrorRow(
|
||||
BackupValues.BackupCreationError.BACKUP_FILE_TOO_LARGE -> {
|
||||
BackupAlertText {
|
||||
if (lastMessageCutoffTime > 0) {
|
||||
append(stringResource(R.string.BackupStatusRow__not_backing_up_old_messages, DateUtils.getDayPrecisionTimeString(context, locale, lastMessageCutoffTime)))
|
||||
append(stringResource(R.string.BackupStatusRow__not_backing_up_old_messages, DateUtils.getDayPrecisionTimeString(context, locale.platformLocale, lastMessageCutoffTime)))
|
||||
} else {
|
||||
append(stringResource(R.string.BackupStatusRow__backup_file_too_large))
|
||||
}
|
||||
@@ -91,7 +91,7 @@ fun BackupCreateErrorRow(
|
||||
|
||||
BackupValues.BackupCreationError.NOT_ENOUGH_DISK_SPACE -> {
|
||||
BackupAlertText {
|
||||
append(stringResource(R.string.BackupStatusRow__not_enough_disk_space, DateUtils.getDayPrecisionTimeString(context, locale, lastMessageCutoffTime)))
|
||||
append(stringResource(R.string.BackupStatusRow__not_enough_disk_space, DateUtils.getDayPrecisionTimeString(context, locale.platformLocale, lastMessageCutoffTime)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -43,6 +43,7 @@ import org.signal.core.models.AccountEntropyPool
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.horizontalGutters
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.TemporaryScreenshotSecurity
|
||||
import org.thoughtcrime.securesms.fonts.MonoTypeface
|
||||
import org.thoughtcrime.securesms.registration.ui.restore.BackupKeyVisualTransformation
|
||||
import org.thoughtcrime.securesms.registration.ui.restore.attachBackupKeyAutoFillHelper
|
||||
@@ -59,6 +60,8 @@ fun EnterKeyScreen(
|
||||
captionContent: @Composable () -> Unit,
|
||||
seeKeyButton: @Composable () -> Unit
|
||||
) {
|
||||
TemporaryScreenshotSecurity.bind()
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
|
||||
+3
@@ -59,6 +59,7 @@ import org.signal.core.ui.compose.horizontalGutters
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.signal.core.util.Util
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.TemporaryScreenshotSecurity
|
||||
import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeyCredentialManagerHandler
|
||||
import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeySaveState
|
||||
import org.thoughtcrime.securesms.fonts.MonoTypeface
|
||||
@@ -133,6 +134,8 @@ fun MessageBackupsKeyRecordScreen(
|
||||
mode: MessageBackupsKeyRecordMode = MessageBackupsKeyRecordMode.Next(onNextClick = {}),
|
||||
notifyKeyIsSameAsOnDeviceBackupKey: Boolean = false
|
||||
) {
|
||||
TemporaryScreenshotSecurity.bind()
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val backupKeyString = remember(backupKey) {
|
||||
backupKey.chunked(4).joinToString(" ")
|
||||
|
||||
+2
-1
@@ -108,9 +108,10 @@ fun VerifyBackupPinScreen(
|
||||
append(stringResource(id = R.string.VerifyBackupPinScreen__enter_the_backup_key_that_you_recorded))
|
||||
append(" ")
|
||||
|
||||
val supportUrl = stringResource(R.string.remote_backup_support_url)
|
||||
withLink(
|
||||
LinkAnnotation.Clickable(tag = "learn-more") {
|
||||
CommunicationActions.openBrowserLink(context, context.getString(R.string.remote_backup_support_url))
|
||||
CommunicationActions.openBrowserLink(context, supportUrl)
|
||||
}
|
||||
) {
|
||||
withStyle(SpanStyle(color = MaterialTheme.colorScheme.primary)) {
|
||||
|
||||
+2
-2
@@ -10,8 +10,8 @@ import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.ui.CurrencySelection
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.ui.NetworkFailure
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadingCircle
|
||||
import org.thoughtcrime.securesms.components.settings.models.SplashImage
|
||||
|
||||
+4
-11
@@ -16,9 +16,8 @@ import androidx.fragment.app.FragmentActivity
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.signal.core.util.getParcelableExtraCompat
|
||||
import org.thoughtcrime.securesms.calls.links.EditCallLinkNameDialogFragment
|
||||
import org.thoughtcrime.securesms.main.MainNavigationCallDetailRouter
|
||||
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation
|
||||
import org.thoughtcrime.securesms.main.MainNavigationListLocation
|
||||
import org.thoughtcrime.securesms.main.MainNavigationRouter
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
@@ -56,23 +55,17 @@ class CallLinkDetailsActivity : FragmentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private inner class Router : MainNavigationRouter {
|
||||
override fun goTo(location: MainNavigationDetailLocation) {
|
||||
private inner class Router : MainNavigationCallDetailRouter {
|
||||
override fun goToCallDetail(location: MainNavigationDetailLocation.Calls) {
|
||||
when (location) {
|
||||
is MainNavigationDetailLocation.Calls.CallLinks.EditCallLinkName -> {
|
||||
EditCallLinkNameDialogFragment().apply {
|
||||
arguments = bundleOf(EditCallLinkNameDialogFragment.ARG_NAME to viewModel.nameSnapshot)
|
||||
}.show(supportFragmentManager, null)
|
||||
}
|
||||
|
||||
is MainNavigationDetailLocation.Empty -> {
|
||||
finishAfterTransition()
|
||||
}
|
||||
|
||||
else -> error("Unsupported route $location")
|
||||
}
|
||||
}
|
||||
|
||||
override fun goTo(location: MainNavigationListLocation) = Unit
|
||||
override fun exitDetailLocation() = finishAfterTransition()
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -46,8 +46,8 @@ import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar.YouAreAlrea
|
||||
import org.thoughtcrime.securesms.calls.links.CallLinks
|
||||
import org.thoughtcrime.securesms.calls.links.SignalCallRow
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
import org.thoughtcrime.securesms.main.MainNavigationCallDetailRouter
|
||||
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation
|
||||
import org.thoughtcrime.securesms.main.MainNavigationRouter
|
||||
import org.thoughtcrime.securesms.main.MainNavigationViewModel
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
|
||||
@@ -63,7 +63,7 @@ fun CallLinkDetailsScreen(
|
||||
viewModel: CallLinkDetailsViewModel = viewModel {
|
||||
CallLinkDetailsViewModel(roomId)
|
||||
},
|
||||
router: MainNavigationRouter = viewModel<MainNavigationViewModel>(viewModelStoreOwner = LocalActivity.current as ComponentActivity) {
|
||||
router: MainNavigationCallDetailRouter = viewModel<MainNavigationViewModel>(viewModelStoreOwner = LocalActivity.current as ComponentActivity) {
|
||||
error("Should already be created.")
|
||||
}
|
||||
) {
|
||||
@@ -90,7 +90,7 @@ fun CallLinkDetailsScreen(
|
||||
class DefaultCallLinkDetailsCallback(
|
||||
private val activity: FragmentActivity,
|
||||
private val viewModel: CallLinkDetailsViewModel,
|
||||
private val router: MainNavigationRouter
|
||||
private val router: MainNavigationCallDetailRouter
|
||||
) : CallLinkDetailsCallback {
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
@@ -113,7 +113,7 @@ class DefaultCallLinkDetailsCallback(
|
||||
}
|
||||
|
||||
override fun onEditNameClicked() {
|
||||
router.goTo(MainNavigationDetailLocation.Calls.CallLinks.EditCallLinkName(callLinkRoomId = viewModel.recipientSnapshot!!.requireCallLinkRoomId()))
|
||||
router.goToCallDetail(MainNavigationDetailLocation.Calls.CallLinks.EditCallLinkName(callLinkRoomId = viewModel.recipientSnapshot!!.requireCallLinkRoomId()))
|
||||
}
|
||||
|
||||
override fun onShareClicked() {
|
||||
@@ -152,7 +152,7 @@ class DefaultCallLinkDetailsCallback(
|
||||
viewModel.setDisplayRevocationDialog(false)
|
||||
activity.lifecycleScope.launch {
|
||||
if (viewModel.delete()) {
|
||||
router.goTo(MainNavigationDetailLocation.Empty)
|
||||
router.exitDetailLocation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,13 +324,16 @@ class CallLogAdapter(
|
||||
return
|
||||
}
|
||||
|
||||
presentRecipientDetails(model.call.peer, model.call.searchQuery)
|
||||
presentRecipientDetails(model.call)
|
||||
presentCallInfo(model.call, model.call.date)
|
||||
presentCallType(model)
|
||||
}
|
||||
|
||||
private fun presentRecipientDetails(recipient: Recipient, searchQuery: String?) {
|
||||
binding.callRecipientAvatar.setAvatar(Glide.with(binding.callRecipientAvatar), recipient, true)
|
||||
private fun presentRecipientDetails(call: CallLogRow.Call) {
|
||||
val recipient = call.peer
|
||||
val searchQuery = call.searchQuery
|
||||
binding.callRecipientAvatar.setAvatar(Glide.with(binding.callRecipientAvatar), recipient, false)
|
||||
binding.callRecipientAvatar.setOnClickListener { onCallClicked(call) }
|
||||
binding.callRecipientBadge.setBadgeFromRecipient(recipient)
|
||||
binding.callRecipientName.text = if (searchQuery != null) {
|
||||
SearchUtil.getHighlightedSpan(
|
||||
|
||||
@@ -333,7 +333,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
if (viewModel.selectionStateSnapshot.isNotEmpty(binding.recycler.adapter!!.itemCount)) {
|
||||
viewModel.toggleSelected(callLogRow.id)
|
||||
} else {
|
||||
mainNavigationViewModel.goTo(MainNavigationDetailLocation.Calls.CallLinks.CallLinkDetails(callLogRow.record.roomId))
|
||||
mainNavigationViewModel.goTo(MainNavigationDetailLocation.CallLinkDetails(callLogRow.record.roomId))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -214,7 +213,8 @@ private fun UserMessagesHost(
|
||||
onDismiss: (UserMessage) -> Unit,
|
||||
snackbarHostState: SnackbarHostState
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val youAreAlreadyInACall = stringResource(R.string.CommunicationActions__you_are_already_in_a_call)
|
||||
val errorRetrievingContacts = stringResource(R.string.ContactSelectionListFragment_error_retrieving_contacts_check_your_network_connection)
|
||||
|
||||
when (userMessage) {
|
||||
null -> {}
|
||||
@@ -228,14 +228,14 @@ private fun UserMessagesHost(
|
||||
|
||||
is UserMessage.UserAlreadyInAnotherCall -> LaunchedEffect(userMessage) {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = context.getString(R.string.CommunicationActions__you_are_already_in_a_call)
|
||||
message = youAreAlreadyInACall
|
||||
)
|
||||
onDismiss(userMessage)
|
||||
}
|
||||
|
||||
is UserMessage.ContactsRefreshFailed -> LaunchedEffect(userMessage) {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = context.getString(R.string.ContactSelectionListFragment_error_retrieving_contacts_check_your_network_connection)
|
||||
message = errorRetrievingContacts
|
||||
)
|
||||
onDismiss(userMessage)
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ public final class AudioView extends FrameLayout {
|
||||
@NonNull private final AnimatingToggle controlToggle;
|
||||
@NonNull private final View progressAndPlay;
|
||||
@NonNull private final LottieAnimationView playPauseButton;
|
||||
@NonNull private final View downloadContainer;
|
||||
@NonNull private final ImageView downloadButton;
|
||||
@Nullable private final ProgressWheel circleProgress;
|
||||
@NonNull private final SeekBar seekBar;
|
||||
@@ -121,13 +122,14 @@ public final class AudioView extends FrameLayout {
|
||||
throw new IllegalStateException("Unsupported mode: " + mode);
|
||||
}
|
||||
|
||||
this.controlToggle = findViewById(R.id.control_toggle);
|
||||
this.playPauseButton = findViewById(R.id.play);
|
||||
this.progressAndPlay = findViewById(R.id.progress_and_play);
|
||||
this.downloadButton = findViewById(R.id.download);
|
||||
this.circleProgress = findViewById(R.id.circle_progress);
|
||||
this.seekBar = findViewById(R.id.seek);
|
||||
this.duration = findViewById(R.id.duration);
|
||||
this.controlToggle = findViewById(R.id.control_toggle);
|
||||
this.playPauseButton = findViewById(R.id.play);
|
||||
this.progressAndPlay = findViewById(R.id.progress_and_play);
|
||||
this.downloadContainer = findViewById(R.id.download_container);
|
||||
this.downloadButton = findViewById(R.id.download);
|
||||
this.circleProgress = findViewById(R.id.circle_progress);
|
||||
this.seekBar = findViewById(R.id.seek);
|
||||
this.duration = findViewById(R.id.duration);
|
||||
|
||||
lottieDirection = REVERSE;
|
||||
this.playPauseButton.setOnClickListener(new PlayPauseClickedListener());
|
||||
@@ -168,6 +170,7 @@ public final class AudioView extends FrameLayout {
|
||||
|
||||
public void setProgressAndPlayBackgroundTint(@ColorInt int color) {
|
||||
progressAndPlay.getBackground().setColorFilter(color, PorterDuff.Mode.SRC_IN);
|
||||
downloadContainer.getBackground().setColorFilter(color, PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
|
||||
public Observer<VoiceNotePlaybackState> getPlaybackStateObserver() {
|
||||
@@ -195,7 +198,7 @@ public final class AudioView extends FrameLayout {
|
||||
}
|
||||
|
||||
if (showControls && audio.isPendingDownload()) {
|
||||
controlToggle.displayQuick(downloadButton);
|
||||
controlToggle.displayQuick(downloadContainer);
|
||||
seekBar.setEnabled(false);
|
||||
downloadButton.setOnClickListener(new DownloadClickedListener(audio));
|
||||
if (circleProgress != null) {
|
||||
|
||||
@@ -30,11 +30,12 @@ import com.bumptech.glide.request.target.SimpleTarget;
|
||||
import com.bumptech.glide.request.target.Target;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
|
||||
import org.signal.core.util.ContextUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.avatar.fallback.FallbackAvatar;
|
||||
import org.thoughtcrime.securesms.avatar.fallback.FallbackAvatarDrawable;
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity;
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsNavigator;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.SystemContactPhoto;
|
||||
@@ -53,9 +54,7 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class AvatarImageView extends AppCompatImageView {
|
||||
|
||||
private static final int SIZE_LARGE = 1;
|
||||
private static final int SIZE_SMALL = 2;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = Log.tag(AvatarImageView.class);
|
||||
@@ -156,10 +155,10 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
|
||||
public void setAvatar(@NonNull RequestManager requestManager, @Nullable Recipient recipient, boolean quickContactEnabled, boolean useSelfProfileAvatar, boolean useBlurGradient) {
|
||||
setAvatar(requestManager, recipient, new AvatarOptions.Builder(this)
|
||||
.withUseSelfProfileAvatar(useSelfProfileAvatar)
|
||||
.withQuickContactEnabled(quickContactEnabled)
|
||||
.withUseBlurGradient(useBlurGradient)
|
||||
.build());
|
||||
.withUseSelfProfileAvatar(useSelfProfileAvatar)
|
||||
.withQuickContactEnabled(quickContactEnabled)
|
||||
.withUseBlurGradient(useBlurGradient)
|
||||
.build());
|
||||
}
|
||||
|
||||
private void setAvatar(@Nullable Recipient recipient, @NonNull AvatarOptions avatarOptions) {
|
||||
@@ -182,8 +181,8 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
|
||||
if (!photo.equals(recipientContactPhoto) || shouldBlur != blurred || !Objects.equals(chatColors, this.chatColors) || !Objects.equals(initials, this.initials)) {
|
||||
requestManager.clear(this);
|
||||
this.chatColors = chatColors;
|
||||
this.initials = initials;
|
||||
this.chatColors = chatColors;
|
||||
this.initials = initials;
|
||||
recipientContactPhoto = photo;
|
||||
|
||||
FallbackAvatarProvider activeFallbackPhotoProvider = this.fallbackAvatarProvider;
|
||||
@@ -215,12 +214,12 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
List<Transformation<Bitmap>> transforms = Collections.singletonList(new CircleCrop());
|
||||
|
||||
RequestBuilder<Drawable> request = requestManager.load(photo.contactPhoto)
|
||||
.fallback(fallback)
|
||||
.error(fallback)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.downsample(DownsampleStrategy.CENTER_INSIDE)
|
||||
.transform(new MultiTransformation<>(transforms))
|
||||
.addListener(redownloadRequestListener);
|
||||
.fallback(fallback)
|
||||
.error(fallback)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.downsample(DownsampleStrategy.CENTER_INSIDE)
|
||||
.transform(new MultiTransformation<>(transforms))
|
||||
.addListener(redownloadRequestListener);
|
||||
|
||||
if (wasUnblurred) {
|
||||
blurred = shouldBlur;
|
||||
@@ -260,17 +259,12 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
private void setAvatarClickHandler(@NonNull final Recipient recipient, boolean quickContactEnabled) {
|
||||
if (quickContactEnabled) {
|
||||
super.setOnClickListener(v -> {
|
||||
Context context = getContext();
|
||||
FragmentActivity activity = ContextUtil.requireFragmentActivity(getContext());
|
||||
|
||||
if (recipient.isPushGroup()) {
|
||||
context.startActivity(ConversationSettingsActivity.forGroup(context, recipient.requireGroupId().requirePush()),
|
||||
ConversationSettingsActivity.createTransitionBundle(context, this));
|
||||
ConversationSettingsNavigator.navigate(activity, recipient);
|
||||
} else {
|
||||
if (context instanceof FragmentActivity) {
|
||||
RecipientBottomSheetDialogFragment.show(((FragmentActivity) context).getSupportFragmentManager(), recipient.getId(), null);
|
||||
} else {
|
||||
context.startActivity(ConversationSettingsActivity.forRecipient(context, recipient.getId()),
|
||||
ConversationSettingsActivity.createTransitionBundle(context, this));
|
||||
}
|
||||
RecipientBottomSheetDialogFragment.show(activity.getSupportFragmentManager(), recipient.getId(), null);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -283,13 +277,13 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
Drawable fallback = new FallbackAvatarDrawable(getContext(), new FallbackAvatar.Resource.Group(color)).circleCrop();
|
||||
|
||||
Glide.with(this)
|
||||
.load(avatarBytes)
|
||||
.dontAnimate()
|
||||
.fallback(fallback)
|
||||
.error(fallback)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.circleCrop()
|
||||
.into(this);
|
||||
.load(avatarBytes)
|
||||
.dontAnimate()
|
||||
.fallback(fallback)
|
||||
.error(fallback)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.circleCrop()
|
||||
.into(this);
|
||||
}
|
||||
|
||||
public void setNonAvatarImageResource(@DrawableRes int imageResource) {
|
||||
@@ -308,7 +302,8 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
}
|
||||
}
|
||||
|
||||
private static class DefaultFallbackAvatarProvider implements FallbackAvatarProvider {}
|
||||
private static class DefaultFallbackAvatarProvider implements FallbackAvatarProvider {
|
||||
}
|
||||
|
||||
private static class RecipientContactPhoto {
|
||||
|
||||
|
||||
+10
-2
@@ -254,7 +254,11 @@ public class ConversationItemFooter extends ConstraintLayout {
|
||||
}
|
||||
});
|
||||
|
||||
dateView.setMaxWidth(ViewUtil.dpToPx(32));
|
||||
ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) dateView.getLayoutParams();
|
||||
params.startToEnd = R.id.footer_audio_playback_speed_toggle;
|
||||
params.constrainedWidth = true;
|
||||
params.horizontalBias = 1f;
|
||||
dateView.setLayoutParams(params);
|
||||
}
|
||||
|
||||
private void hidePlaybackSpeedToggle() {
|
||||
@@ -276,7 +280,11 @@ public class ConversationItemFooter extends ConstraintLayout {
|
||||
}
|
||||
});
|
||||
|
||||
dateView.setMaxWidth(Integer.MAX_VALUE);
|
||||
ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) dateView.getLayoutParams();
|
||||
params.startToEnd = ConstraintLayout.LayoutParams.UNSET;
|
||||
params.constrainedWidth = false;
|
||||
params.horizontalBias = 0.5f;
|
||||
dateView.setLayoutParams(params);
|
||||
}
|
||||
|
||||
private @NonNull Rect getPlaybackSpeedToggleTouchDelegateRect() {
|
||||
|
||||
@@ -9,10 +9,10 @@ import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.signal.core.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.SimpleEmojiTextView;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.util.DrawableUtil;
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.Shader
|
||||
import android.graphics.drawable.Drawable
|
||||
|
||||
/**
|
||||
* Draws [bitmap] as a repeating tiled pattern rotated by [rotationDegrees].
|
||||
*/
|
||||
class RotatedTiledDrawable(
|
||||
private val bitmap: Bitmap,
|
||||
private val rotationDegrees: Float
|
||||
) : Drawable() {
|
||||
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
shader = android.graphics.BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
|
||||
}
|
||||
|
||||
override fun onBoundsChange(bounds: android.graphics.Rect) {
|
||||
paint.shader.setLocalMatrix(
|
||||
Matrix().apply { setRotate(rotationDegrees, bounds.exactCenterX(), bounds.exactCenterY()) }
|
||||
)
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
canvas.drawRect(bounds, paint)
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
paint.alpha = alpha
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||
paint.colorFilter = colorFilter
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
|
||||
}
|
||||
+47
-8
@@ -5,18 +5,41 @@
|
||||
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import java.util.WeakHashMap
|
||||
|
||||
/**
|
||||
* Applies temporary screenshot security for the given component lifecycle.
|
||||
*
|
||||
* Multiple callers can request security on the same window concurrently; the
|
||||
* flag is only cleared once every caller has released its hold.
|
||||
*/
|
||||
object TemporaryScreenshotSecurity {
|
||||
|
||||
private val activeHolds = WeakHashMap<Window, Int>()
|
||||
|
||||
@Composable
|
||||
fun bind() {
|
||||
val activity = LocalActivity.current as? ComponentActivity ?: return
|
||||
|
||||
DisposableEffect(activity) {
|
||||
acquire(activity)
|
||||
|
||||
onDispose {
|
||||
release(activity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun bindToViewLifecycleOwner(fragment: Fragment) {
|
||||
val observer = LifecycleObserver { fragment.requireActivity() }
|
||||
@@ -31,21 +54,37 @@ object TemporaryScreenshotSecurity {
|
||||
activity.lifecycle.addObserver(observer)
|
||||
}
|
||||
|
||||
private fun acquire(activity: ComponentActivity) {
|
||||
val window = activity.window
|
||||
val previous = activeHolds[window] ?: 0
|
||||
activeHolds[window] = previous + 1
|
||||
if (previous == 0 && !TextSecurePreferences.isScreenSecurityEnabled(activity)) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun release(activity: ComponentActivity) {
|
||||
val window = activity.window
|
||||
val next = ((activeHolds[window] ?: 0) - 1).coerceAtLeast(0)
|
||||
if (next == 0) {
|
||||
activeHolds.remove(window)
|
||||
if (!TextSecurePreferences.isScreenSecurityEnabled(activity)) {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
} else {
|
||||
activeHolds[window] = next
|
||||
}
|
||||
}
|
||||
|
||||
private class LifecycleObserver(
|
||||
private val activityProvider: () -> ComponentActivity
|
||||
) : DefaultLifecycleObserver {
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
val activity = activityProvider()
|
||||
if (!TextSecurePreferences.isScreenSecurityEnabled(activity)) {
|
||||
activity.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
acquire(activityProvider())
|
||||
}
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
val activity = activityProvider()
|
||||
if (!TextSecurePreferences.isScreenSecurityEnabled(activity)) {
|
||||
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
release(activityProvider())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -69,12 +69,14 @@ fun <Reason> SendSupportEmailEffect(
|
||||
filterRes: ContactSupportCallbacks.StringForReason<Reason>,
|
||||
hide: () -> Unit
|
||||
) {
|
||||
val subject = stringResource(subjectRes(contactSupportState.reason))
|
||||
val helpDebugLog = stringResource(R.string.HelpFragment__debug_log)
|
||||
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(contactSupportState.sendEmail) {
|
||||
if (contactSupportState.sendEmail) {
|
||||
val subject = context.getString(subjectRes(contactSupportState.reason))
|
||||
val prefix = if (contactSupportState.debugLogUrl != null) {
|
||||
"\n${context.getString(R.string.HelpFragment__debug_log)} ${contactSupportState.debugLogUrl}\n\n"
|
||||
"\n$helpDebugLog ${contactSupportState.debugLogUrl}\n\n"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
@@ -15,11 +15,11 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearSmoothScroller;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.signal.core.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
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.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel;
|
||||
|
||||
@@ -243,7 +243,11 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
return;
|
||||
}
|
||||
|
||||
textView.setPrecomputedText(precomputedTextCompat);
|
||||
try {
|
||||
textView.setPrecomputedText(precomputedTextCompat);
|
||||
} catch (IllegalArgumentException e) {
|
||||
textView.setText(text, type);
|
||||
}
|
||||
|
||||
if (textView.sizeChangeInProgress) {
|
||||
textView.sizeChangeInProgress = false;
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.AppCompatImageButton;
|
||||
|
||||
import org.signal.core.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
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 {
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.components.emoji;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
@@ -15,6 +14,7 @@ import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
||||
import org.signal.core.util.ContextUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.InputAwareLayout.InputView;
|
||||
@@ -29,12 +29,12 @@ public class MediaKeyboard extends FrameLayout implements InputView {
|
||||
private static final String EMOJI_SEARCH = "emoji_search_fragment";
|
||||
|
||||
@Nullable private MediaKeyboardListener keyboardListener;
|
||||
private boolean isInitialised;
|
||||
private int latestKeyboardHeight;
|
||||
private State keyboardState;
|
||||
private KeyboardPagerFragment keyboardPagerFragment;
|
||||
private FragmentManager fragmentManager;
|
||||
private int mediaKeyboardTheme;
|
||||
private boolean isInitialised;
|
||||
private int latestKeyboardHeight;
|
||||
private State keyboardState;
|
||||
private KeyboardPagerFragment keyboardPagerFragment;
|
||||
private FragmentManager fragmentManager;
|
||||
private int mediaKeyboardTheme;
|
||||
|
||||
public MediaKeyboard(Context context) {
|
||||
this(context, null);
|
||||
@@ -175,7 +175,7 @@ public class MediaKeyboard extends FrameLayout implements InputView {
|
||||
LayoutInflater.from(getContext()).inflate(R.layout.media_keyboard, this, true);
|
||||
|
||||
if (fragmentManager == null) {
|
||||
FragmentActivity activity = resolveActivity(getContext());
|
||||
FragmentActivity activity = ContextUtil.requireFragmentActivity(getContext());
|
||||
fragmentManager = activity.getSupportFragmentManager();
|
||||
}
|
||||
|
||||
@@ -188,25 +188,17 @@ public class MediaKeyboard extends FrameLayout implements InputView {
|
||||
.replace(R.id.media_keyboard_fragment_container, keyboardPagerFragment, TAG)
|
||||
.commitNowAllowingStateLoss();
|
||||
|
||||
keyboardState = State.NORMAL;
|
||||
latestKeyboardHeight = -1;
|
||||
isInitialised = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static FragmentActivity resolveActivity(@Nullable Context context) {
|
||||
if (context instanceof FragmentActivity) {
|
||||
return (FragmentActivity) context;
|
||||
} else if (context instanceof ContextThemeWrapper) {
|
||||
return resolveActivity(((ContextThemeWrapper) context).getBaseContext());
|
||||
} else {
|
||||
throw new IllegalStateException("Could not locate FragmentActivity");
|
||||
keyboardState = State.NORMAL;
|
||||
latestKeyboardHeight = -1;
|
||||
isInitialised = true;
|
||||
}
|
||||
}
|
||||
|
||||
public interface MediaKeyboardListener {
|
||||
void onShown();
|
||||
|
||||
void onHidden();
|
||||
|
||||
void onKeyboardChanged(@NonNull KeyboardPage page);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -11,9 +11,9 @@ import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
|
||||
import org.signal.core.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.spoiler.SpoilerAnnotation;
|
||||
import org.thoughtcrime.securesms.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.util.DrawableUtil;
|
||||
import org.signal.core.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
+4
-2
@@ -31,6 +31,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLocale
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
@@ -507,6 +508,7 @@ private fun ActiveBackupsRow(
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
|
||||
val locale = LocalLocale.current.platformLocale
|
||||
when (val type = backupState.messageBackupsType) {
|
||||
is MessageBackupsType.Paid -> {
|
||||
val body = if (backupState is BackupState.Canceled) {
|
||||
@@ -514,13 +516,13 @@ private fun ActiveBackupsRow(
|
||||
} else if (type.pricePerMonth.amount == BigDecimal.ZERO) {
|
||||
stringResource(
|
||||
R.string.BackupsSettingsFragment_renews_s,
|
||||
DateUtils.formatDateWithYear(Locale.getDefault(), backupState.renewalTime.inWholeMilliseconds)
|
||||
DateUtils.formatDateWithYear(locale, backupState.renewalTime.inWholeMilliseconds)
|
||||
)
|
||||
} else {
|
||||
stringResource(
|
||||
R.string.BackupsSettingsFragment_s_month_renews_s,
|
||||
FiatMoneyUtil.format(LocalContext.current.resources, type.pricePerMonth),
|
||||
DateUtils.formatDateWithYear(Locale.getDefault(), backupState.renewalTime.inWholeMilliseconds)
|
||||
DateUtils.formatDateWithYear(locale, backupState.renewalTime.inWholeMilliseconds)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms.components.settings.app.backups.local
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
@@ -176,6 +177,7 @@ class LocalBackupsFragment : ComposeFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("LocalContextGetResourceValueCall")
|
||||
@Composable
|
||||
private fun rememberChooseBackupLocationLauncher(backStack: NavBackStack<NavKey>): ActivityResultLauncher<Uri?> {
|
||||
val context = LocalContext.current
|
||||
|
||||
+6
-2
@@ -54,6 +54,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLocale
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
@@ -1051,9 +1052,10 @@ private fun BackupCard(
|
||||
else -> error("Not supported here.")
|
||||
}
|
||||
|
||||
val locale = LocalLocale.current.platformLocale
|
||||
if (backupState.renewalTime > 0.seconds) {
|
||||
Text(
|
||||
text = stringResource(resource, DateUtils.formatDateWithYear(Locale.getDefault(), backupState.renewalTime.inWholeMilliseconds))
|
||||
text = stringResource(resource, DateUtils.formatDateWithYear(locale, backupState.renewalTime.inWholeMilliseconds))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1873,8 +1875,10 @@ private fun ErrorCardPreview() {
|
||||
@Composable
|
||||
private fun PendingCardPreview() {
|
||||
Previews.Preview {
|
||||
val locale = LocalLocale.current.platformLocale
|
||||
|
||||
PendingCard(
|
||||
price = FiatMoney(BigDecimal.TEN, Currency.getInstance(Locale.getDefault()))
|
||||
price = FiatMoney(BigDecimal.TEN, Currency.getInstance(locale))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -103,6 +103,10 @@ class ChangeNumberVerifyFragment : LoggingFragment(R.layout.fragment_change_phon
|
||||
|
||||
is VerificationCodeRequestResult.ChallengeRequired -> {
|
||||
Log.i(TAG, "Unable to request sms code due to challenges required: ${castResult.challenges.joinToString { it.key }}")
|
||||
if (castResult.challenges.isEmpty()) {
|
||||
Log.w(TAG, "Challenge required but no challenges listed, showing error.")
|
||||
showErrorDialog(R.string.RegistrationActivity_sms_provider_error)
|
||||
}
|
||||
}
|
||||
|
||||
is VerificationCodeRequestResult.RateLimited -> {
|
||||
|
||||
+16
@@ -27,6 +27,7 @@ import org.signal.core.util.bytes
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
|
||||
import org.thoughtcrime.securesms.mms.SentMediaQuality
|
||||
import org.thoughtcrime.securesms.util.AttachmentUtil
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.webrtc.CallDataMode
|
||||
import kotlin.math.abs
|
||||
@@ -169,6 +170,21 @@ private fun DataAndStorageSettingsScreen(
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.DataAndStorageSettingsFragment__voice_messages_and_stickers_under_size_are_always_auto_downloaded,
|
||||
AttachmentUtil.SMALL_ATTACHMENT_SIZE.toUnitString()
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Dividers.Default()
|
||||
}
|
||||
|
||||
+17
@@ -207,12 +207,21 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from("Force split pane UI on phones."),
|
||||
isEnabled = !state.forceSinglePane,
|
||||
isChecked = state.forceSplitPane,
|
||||
onClick = {
|
||||
viewModel.setForceSplitPane(!state.forceSplitPane)
|
||||
}
|
||||
)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from("Force single-pane on newer devices."),
|
||||
isChecked = state.forceSinglePane,
|
||||
onClick = {
|
||||
viewModel.setForceSinglePane(!state.forceSinglePane)
|
||||
}
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from("Display enable permission sheet"),
|
||||
onClick = {
|
||||
@@ -370,6 +379,14 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
}
|
||||
)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from("Enable ANR-induced crashing"),
|
||||
isChecked = SignalStore.internal.anrDetectionCrashes,
|
||||
onClick = {
|
||||
SignalStore.internal.anrDetectionCrashes = !SignalStore.internal.anrDetectionCrashes
|
||||
}
|
||||
)
|
||||
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(DSLSettingsText.from("Logging"))
|
||||
|
||||
+5
-1
@@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.RemoteMegaphoneRecord
|
||||
import org.thoughtcrime.securesms.database.model.addButton
|
||||
import org.thoughtcrime.securesms.database.model.addLink
|
||||
import org.thoughtcrime.securesms.database.model.addStyle
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
@@ -48,9 +49,12 @@ class InternalSettingsRepository(context: Context) {
|
||||
|
||||
val title = "Release Note Title"
|
||||
val bodyText = "Release note body. Aren't I awesome?"
|
||||
val body = "$title\n\n$bodyText"
|
||||
val linkUrl = "https://signal.org"
|
||||
val body = "$title\n\n$bodyText\n\n$linkUrl"
|
||||
val linkStart = body.length - linkUrl.length
|
||||
val bodyRangeList = BodyRangeList.Builder()
|
||||
.addStyle(BodyRangeList.BodyRange.Style.BOLD, 0, title.length)
|
||||
.addLink(linkUrl, linkStart, linkUrl.length)
|
||||
|
||||
bodyRangeList.addButton("Call to Action Text", callToAction, body.lastIndex, 0)
|
||||
|
||||
|
||||
+1
@@ -31,6 +31,7 @@ data class InternalSettingsState(
|
||||
val hasPendingOneTimeDonation: Boolean,
|
||||
val hevcEncoding: Boolean,
|
||||
val forceSplitPane: Boolean,
|
||||
val forceSinglePane: Boolean,
|
||||
val useNewMediaActivity: Boolean,
|
||||
val disableInternalUser: Boolean
|
||||
)
|
||||
|
||||
+6
@@ -203,6 +203,7 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
hasPendingOneTimeDonation = SignalStore.inAppPayments.getPendingOneTimeDonation() != null,
|
||||
hevcEncoding = SignalStore.internal.hevcEncoding,
|
||||
forceSplitPane = SignalStore.internal.forceSplitPane,
|
||||
forceSinglePane = SignalStore.internal.forceSinglePane,
|
||||
useNewMediaActivity = SignalStore.internal.useNewMediaActivity,
|
||||
disableInternalUser = RemoteConfig.internalUserDisabled
|
||||
)
|
||||
@@ -225,6 +226,11 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setForceSinglePane(forceSinglePane: Boolean) {
|
||||
SignalStore.internal.forceSinglePane = forceSinglePane
|
||||
refresh()
|
||||
}
|
||||
|
||||
class Factory(private val repository: InternalSettingsRepository) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(modelClass.cast(InternalSettingsViewModel(repository)))
|
||||
|
||||
+1
-1
@@ -50,8 +50,8 @@ import org.signal.core.ui.compose.ComposeFragment
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalIcons
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.core.util.Util
|
||||
import org.signal.libsignal.protocol.util.Hex
|
||||
import org.thoughtcrime.securesms.components.settings.app.internal.sqlite.InternalSqlitePlaygroundViewModel.QueryResult
|
||||
|
||||
class InternalSqlitePlaygroundFragment : ComposeFragment() {
|
||||
|
||||
+2
-2
@@ -34,9 +34,9 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.boost.Boost
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewaySelectorBottomSheet
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.thanks.ThanksForYourSupportBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.ui.CurrencySelection
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.ui.NetworkFailure
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
|
||||
+1
-2
@@ -46,8 +46,7 @@ sealed interface GatewayOrderStrategy {
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getStrategy(): GatewayOrderStrategy {
|
||||
val self = Recipient.self()
|
||||
fun getStrategy(self: Recipient = Recipient.self()): GatewayOrderStrategy {
|
||||
val e164 = self.e164.orNull() ?: return Default
|
||||
|
||||
return if (PhoneNumberUtil.getInstance().parse(e164, "").countryCode == 1) {
|
||||
|
||||
+39
-161
@@ -1,34 +1,28 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment
|
||||
import org.signal.core.util.dp
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.models.BadgeDisplay112
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.GooglePayComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.IdealWeroButton
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.PayPalButton
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadingCircle
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.payments.currency.CurrencyUtil
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
@@ -36,172 +30,56 @@ import org.signal.core.ui.R as CoreUiR
|
||||
/**
|
||||
* Entry point to capturing the necessary payment token to pay for a donation
|
||||
*/
|
||||
class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
class GatewaySelectorBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
|
||||
private val args: GatewaySelectorBottomSheetArgs by navArgs()
|
||||
override val peekHeightPercentage: Float = 1f
|
||||
|
||||
private val viewModel: GatewaySelectorViewModel by viewModel {
|
||||
GatewaySelectorViewModel(args, requireListener<GooglePayComponent>().googlePayRepository)
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
BadgeDisplay112.register(adapter)
|
||||
GooglePayButton.register(adapter)
|
||||
PayPalButton.register(adapter)
|
||||
IndeterminateLoadingCircle.register(adapter)
|
||||
IdealWeroButton.register(adapter)
|
||||
@Composable
|
||||
override fun SheetContent() {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
||||
|
||||
lifecycleDisposable += viewModel.state.subscribe { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
}
|
||||
GatewaySelectorBottomSheetContent(state, onEvent = this::onEvent)
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: GatewaySelectorState): DSLConfiguration {
|
||||
return when (state) {
|
||||
GatewaySelectorState.Loading -> {
|
||||
configure {
|
||||
space(16.dp)
|
||||
customPref(IndeterminateLoadingCircle)
|
||||
space(16.dp)
|
||||
private fun onEvent(event: GatewaySelectorBottomSheetEvent) {
|
||||
when (event) {
|
||||
GatewaySelectorBottomSheetEvent.GOOGLE_PAY_SELECTED -> {
|
||||
setPaymentMethodAndDismiss(InAppPaymentData.PaymentMethodType.GOOGLE_PAY)
|
||||
}
|
||||
|
||||
GatewaySelectorBottomSheetEvent.PAYPAL_SELECTED -> {
|
||||
setPaymentMethodAndDismiss(InAppPaymentData.PaymentMethodType.PAYPAL)
|
||||
}
|
||||
|
||||
GatewaySelectorBottomSheetEvent.SEPA_SELECTED -> {
|
||||
if (viewModel.checkIsSepaPaymentValidAmount()) {
|
||||
setPaymentMethodAndDismiss(InAppPaymentData.PaymentMethodType.SEPA_DEBIT)
|
||||
} else {
|
||||
findNavController().popBackStack()
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(FAILURE_KEY to true, SEPA_EURO_MAX to viewModel.getSepaMaximum()))
|
||||
}
|
||||
}
|
||||
is GatewaySelectorState.Ready -> {
|
||||
configure {
|
||||
customPref(
|
||||
BadgeDisplay112.Model(
|
||||
badge = state.inAppPayment.data.badge!!.let { Badges.fromDatabaseBadge(it) },
|
||||
withDisplayText = false
|
||||
)
|
||||
)
|
||||
|
||||
space(12.dp)
|
||||
GatewaySelectorBottomSheetEvent.IDEAL_SELECTED -> {
|
||||
setPaymentMethodAndDismiss(InAppPaymentData.PaymentMethodType.IDEAL)
|
||||
}
|
||||
|
||||
presentTitleAndSubtitle(requireContext(), state.inAppPayment)
|
||||
|
||||
space(16.dp)
|
||||
|
||||
state.gatewayOrderStrategy.orderedGateways.forEach { gateway ->
|
||||
when (gateway) {
|
||||
InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING -> error("Unsupported payment method.")
|
||||
InAppPaymentData.PaymentMethodType.GOOGLE_PAY -> renderGooglePayButton(state)
|
||||
InAppPaymentData.PaymentMethodType.PAYPAL -> renderPayPalButton(state)
|
||||
InAppPaymentData.PaymentMethodType.CARD -> renderCreditCardButton(state)
|
||||
InAppPaymentData.PaymentMethodType.SEPA_DEBIT -> renderSEPADebitButton(state)
|
||||
InAppPaymentData.PaymentMethodType.IDEAL -> renderIDEALButton(state)
|
||||
InAppPaymentData.PaymentMethodType.UNKNOWN -> error("Unsupported payment method.")
|
||||
}
|
||||
}
|
||||
|
||||
space(16.dp)
|
||||
}
|
||||
GatewaySelectorBottomSheetEvent.CREDIT_CARD_SELECTED -> {
|
||||
setPaymentMethodAndDismiss(InAppPaymentData.PaymentMethodType.CARD)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.renderGooglePayButton(state: GatewaySelectorState.Ready) {
|
||||
if (state.isGooglePayAvailable) {
|
||||
space(16.dp)
|
||||
|
||||
customPref(
|
||||
GooglePayButton.Model(
|
||||
isEnabled = true,
|
||||
onClick = {
|
||||
lifecycleDisposable += viewModel.updateInAppPaymentMethod(InAppPaymentData.PaymentMethodType.GOOGLE_PAY)
|
||||
.subscribeBy {
|
||||
findNavController().popBackStack()
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to it))
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.renderPayPalButton(state: GatewaySelectorState.Ready) {
|
||||
if (state.isPayPalAvailable) {
|
||||
space(16.dp)
|
||||
|
||||
customPref(
|
||||
PayPalButton.Model(
|
||||
onClick = {
|
||||
lifecycleDisposable += viewModel.updateInAppPaymentMethod(InAppPaymentData.PaymentMethodType.PAYPAL)
|
||||
.subscribeBy {
|
||||
findNavController().popBackStack()
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to it))
|
||||
}
|
||||
},
|
||||
isEnabled = true
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.renderCreditCardButton(state: GatewaySelectorState.Ready) {
|
||||
if (state.isCreditCardAvailable) {
|
||||
space(16.dp)
|
||||
|
||||
primaryButton(
|
||||
text = DSLSettingsText.from(R.string.GatewaySelectorBottomSheet__credit_or_debit_card),
|
||||
icon = DSLSettingsIcon.from(R.drawable.credit_card, CoreUiR.color.signal_colorOnCustom),
|
||||
disableOnClick = true,
|
||||
onClick = {
|
||||
lifecycleDisposable += viewModel.updateInAppPaymentMethod(InAppPaymentData.PaymentMethodType.CARD)
|
||||
.subscribeBy {
|
||||
findNavController().popBackStack()
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to it))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.renderSEPADebitButton(state: GatewaySelectorState.Ready) {
|
||||
if (state.isSEPADebitAvailable) {
|
||||
space(16.dp)
|
||||
|
||||
tonalButton(
|
||||
text = DSLSettingsText.from(R.string.GatewaySelectorBottomSheet__bank_transfer),
|
||||
icon = DSLSettingsIcon.from(R.drawable.bank_transfer),
|
||||
disableOnClick = true,
|
||||
onClick = {
|
||||
val price = state.inAppPayment.data.amount!!.toFiatMoney()
|
||||
if (state.sepaEuroMaximum != null &&
|
||||
price.currency == CurrencyUtil.EURO &&
|
||||
price.amount > state.sepaEuroMaximum.amount
|
||||
) {
|
||||
findNavController().popBackStack()
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(FAILURE_KEY to true, SEPA_EURO_MAX to state.sepaEuroMaximum.amount))
|
||||
} else {
|
||||
lifecycleDisposable += viewModel.updateInAppPaymentMethod(InAppPaymentData.PaymentMethodType.SEPA_DEBIT)
|
||||
.subscribeBy {
|
||||
findNavController().popBackStack()
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to it))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.renderIDEALButton(state: GatewaySelectorState.Ready) {
|
||||
if (state.isIDEALAvailable) {
|
||||
space(16.dp)
|
||||
|
||||
customPref(
|
||||
IdealWeroButton.Model(
|
||||
onClick = {
|
||||
lifecycleDisposable += viewModel.updateInAppPaymentMethod(InAppPaymentData.PaymentMethodType.IDEAL)
|
||||
.subscribeBy {
|
||||
findNavController().popBackStack()
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to it))
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
private fun setPaymentMethodAndDismiss(type: InAppPaymentData.PaymentMethodType) {
|
||||
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Main) {
|
||||
val inAppPayment = viewModel.updateInAppPaymentMethod(type)
|
||||
findNavController().popBackStack()
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to inAppPayment))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+366
@@ -0,0 +1,366 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.BottomSheets
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.horizontalGutters
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.DonateWithGooglePayButton
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.BadgeImage112
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toDecimalValue
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.ui.IdealWeroButton
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.ui.PayPalButton
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BadgeList
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.FiatValue
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@Composable
|
||||
fun GatewaySelectorBottomSheetContent(
|
||||
state: GatewaySelectorState,
|
||||
onEvent: (GatewaySelectorBottomSheetEvent) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier
|
||||
.testTag(GatewaySelectorTestTags.CONTAINER)
|
||||
.verticalScroll(scrollState)
|
||||
.horizontalGutters()
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
BottomSheets.Handle()
|
||||
|
||||
when (state) {
|
||||
GatewaySelectorState.Loading -> Loading()
|
||||
is GatewaySelectorState.Ready -> Ready(state, onEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Loading() {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.padding(vertical = 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Ready(state: GatewaySelectorState.Ready, onEvent: (GatewaySelectorBottomSheetEvent) -> Unit) {
|
||||
BadgeImage112(
|
||||
badge = state.inAppPayment.data.badge!!.let { Badges.fromDatabaseBadge(it) },
|
||||
modifier = Modifier.size(112.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.size(12.dp))
|
||||
|
||||
TitleAndSubtitle(state.inAppPayment)
|
||||
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
|
||||
var isGatewaySelected by remember { mutableStateOf(false) }
|
||||
val onGatewaySelected: (GatewaySelectorBottomSheetEvent) -> Unit = remember(onEvent) {
|
||||
{
|
||||
if (!isGatewaySelected) {
|
||||
isGatewaySelected = true
|
||||
onEvent(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.gatewayOrderStrategy.orderedGateways.forEach {
|
||||
when (it) {
|
||||
InAppPaymentData.PaymentMethodType.UNKNOWN -> error("Unsupported payment method.")
|
||||
InAppPaymentData.PaymentMethodType.GOOGLE_PAY -> {
|
||||
if (state.isGooglePayAvailable) {
|
||||
DonateWithGooglePayButton(
|
||||
onClick = { onGatewaySelected(GatewaySelectorBottomSheetEvent.GOOGLE_PAY_SELECTED) },
|
||||
enabled = !isGatewaySelected,
|
||||
modifier = Modifier
|
||||
.testTag(GatewaySelectorTestTags.GOOGLE_PAY_BUTTON)
|
||||
.padding(top = 16.dp)
|
||||
.fillMaxWidth()
|
||||
.height(44.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
InAppPaymentData.PaymentMethodType.CARD -> {
|
||||
if (state.isCreditCardAvailable) {
|
||||
Buttons.LargePrimary(
|
||||
onClick = { onGatewaySelected(GatewaySelectorBottomSheetEvent.CREDIT_CARD_SELECTED) },
|
||||
enabled = !isGatewaySelected,
|
||||
modifier = Modifier
|
||||
.testTag(GatewaySelectorTestTags.CREDIT_CARD_BUTTON)
|
||||
.padding(top = 16.dp)
|
||||
.fillMaxWidth()
|
||||
.height(44.dp)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.credit_card),
|
||||
contentDescription = null
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.GatewaySelectorBottomSheet__credit_or_debit_card)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InAppPaymentData.PaymentMethodType.SEPA_DEBIT -> {
|
||||
if (state.isSEPADebitAvailable) {
|
||||
Buttons.LargeTonal(
|
||||
onClick = { onGatewaySelected(GatewaySelectorBottomSheetEvent.SEPA_SELECTED) },
|
||||
enabled = !isGatewaySelected,
|
||||
modifier = Modifier
|
||||
.testTag(GatewaySelectorTestTags.SEPA_BUTTON)
|
||||
.padding(top = 16.dp)
|
||||
.fillMaxWidth()
|
||||
.height(44.dp)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.bank_transfer),
|
||||
contentDescription = null
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.GatewaySelectorBottomSheet__bank_transfer)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InAppPaymentData.PaymentMethodType.IDEAL -> {
|
||||
if (state.isIDEALAvailable) {
|
||||
IdealWeroButton(
|
||||
onClick = { onGatewaySelected(GatewaySelectorBottomSheetEvent.IDEAL_SELECTED) },
|
||||
enabled = !isGatewaySelected,
|
||||
modifier = Modifier
|
||||
.testTag(GatewaySelectorTestTags.IDEAL_BUTTON)
|
||||
.padding(top = 16.dp)
|
||||
.height(44.dp)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
InAppPaymentData.PaymentMethodType.PAYPAL -> {
|
||||
if (state.isPayPalAvailable) {
|
||||
PayPalButton(
|
||||
enabled = !isGatewaySelected,
|
||||
onClick = { onGatewaySelected(GatewaySelectorBottomSheetEvent.PAYPAL_SELECTED) },
|
||||
modifier = Modifier
|
||||
.testTag(GatewaySelectorTestTags.PAYPAL_BUTTON)
|
||||
.padding(top = 16.dp)
|
||||
.height(44.dp)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING -> error("Unsupported payment method.")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun GatewaySelectorBottomSheetContentLoadingPreview() {
|
||||
Previews.BottomSheetContentPreview {
|
||||
GatewaySelectorBottomSheetContent(
|
||||
state = GatewaySelectorState.Loading,
|
||||
onEvent = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TitleAndSubtitle(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
when (inAppPayment.type) {
|
||||
InAppPaymentType.UNKNOWN -> error("Unsupported type UNKNOWN")
|
||||
InAppPaymentType.ONE_TIME_GIFT -> OneTimeGiftTitleAndSubtitle(inAppPayment)
|
||||
InAppPaymentType.ONE_TIME_DONATION -> RecurringDonationTitleAndSubtitle(inAppPayment)
|
||||
InAppPaymentType.RECURRING_DONATION -> OneTimeDonationTitleAndSubtitle(inAppPayment)
|
||||
InAppPaymentType.RECURRING_BACKUP -> error("This type is not supported")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RecurringDonationTitleAndSubtitle(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
Text(
|
||||
text = stringResource(R.string.GatewaySelectorBottomSheet__donate_s_month_to_signal, rememberFormattedAmount(inAppPayment)),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.padding(bottom = 6.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.GatewaySelectorBottomSheet__get_a_s_badge, inAppPayment.data.badge!!.name),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OneTimeDonationTitleAndSubtitle(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
Text(
|
||||
text = stringResource(R.string.GatewaySelectorBottomSheet__donate_s_to_signal, rememberFormattedAmount(inAppPayment)),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.padding(bottom = 6.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = pluralStringResource(R.plurals.GatewaySelectorBottomSheet__get_a_s_badge_for_d_days, 30, inAppPayment.data.badge!!.name, 30),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OneTimeGiftTitleAndSubtitle(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
Text(
|
||||
text = stringResource(R.string.GatewaySelectorBottomSheet__donate_s_to_signal, rememberFormattedAmount(inAppPayment)),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.padding(bottom = 6.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.GatewaySelectorBottomSheet__donate_for_a_friend),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberFormattedAmount(inAppPayment: InAppPaymentTable.InAppPayment): String {
|
||||
val resources = LocalResources.current
|
||||
return remember(inAppPayment.data.amount) {
|
||||
FiatMoneyUtil.format(resources, inAppPayment.data.amount!!.toFiatMoney())
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun GatewaySelectorBottomSheetContentReadyOneTimeDonationPreview() {
|
||||
Previews.BottomSheetContentPreview {
|
||||
GatewaySelectorBottomSheetContent(
|
||||
state = rememberGatewaySelectorBottomSheetContentPreviewState(InAppPaymentType.ONE_TIME_DONATION),
|
||||
onEvent = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun GatewaySelectorBottomSheetContentReadyRecurringDonationPreview() {
|
||||
Previews.BottomSheetContentPreview {
|
||||
GatewaySelectorBottomSheetContent(
|
||||
state = rememberGatewaySelectorBottomSheetContentPreviewState(InAppPaymentType.RECURRING_DONATION),
|
||||
onEvent = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun GatewaySelectorBottomSheetContentReadyOneTimeGiftDonationPreview() {
|
||||
Previews.BottomSheetContentPreview {
|
||||
GatewaySelectorBottomSheetContent(
|
||||
state = rememberGatewaySelectorBottomSheetContentPreviewState(InAppPaymentType.ONE_TIME_GIFT),
|
||||
onEvent = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@VisibleForTesting
|
||||
fun rememberGatewaySelectorBottomSheetContentPreviewState(type: InAppPaymentType): GatewaySelectorState.Ready {
|
||||
return remember {
|
||||
GatewaySelectorState.Ready(
|
||||
inAppPayment = InAppPaymentTable.InAppPayment(
|
||||
id = InAppPaymentTable.InAppPaymentId(1),
|
||||
type = type,
|
||||
state = InAppPaymentTable.State.CREATED,
|
||||
insertedAt = 1.milliseconds,
|
||||
updatedAt = 1.milliseconds,
|
||||
notified = true,
|
||||
subscriberId = null,
|
||||
endOfPeriod = 0.milliseconds,
|
||||
data = InAppPaymentData(
|
||||
badge = BadgeList.Badge(
|
||||
name = type.name.lowercase()
|
||||
),
|
||||
amount = FiatValue(currencyCode = "USD", amount = BigDecimal.TEN.toDecimalValue())
|
||||
)
|
||||
),
|
||||
gatewayOrderStrategy = GatewayOrderStrategy.getStrategy(
|
||||
self = Recipient(
|
||||
isResolving = false,
|
||||
e164Value = "+15555555555"
|
||||
)
|
||||
),
|
||||
isGooglePayAvailable = true,
|
||||
isPayPalAvailable = true,
|
||||
isCreditCardAvailable = true,
|
||||
isSEPADebitAvailable = true,
|
||||
isIDEALAvailable = true,
|
||||
sepaEuroMaximum = FiatMoney(BigDecimal.ONE, Currency.getInstance("USD"))
|
||||
)
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway
|
||||
|
||||
enum class GatewaySelectorBottomSheetEvent {
|
||||
GOOGLE_PAY_SELECTED,
|
||||
PAYPAL_SELECTED,
|
||||
SEPA_SELECTED,
|
||||
IDEAL_SELECTED,
|
||||
CREDIT_CARD_SELECTED
|
||||
}
|
||||
+7
-4
@@ -1,8 +1,9 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway
|
||||
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.getAvailablePaymentMethods
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
@@ -35,8 +36,8 @@ object GatewaySelectorRepository {
|
||||
}
|
||||
}
|
||||
|
||||
fun setInAppPaymentMethodType(inAppPayment: InAppPaymentTable.InAppPayment, paymentMethodType: InAppPaymentData.PaymentMethodType): Single<InAppPaymentTable.InAppPayment> {
|
||||
return Single.fromCallable {
|
||||
suspend fun setInAppPaymentMethodType(inAppPayment: InAppPaymentTable.InAppPayment, paymentMethodType: InAppPaymentData.PaymentMethodType): InAppPaymentTable.InAppPayment {
|
||||
return withContext(Dispatchers.IO) {
|
||||
SignalDatabase.inAppPayments.update(
|
||||
inAppPayment.copy(
|
||||
data = inAppPayment.data.copy(
|
||||
@@ -44,7 +45,9 @@ object GatewaySelectorRepository {
|
||||
)
|
||||
)
|
||||
)
|
||||
}.flatMap { InAppPaymentsRepository.requireInAppPayment(inAppPayment.id) }
|
||||
|
||||
SignalDatabase.inAppPayments.getById(inAppPayment.id) ?: throw Exception("Not found.")
|
||||
}
|
||||
}
|
||||
|
||||
data class GatewayConfiguration(
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway
|
||||
|
||||
object GatewaySelectorTestTags {
|
||||
const val CONTAINER = "container"
|
||||
const val GOOGLE_PAY_BUTTON = "google_pay_button"
|
||||
const val PAYPAL_BUTTON = "paypal_button"
|
||||
const val CREDIT_CARD_BUTTON = "credit_card_button"
|
||||
const val SEPA_BUTTON = "sepa_button"
|
||||
const val IDEAL_BUTTON = "ideal_button"
|
||||
}
|
||||
+27
-8
@@ -1,29 +1,33 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.GooglePayRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppDonations
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import org.thoughtcrime.securesms.payments.currency.CurrencyUtil
|
||||
import java.math.BigDecimal
|
||||
|
||||
class GatewaySelectorViewModel(
|
||||
args: GatewaySelectorBottomSheetArgs,
|
||||
repository: GooglePayRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val store = RxStore<GatewaySelectorState>(GatewaySelectorState.Loading)
|
||||
private val store = MutableStateFlow<GatewaySelectorState>(GatewaySelectorState.Loading)
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
val state = store.stateFlowable
|
||||
val state = store.asStateFlow()
|
||||
|
||||
init {
|
||||
val inAppPayment = InAppPaymentsRepository.requireInAppPayment(args.inAppPaymentId)
|
||||
@@ -48,13 +52,28 @@ class GatewaySelectorViewModel(
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
store.dispose()
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
fun updateInAppPaymentMethod(inAppPaymentMethodType: InAppPaymentData.PaymentMethodType): Single<InAppPaymentTable.InAppPayment> {
|
||||
val state = store.state as GatewaySelectorState.Ready
|
||||
fun getSepaMaximum(): BigDecimal {
|
||||
val state = store.value as GatewaySelectorState.Ready
|
||||
return state.sepaEuroMaximum!!.amount
|
||||
}
|
||||
|
||||
return GatewaySelectorRepository.setInAppPaymentMethodType(state.inAppPayment, inAppPaymentMethodType).observeOn(AndroidSchedulers.mainThread())
|
||||
fun checkIsSepaPaymentValidAmount(): Boolean {
|
||||
val state = store.value as GatewaySelectorState.Ready
|
||||
|
||||
val price = state.inAppPayment.data.amount!!.toFiatMoney()
|
||||
return !(
|
||||
state.sepaEuroMaximum != null &&
|
||||
price.currency == CurrencyUtil.EURO &&
|
||||
price.amount > state.sepaEuroMaximum.amount
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun updateInAppPaymentMethod(inAppPaymentMethodType: InAppPaymentData.PaymentMethodType): InAppPaymentTable.InAppPayment {
|
||||
val state = store.value as GatewaySelectorState.Ready
|
||||
|
||||
return GatewaySelectorRepository.setInAppPaymentMethodType(state.inAppPayment, inAppPaymentMethodType)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -29,8 +29,8 @@ import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.completed.InAppPaymentsBottomSheetDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.CheckoutFlowActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.thanks.ThanksForYourSupportBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.ui.NetworkFailure
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadingCircle
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.models
|
||||
|
||||
import android.view.View
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
|
||||
object GooglePayButton {
|
||||
|
||||
class Model(val onClick: () -> Unit, override val isEnabled: Boolean) : PreferenceModel<Model>(isEnabled = isEnabled) {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean = true
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||
|
||||
private val googlePayButton: View = findViewById(R.id.googlepay_button)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
googlePayButton.isEnabled = model.isEnabled
|
||||
googlePayButton.setOnClickListener {
|
||||
googlePayButton.isEnabled = false
|
||||
model.onClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun register(adapter: MappingAdapter) {
|
||||
adapter.registerFactory(Model::class.java, LayoutFactory({ ViewHolder(it) }, R.layout.google_pay_button_pref))
|
||||
}
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.models
|
||||
|
||||
import org.thoughtcrime.securesms.databinding.PaypalButtonBinding
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.BindingFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.BindingViewHolder
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
|
||||
object PayPalButton {
|
||||
fun register(mappingAdapter: MappingAdapter) {
|
||||
mappingAdapter.registerFactory(Model::class.java, BindingFactory(::ViewHolder, PaypalButtonBinding::inflate))
|
||||
}
|
||||
|
||||
class Model(val onClick: () -> Unit, val isEnabled: Boolean) : MappingModel<Model> {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean = true
|
||||
override fun areContentsTheSame(newItem: Model): Boolean = isEnabled == newItem.isEnabled
|
||||
}
|
||||
|
||||
class ViewHolder(binding: PaypalButtonBinding) : BindingViewHolder<Model, PaypalButtonBinding>(binding) {
|
||||
override fun bind(model: Model) {
|
||||
binding.paypalButton.isEnabled = model.isEnabled
|
||||
binding.paypalButton.setOnClickListener {
|
||||
binding.paypalButton.isEnabled = false
|
||||
model.onClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.models
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.ui
|
||||
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
+9
-47
@@ -1,19 +1,11 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.models
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.ButtonColors
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
@@ -21,49 +13,19 @@ import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.horizontalGutters
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.components.settings.models.DSLComposePreference
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
|
||||
/**
|
||||
* DSL Ideal | Wero button for the payments gateway.
|
||||
*/
|
||||
object IdealWeroButton {
|
||||
|
||||
@Stable
|
||||
class Model(val onClick: () -> Unit) : PreferenceModel<Model>() {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean = true
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: ComposeView) : DSLComposePreference.ViewHolder<Model>(itemView) {
|
||||
@Composable
|
||||
override fun Content(model: Model) {
|
||||
IdealWeroButton(model)
|
||||
}
|
||||
}
|
||||
|
||||
fun register(adapter: MappingAdapter) {
|
||||
DSLComposePreference.register(adapter) { ViewHolder(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IdealWeroButton(model: IdealWeroButton.Model) {
|
||||
var enabled by remember { mutableStateOf(true) }
|
||||
|
||||
fun IdealWeroButton(
|
||||
onClick: () -> Unit,
|
||||
enabled: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Buttons.LargeTonal(
|
||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
|
||||
onClick = {
|
||||
enabled = false
|
||||
model.onClick()
|
||||
},
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
modifier = Modifier
|
||||
.height(44.dp)
|
||||
.horizontalGutters()
|
||||
.fillMaxWidth(),
|
||||
modifier = modifier,
|
||||
colors = ButtonColors(
|
||||
containerColor = colorResource(org.signal.core.ui.R.color.signal_light_colorPrimaryContainer),
|
||||
contentColor = colorResource(org.signal.core.ui.R.color.signal_light_colorOnPrimaryContainer),
|
||||
@@ -82,6 +44,6 @@ private fun IdealWeroButton(model: IdealWeroButton.Model) {
|
||||
@Composable
|
||||
private fun IdealWeroButtonPreview() {
|
||||
Previews.Preview {
|
||||
IdealWeroButton(model = remember { IdealWeroButton.Model(onClick = {}) })
|
||||
IdealWeroButton(onClick = {}, enabled = true)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.models
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.ui
|
||||
|
||||
import android.view.View
|
||||
import com.google.android.material.button.MaterialButton
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.horizontalGutters
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
@Composable
|
||||
fun PayPalButton(
|
||||
enabled: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val overlayColor = colorResource(org.signal.core.ui.R.color.signal_light_colorTransparent3)
|
||||
Buttons.LargeTonal(
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
contentPadding = PaddingValues.Zero,
|
||||
modifier = modifier.drawWithContent {
|
||||
drawContent()
|
||||
|
||||
if (!enabled) {
|
||||
drawRoundRect(
|
||||
color = overlayColor,
|
||||
cornerRadius = CornerRadius(500f, 500f)
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFFF6C757),
|
||||
disabledContainerColor = Color(0xFFF6C757)
|
||||
)
|
||||
) {
|
||||
Image(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.paypal),
|
||||
contentDescription = stringResource(R.string.BackupsTypeSettingsFragment__paypal)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
fun PayPalButtonPreview() {
|
||||
Previews.Preview {
|
||||
PayPalButton(
|
||||
enabled = true,
|
||||
onClick = {},
|
||||
modifier = Modifier
|
||||
.horizontalGutters()
|
||||
.fillMaxWidth()
|
||||
.height(44.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
fun PayPalButtonDisabledPreview() {
|
||||
Previews.Preview {
|
||||
PayPalButton(
|
||||
enabled = false,
|
||||
onClick = {},
|
||||
modifier = Modifier
|
||||
.horizontalGutters()
|
||||
.fillMaxWidth()
|
||||
.height(44.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
+19
-24
@@ -7,7 +7,6 @@ import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
@@ -49,8 +48,7 @@ import androidx.core.app.ShareCompat
|
||||
import androidx.core.app.TaskStackBuilder
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
@@ -58,8 +56,10 @@ import com.google.accompanist.permissions.PermissionState
|
||||
import com.google.accompanist.permissions.PermissionStatus
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.signal.camera.CameraScreenEvents
|
||||
import org.signal.camera.CameraScreenState
|
||||
import org.signal.camera.CameraScreenViewModel
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.ComposeFragment
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
@@ -69,7 +69,6 @@ import org.signal.core.ui.compose.SignalIcons
|
||||
import org.signal.core.ui.compose.Snackbars
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.signal.core.ui.permissions.Permissions
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData
|
||||
@@ -85,7 +84,6 @@ import java.util.UUID
|
||||
class UsernameLinkSettingsFragment : ComposeFragment() {
|
||||
|
||||
private val viewModel: UsernameLinkSettingsViewModel by viewModels()
|
||||
private val disposables: LifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
private lateinit var galleryLauncher: ActivityResultLauncher<Unit>
|
||||
|
||||
@@ -115,21 +113,29 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
|
||||
val linkCopiedEvent: UUID? by viewModel.linkCopiedEvent
|
||||
val helpText = stringResource(id = R.string.UsernameLinkSettings_scan_this_qr_code)
|
||||
|
||||
val cameraViewModel: CameraScreenViewModel = viewModel { CameraScreenViewModel() }
|
||||
val cameraState by cameraViewModel.state
|
||||
|
||||
val cameraPermissionState: PermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA) {
|
||||
viewModel.onTabSelected(ActiveTab.Scan)
|
||||
}
|
||||
|
||||
LaunchedEffect(cameraViewModel) {
|
||||
cameraViewModel.qrCodeDetected.collect { data ->
|
||||
viewModel.onQrCodeScanned(data)
|
||||
}
|
||||
}
|
||||
|
||||
MainScreen(
|
||||
state = state,
|
||||
navController = navController,
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
disposables = disposables.disposables,
|
||||
cameraState = cameraState,
|
||||
cameraEmitter = cameraViewModel::onEvent,
|
||||
cameraPermissionState = cameraPermissionState,
|
||||
onCodeTabSelected = { viewModel.onTabSelected(ActiveTab.Code) },
|
||||
onScanTabSelected = { viewModel.onTabSelected(ActiveTab.Scan) },
|
||||
onUsernameLinkResetResultHandled = { viewModel.onUsernameLinkResetResultHandled() },
|
||||
onShareBadge = { shareQrBadge(requireActivity(), viewModel.generateQrCodeImage(helpText)) },
|
||||
onQrCodeScanned = { data -> viewModel.onQrCodeScanned(data) },
|
||||
onQrResultHandled = { viewModel.onQrResultHandled() },
|
||||
onOpenCameraClicked = { askCameraPermissions() },
|
||||
onOpenGalleryClicked = { galleryLauncher.launch(Unit) },
|
||||
@@ -143,10 +149,6 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
disposables.bindTo(viewLifecycleOwner)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.onResume()
|
||||
@@ -167,14 +169,13 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
|
||||
private fun MainScreen(
|
||||
state: UsernameLinkSettingsState,
|
||||
navController: NavController? = null,
|
||||
lifecycleOwner: LifecycleOwner = previewLifecycleOwner,
|
||||
disposables: CompositeDisposable = CompositeDisposable(),
|
||||
cameraState: CameraScreenState = CameraScreenState(),
|
||||
cameraEmitter: (CameraScreenEvents) -> Unit = {},
|
||||
cameraPermissionState: PermissionState = previewPermissionState(),
|
||||
onCodeTabSelected: () -> Unit = {},
|
||||
onScanTabSelected: () -> Unit = {},
|
||||
onUsernameLinkResetResultHandled: () -> Unit = {},
|
||||
onShareBadge: () -> Unit = {},
|
||||
onQrCodeScanned: (String) -> Unit = {},
|
||||
onQrResultHandled: () -> Unit = {},
|
||||
onOpenCameraClicked: () -> Unit = {},
|
||||
onOpenGalleryClicked: () -> Unit = {},
|
||||
@@ -238,10 +239,9 @@ private fun MainScreen(
|
||||
exit = slideOutHorizontally(targetOffsetX = { fullWidth -> fullWidth })
|
||||
) {
|
||||
UsernameQrScanScreen(
|
||||
lifecycleOwner = lifecycleOwner,
|
||||
disposables = disposables,
|
||||
qrScanResult = state.qrScanResult,
|
||||
onQrCodeScanned = onQrCodeScanned,
|
||||
cameraState = cameraState,
|
||||
cameraEmitter = cameraEmitter,
|
||||
onQrResultHandled = onQrResultHandled,
|
||||
onOpenCameraClicked = onOpenCameraClicked,
|
||||
onOpenGalleryClicked = onOpenGalleryClicked,
|
||||
@@ -394,11 +394,6 @@ private fun previewPermissionState(): PermissionState {
|
||||
}
|
||||
}
|
||||
|
||||
private val previewLifecycleOwner: LifecycleOwner = object : LifecycleOwner {
|
||||
override val lifecycle: Lifecycle
|
||||
get() = throw UnsupportedOperationException("Only for tests")
|
||||
}
|
||||
|
||||
private fun shareQrBadge(activity: Activity, badge: Bitmap?) {
|
||||
if (badge == null) {
|
||||
return
|
||||
|
||||
+87
-25
@@ -1,45 +1,50 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import org.signal.camera.CameraCaptureMode
|
||||
import org.signal.camera.CameraScreen
|
||||
import org.signal.camera.CameraScreenEvents
|
||||
import org.signal.camera.CameraScreenState
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Dialogs
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.signal.qr.QrScannerView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXRemoteConfig
|
||||
import org.thoughtcrime.securesms.qr.QrScanScreens
|
||||
import org.thoughtcrime.securesms.qr.QrCrosshair
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* A screen that allows you to scan a QR code to start a chat.
|
||||
*/
|
||||
@Composable
|
||||
fun UsernameQrScanScreen(
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
disposables: CompositeDisposable,
|
||||
qrScanResult: QrScanResult?,
|
||||
onQrCodeScanned: (String) -> Unit,
|
||||
cameraState: CameraScreenState,
|
||||
cameraEmitter: (CameraScreenEvents) -> Unit,
|
||||
onQrResultHandled: () -> Unit,
|
||||
onOpenCameraClicked: () -> Unit,
|
||||
onOpenGalleryClicked: () -> Unit,
|
||||
@@ -88,26 +93,49 @@ fun UsernameQrScanScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f, true)
|
||||
.background(Color.Black)
|
||||
) {
|
||||
QrScanScreens.QrScanScreen(
|
||||
factory = { context ->
|
||||
val view = QrScannerView(context)
|
||||
disposables += view.qrData.throttleFirst(3000, TimeUnit.MILLISECONDS).subscribe { data ->
|
||||
onQrCodeScanned(data)
|
||||
if (hasCameraPermission) {
|
||||
CameraScreen(
|
||||
state = cameraState,
|
||||
emitter = cameraEmitter,
|
||||
enableQrScanning = true,
|
||||
captureMode = CameraCaptureMode.ImageOnly,
|
||||
roundCorners = false,
|
||||
fillViewport = true,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
QrCrosshair(modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
} else {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.padding(48.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.CameraXFragment_to_scan_qr_code_allow_camera),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = Color.White
|
||||
)
|
||||
Buttons.MediumTonal(
|
||||
colors = ButtonDefaults.filledTonalButtonColors(),
|
||||
onClick = onOpenCameraClicked
|
||||
) {
|
||||
Text(stringResource(R.string.CameraXFragment_allow_access))
|
||||
}
|
||||
view
|
||||
},
|
||||
update = { view ->
|
||||
view.start(lifecycleOwner = lifecycleOwner, forceLegacy = CameraXRemoteConfig.isBlocklisted())
|
||||
},
|
||||
hasPermission = hasCameraPermission,
|
||||
onRequestPermissions = onOpenCameraClicked,
|
||||
qrHeaderLabelString = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FloatingActionButton(
|
||||
shape = CircleShape,
|
||||
containerColor = SignalTheme.colors.colorSurface1,
|
||||
modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 24.dp),
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 24.dp),
|
||||
onClick = onOpenGalleryClicked
|
||||
) {
|
||||
Image(
|
||||
@@ -143,3 +171,37 @@ private fun QrScanResultDialog(title: String? = null, message: String, onDismiss
|
||||
onDismiss = onDismiss
|
||||
)
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun UsernameQrScanScreenPreview() {
|
||||
Previews.Preview {
|
||||
UsernameQrScanScreen(
|
||||
qrScanResult = null,
|
||||
cameraState = CameraScreenState(),
|
||||
cameraEmitter = {},
|
||||
onQrResultHandled = {},
|
||||
onOpenCameraClicked = {},
|
||||
onOpenGalleryClicked = {},
|
||||
onRecipientFound = {},
|
||||
hasCameraPermission = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun UsernameQrScanScreenNoPermissionPreview() {
|
||||
Previews.Preview {
|
||||
UsernameQrScanScreen(
|
||||
qrScanResult = null,
|
||||
cameraState = CameraScreenState(),
|
||||
cameraEmitter = {},
|
||||
onQrResultHandled = {},
|
||||
onOpenCameraClicked = {},
|
||||
onOpenGalleryClicked = {},
|
||||
onRecipientFound = {},
|
||||
hasCameraPermission = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+20
-14
@@ -24,22 +24,24 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.MultiplePermissionsState
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import org.signal.camera.CameraScreenEvents
|
||||
import org.signal.camera.CameraScreenState
|
||||
import org.signal.camera.CameraScreenViewModel
|
||||
import org.signal.core.ui.compose.Dialogs
|
||||
import org.signal.core.ui.compose.SignalIcons
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.signal.core.ui.permissions.Permissions
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.getParcelableExtraCompat
|
||||
import org.signal.core.util.permissions.PermissionCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -57,7 +59,6 @@ class UsernameQrScannerActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private val viewModel: UsernameQrScannerViewModel by viewModels()
|
||||
private val disposables = LifecycleDisposable()
|
||||
|
||||
@SuppressLint("MissingSuperCall")
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
@@ -66,7 +67,6 @@ class UsernameQrScannerActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
disposables.bindTo(this)
|
||||
|
||||
val galleryLauncher = registerForActivityResult(QrImageSelectionActivity.Contract()) { uri ->
|
||||
if (uri != null) {
|
||||
@@ -86,14 +86,22 @@ class UsernameQrScannerActivity : AppCompatActivity() {
|
||||
val cameraPermissionState: PermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
|
||||
val state by viewModel.state
|
||||
|
||||
val cameraViewModel: CameraScreenViewModel = viewModel { CameraScreenViewModel() }
|
||||
val cameraState by cameraViewModel.state
|
||||
|
||||
LaunchedEffect(cameraViewModel) {
|
||||
cameraViewModel.qrCodeDetected.collect { url ->
|
||||
viewModel.onQrScanned(url)
|
||||
}
|
||||
}
|
||||
|
||||
SignalTheme {
|
||||
Content(
|
||||
lifecycleOwner = this,
|
||||
diposables = disposables.disposables,
|
||||
state = state,
|
||||
cameraState = cameraState,
|
||||
cameraEmitter = cameraViewModel::onEvent,
|
||||
galleryPermissionsState = galleryPermissionState,
|
||||
cameraPermissionState = cameraPermissionState,
|
||||
onQrScanned = { url -> viewModel.onQrScanned(url) },
|
||||
onQrResultHandled = {
|
||||
finish()
|
||||
},
|
||||
@@ -143,12 +151,11 @@ class UsernameQrScannerActivity : AppCompatActivity() {
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun Content(
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
diposables: CompositeDisposable,
|
||||
state: UsernameQrScannerViewModel.ScannerState,
|
||||
cameraState: CameraScreenState,
|
||||
cameraEmitter: (CameraScreenEvents) -> Unit,
|
||||
galleryPermissionsState: MultiplePermissionsState,
|
||||
cameraPermissionState: PermissionState,
|
||||
onQrScanned: (String) -> Unit,
|
||||
onQrResultHandled: () -> Unit,
|
||||
onOpenCameraClicked: () -> Unit,
|
||||
onOpenGalleryClicked: () -> Unit,
|
||||
@@ -173,10 +180,9 @@ fun Content(
|
||||
}
|
||||
) { contentPadding ->
|
||||
UsernameQrScanScreen(
|
||||
lifecycleOwner = lifecycleOwner,
|
||||
disposables = diposables,
|
||||
qrScanResult = state.qrScanResult,
|
||||
onQrCodeScanned = onQrScanned,
|
||||
cameraState = cameraState,
|
||||
cameraEmitter = cameraEmitter,
|
||||
onQrResultHandled = onQrResultHandled,
|
||||
onOpenCameraClicked = onOpenCameraClicked,
|
||||
onOpenGalleryClicked = onOpenGalleryClicked,
|
||||
|
||||
-32
@@ -1,12 +1,9 @@
|
||||
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 com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity
|
||||
@@ -36,35 +33,6 @@ open class ConversationSettingsActivity : DSLSettingsActivity(), ConversationSet
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun createTransitionBundle(context: Context, avatar: View, windowContent: View): Bundle? {
|
||||
return if (context is Activity) {
|
||||
ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||
context,
|
||||
*arrayOf(
|
||||
androidx.core.util.Pair.create(avatar, "avatar"),
|
||||
androidx.core.util.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, groupId, null)
|
||||
|
||||
+12
-16
@@ -38,6 +38,7 @@ import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.concurrent.addTo
|
||||
import org.signal.core.util.getParcelableArrayListExtraCompat
|
||||
import org.signal.core.util.orNull
|
||||
import org.signal.core.util.requireDrawable
|
||||
import org.signal.core.util.requireParcelableCompat
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.AvatarPreviewActivity
|
||||
@@ -91,8 +92,8 @@ import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupDescription
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupInviteSentDialog
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupsLearnMoreBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.main.MainNavigationChatDetailRouter
|
||||
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation
|
||||
import org.thoughtcrime.securesms.main.MainNavigationRouter
|
||||
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXRemoteConfig
|
||||
@@ -111,7 +112,6 @@ import org.thoughtcrime.securesms.stories.dialogs.StoryDialogs
|
||||
import org.thoughtcrime.securesms.stories.viewer.AddToGroupStoryDelegate
|
||||
import org.thoughtcrime.securesms.stories.viewer.StoryViewerActivity
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.ContextUtil
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil
|
||||
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
|
||||
@@ -147,30 +147,28 @@ class ConversationSettingsFragment :
|
||||
private val alertDisabledTint by lazy { ContextCompat.getColor(requireContext(), R.color.signal_alert_primary_50) }
|
||||
private val colorizer = ColorizerV2()
|
||||
private val blockIcon by lazy {
|
||||
ContextUtil.requireDrawable(requireContext(), R.drawable.symbol_block_24).apply {
|
||||
requireContext().requireDrawable(R.drawable.symbol_block_24).apply {
|
||||
colorFilter = PorterDuffColorFilter(alertTint, PorterDuff.Mode.SRC_IN)
|
||||
}
|
||||
}
|
||||
|
||||
private val leaveIcon by lazy {
|
||||
ContextUtil.requireDrawable(requireContext(), R.drawable.symbol_leave_24).apply {
|
||||
requireContext().requireDrawable(R.drawable.symbol_leave_24).apply {
|
||||
colorFilter = PorterDuffColorFilter(alertTint, PorterDuff.Mode.SRC_IN)
|
||||
}
|
||||
}
|
||||
|
||||
private val endGroupIcon by lazy {
|
||||
ContextUtil.requireDrawable(requireContext(), R.drawable.symbol_x_circle_24).apply {
|
||||
requireContext().requireDrawable(R.drawable.symbol_x_circle_24).apply {
|
||||
colorFilter = PorterDuffColorFilter(alertTint, PorterDuff.Mode.SRC_IN)
|
||||
}
|
||||
}
|
||||
|
||||
private val viewModel by viewModels<ConversationSettingsViewModel>(
|
||||
factoryProducer = {
|
||||
val groupId = args.groupId as? GroupId
|
||||
|
||||
ConversationSettingsViewModel.Factory(
|
||||
recipientId = args.recipientId,
|
||||
groupId = groupId,
|
||||
groupId = args.groupId,
|
||||
callMessageIds = args.callMessageIds ?: longArrayOf(),
|
||||
repository = ConversationSettingsRepository(requireContext()),
|
||||
messageRequestRepository = MessageRequestRepository(requireContext())
|
||||
@@ -179,7 +177,7 @@ class ConversationSettingsFragment :
|
||||
)
|
||||
|
||||
private var transitionCallback: TransitionCallback? = null
|
||||
private var mainNavRouter: MainNavigationRouter? = null
|
||||
private var chatRouter: MainNavigationChatDetailRouter? = null
|
||||
|
||||
private lateinit var toolbar: Toolbar
|
||||
private lateinit var toolbarAvatarContainer: FrameLayout
|
||||
@@ -196,7 +194,7 @@ class ConversationSettingsFragment :
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
transitionCallback = context as? TransitionCallback
|
||||
mainNavRouter = context as? MainNavigationRouter
|
||||
chatRouter = context as? MainNavigationChatDetailRouter
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
@@ -253,9 +251,7 @@ class ConversationSettingsFragment :
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return if (item.itemId == R.id.action_edit) {
|
||||
val args = ConversationSettingsFragmentArgs.fromBundle(requireArguments())
|
||||
val groupId = args.groupId as GroupId
|
||||
|
||||
startActivity(CreateProfileActivity.getIntentForGroupProfile(requireActivity(), requireNotNull(groupId)))
|
||||
startActivity(CreateProfileActivity.getIntentForGroupProfile(requireActivity(), requireNotNull(args.groupId)))
|
||||
true
|
||||
} else {
|
||||
super.onOptionsItemSelected(item)
|
||||
@@ -263,8 +259,8 @@ class ConversationSettingsFragment :
|
||||
}
|
||||
|
||||
private fun goToConversationList() {
|
||||
if (mainNavRouter != null) {
|
||||
mainNavRouter?.goTo(MainNavigationDetailLocation.Empty)
|
||||
if (chatRouter != null) {
|
||||
chatRouter?.exitDetailLocation()
|
||||
} else {
|
||||
startActivity(MainActivity.clearTopAndOpenDetail(requireContext(), MainNavigationDetailLocation.Empty))
|
||||
}
|
||||
@@ -914,7 +910,7 @@ class ConversationSettingsFragment :
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_link_16),
|
||||
isEnabled = state.recipient.isActiveGroup && !state.isDeprecatedOrUnregistered,
|
||||
onClick = {
|
||||
navController.safeNavigate(ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToShareableGroupLinkFragment(groupState.groupId.requireV2().toString()))
|
||||
navController.safeNavigate(ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToShareableGroupLinkFragment(groupState.groupId))
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.conversation
|
||||
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import org.thoughtcrime.securesms.main.MainNavigationChatDetailRouter
|
||||
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
/**
|
||||
* Routes to the conversation settings screen, handling split-pane vs. standalone activity automatically.
|
||||
*/
|
||||
object ConversationSettingsNavigator {
|
||||
@JvmStatic
|
||||
fun navigate(
|
||||
activity: FragmentActivity,
|
||||
recipient: Recipient
|
||||
) {
|
||||
if (activity is MainNavigationChatDetailRouter) {
|
||||
activity.goToChatDetail(MainNavigationDetailLocation.Chats.ConversationSettings(recipient.id))
|
||||
return
|
||||
}
|
||||
|
||||
val intent = if (recipient.isPushGroup) {
|
||||
ConversationSettingsActivity.forGroup(activity, recipient.requireGroupId())
|
||||
} else {
|
||||
ConversationSettingsActivity.forRecipient(activity, recipient.id)
|
||||
}
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
}
|
||||
+12
-11
@@ -100,15 +100,16 @@ class MuteUntilTimePickerBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
}
|
||||
|
||||
val zonedDateTime = remember { ZonedDateTime.now() }
|
||||
val timezoneDisclaimer = remember {
|
||||
val zoneOffsetFormatter = DateTimeFormatter.ofPattern("OOOO")
|
||||
val zoneNameFormatter = DateTimeFormatter.ofPattern("zzzz")
|
||||
context.getString(
|
||||
R.string.MuteUntilTimePickerBottomSheet__timezone_disclaimer,
|
||||
zoneOffsetFormatter.format(zonedDateTime),
|
||||
zoneNameFormatter.format(zonedDateTime)
|
||||
)
|
||||
}
|
||||
val zoneOffset = remember { DateTimeFormatter.ofPattern("OOOO").format(zonedDateTime) }
|
||||
val zoneName = remember { DateTimeFormatter.ofPattern("zzzz").format(zonedDateTime) }
|
||||
val timezoneDisclaimer = stringResource(
|
||||
R.string.MuteUntilTimePickerBottomSheet__timezone_disclaimer,
|
||||
zoneOffset,
|
||||
zoneName
|
||||
)
|
||||
|
||||
val selectDateTitle = stringResource(R.string.MuteUntilTimePickerBottomSheet__select_date_title)
|
||||
val selectTimeTitle = stringResource(R.string.MuteUntilTimePickerBottomSheet__select_time_title)
|
||||
|
||||
MuteUntilSheetContent(
|
||||
dateText = dateText,
|
||||
@@ -117,7 +118,7 @@ class MuteUntilTimePickerBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
onDateClick = {
|
||||
val local = LocalDateTime.now().atMidnight().atUTC().toMillis()
|
||||
val datePicker = MaterialDatePicker.Builder.datePicker()
|
||||
.setTitleText(context.getString(R.string.MuteUntilTimePickerBottomSheet__select_date_title))
|
||||
.setTitleText(selectDateTitle)
|
||||
.setSelection(selectedDate)
|
||||
.setCalendarConstraints(CalendarConstraints.Builder().setStart(local).setValidator(DateValidatorPointForward.now()).build())
|
||||
.build()
|
||||
@@ -138,7 +139,7 @@ class MuteUntilTimePickerBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
.setTimeFormat(timeFormat)
|
||||
.setHour(selectedHour)
|
||||
.setMinute(selectedMinute)
|
||||
.setTitleText(context.getString(R.string.MuteUntilTimePickerBottomSheet__select_time_title))
|
||||
.setTitleText(selectTimeTitle)
|
||||
.build()
|
||||
|
||||
timePicker.addOnDismissListener {
|
||||
|
||||
+1
-4
@@ -9,7 +9,6 @@ import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupErrors
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
|
||||
@@ -24,10 +23,8 @@ class PermissionsSettingsFragment : DSLSettingsFragment(
|
||||
private val viewModel: PermissionsSettingsViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
val args = PermissionsSettingsFragmentArgs.fromBundle(requireArguments())
|
||||
val groupId = requireNotNull(args.groupId as GroupId)
|
||||
val repository = PermissionsSettingsRepository(requireContext())
|
||||
|
||||
PermissionsSettingsViewModel.Factory(groupId, repository)
|
||||
PermissionsSettingsViewModel.Factory(args.groupId, repository)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ object BioTextPreference {
|
||||
|
||||
override fun getSubhead1Text(context: Context): String? {
|
||||
return if (recipient.isReleaseNotes) {
|
||||
context.getString(R.string.ReleaseNotes__signal_release_notes_and_news)
|
||||
null
|
||||
} else {
|
||||
recipient.combinedAboutAndEmoji
|
||||
}
|
||||
|
||||
+2
-2
@@ -6,6 +6,7 @@ import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.Observer
|
||||
import org.signal.core.util.requireDrawable
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
@@ -13,7 +14,6 @@ import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.groups.memberlabel.MemberLabelPillView
|
||||
import org.thoughtcrime.securesms.groups.memberlabel.StyledMemberLabel
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.ContextUtil
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
@@ -105,7 +105,7 @@ object RecipientPreference {
|
||||
} else {
|
||||
if (recipient.isSystemContact) {
|
||||
SpannableStringBuilder(recipient.getDisplayName(context)).apply {
|
||||
val drawable = ContextUtil.requireDrawable(context, R.drawable.symbol_person_circle_24).apply {
|
||||
val drawable = context.requireDrawable(R.drawable.symbol_person_circle_24).apply {
|
||||
setTint(ContextCompat.getColor(context, CoreUiR.color.signal_colorOnSurface))
|
||||
}
|
||||
SpanUtil.appendCenteredImageSpan(this, drawable, 16, 16)
|
||||
|
||||
+2
-2
@@ -30,10 +30,10 @@ import androidx.core.widget.ImageViewCompat
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.signal.core.util.dp
|
||||
import org.signal.core.util.requireDrawable
|
||||
import org.signal.libsignal.protocol.fingerprint.Fingerprint
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.qr.QrCodeUtil
|
||||
import org.thoughtcrime.securesms.util.ContextUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import java.nio.charset.Charset
|
||||
@@ -205,7 +205,7 @@ class SafetyNumberQrView : ConstraintLayout {
|
||||
private fun createVerifiedBitmap(width: Int, height: Int, @DrawableRes id: Int): Bitmap {
|
||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bitmap)
|
||||
val check = ContextUtil.requireDrawable(context, id).toBitmap()
|
||||
val check = context.requireDrawable(id).toBitmap()
|
||||
val offset = ((width - check.width) / 2).toFloat()
|
||||
canvas.drawBitmap(check, offset, offset, null)
|
||||
return bitmap
|
||||
|
||||
+2
-2
@@ -15,7 +15,7 @@ import org.thoughtcrime.securesms.BaseActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.calls.links.CallLinks
|
||||
import org.thoughtcrime.securesms.calls.links.EditCallLinkNameDialogFragment
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsNavigator
|
||||
import org.thoughtcrime.securesms.components.webrtc.controls.CallInfoView
|
||||
import org.thoughtcrime.securesms.components.webrtc.controls.ControlsAndInfoViewModel
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
@@ -82,7 +82,7 @@ class CallInfoCallbacks(
|
||||
}
|
||||
|
||||
override fun onContactDetails(callParticipant: CallParticipant) {
|
||||
activity.startActivity(ConversationSettingsActivity.forRecipient(activity, callParticipant.recipient.id))
|
||||
ConversationSettingsNavigator.navigate(activity, callParticipant.recipient)
|
||||
}
|
||||
|
||||
override fun onViewSafetyNumber(callParticipant: CallParticipant) {
|
||||
|
||||
+6
-8
@@ -653,6 +653,10 @@ private fun InfoOverlay(
|
||||
if (!renderInPip) {
|
||||
Spacer(modifier = Modifier.size(12.dp))
|
||||
|
||||
val shortDisplayName = rememberRecipientField(recipient) { getShortDisplayName(context) }
|
||||
val sIsBlocked = stringResource(R.string.CallParticipantView__s_is_blocked, shortDisplayName)
|
||||
val canNotReceiveAudio = stringResource(R.string.CallParticipantView__cant_receive_audio_video_from_s, shortDisplayName)
|
||||
|
||||
// Use AndroidView for EmojiTextView
|
||||
AndroidView(
|
||||
factory = { ctx ->
|
||||
@@ -670,15 +674,9 @@ private fun InfoOverlay(
|
||||
},
|
||||
update = { view ->
|
||||
view.text = if (isBlocked) {
|
||||
context.getString(
|
||||
R.string.CallParticipantView__s_is_blocked,
|
||||
recipient.getShortDisplayName(context)
|
||||
)
|
||||
sIsBlocked
|
||||
} else {
|
||||
context.getString(
|
||||
R.string.CallParticipantView__cant_receive_audio_video_from_s,
|
||||
recipient.getShortDisplayName(context)
|
||||
)
|
||||
canNotReceiveAudio
|
||||
}
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
|
||||
+1
-1
@@ -145,7 +145,7 @@ fun IncomingCallScreen(
|
||||
MaterialTheme.typography.headlineMedium.copy(shadow = textShadow)
|
||||
},
|
||||
color = Color.White,
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp)
|
||||
)
|
||||
|
||||
if (callStatus != null) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import org.signal.core.util.BreakIteratorCompat
|
||||
import org.signal.core.util.requireDrawable
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.avatar.fallback.FallbackAvatar
|
||||
import org.thoughtcrime.securesms.avatar.view.AvatarView
|
||||
@@ -29,7 +30,6 @@ import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.ContextUtil
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
@@ -543,7 +543,7 @@ open class ContactSearchAdapter(
|
||||
val recipient = getRecipient(model)
|
||||
val suffix: CharSequence? = if (recipient.isSystemContact && !recipient.showVerified) {
|
||||
SpannableStringBuilder().apply {
|
||||
val drawable = ContextUtil.requireDrawable(context, R.drawable.symbol_person_circle_24).apply {
|
||||
val drawable = context.requireDrawable(R.drawable.symbol_person_circle_24).apply {
|
||||
setTint(ContextCompat.getColor(context, CoreUiR.color.signal_colorOnSurface))
|
||||
}
|
||||
SpanUtil.appendCenteredImageSpan(this, drawable, 16, 16)
|
||||
|
||||
+2
-8
@@ -244,14 +244,8 @@ class ContactSearchPagedDataSource(
|
||||
return contactSearchPagedDataSourceRepository.querySignalContactLetterHeaders(
|
||||
query = query,
|
||||
includeSelfMode = section.includeSelfMode,
|
||||
includePush = when (section.transportType) {
|
||||
ContactSearchConfiguration.TransportType.PUSH, ContactSearchConfiguration.TransportType.ALL -> true
|
||||
else -> false
|
||||
},
|
||||
includeSms = when (section.transportType) {
|
||||
ContactSearchConfiguration.TransportType.SMS, ContactSearchConfiguration.TransportType.ALL -> true
|
||||
else -> false
|
||||
}
|
||||
includePush = true,
|
||||
includeSms = false
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -41,10 +41,6 @@ class ContactSearchView : AbstractComposeView {
|
||||
fun onRecyclerViewReady(recyclerView: RecyclerView)
|
||||
}
|
||||
|
||||
init {
|
||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||
}
|
||||
|
||||
private var viewModel: ContactSearchViewModel? by mutableStateOf(null)
|
||||
private var currentFragmentManager: FragmentManager? = null
|
||||
private var currentDisplayOptions: ContactSearchAdapter.DisplayOptions? = null
|
||||
@@ -54,8 +50,13 @@ class ContactSearchView : AbstractComposeView {
|
||||
private var currentContentBottomPadding: Dp = 0.dp
|
||||
private var currentAdapterFactory: ContactSearchAdapter.AdapterFactory = ContactSearchAdapter.DefaultAdapterFactory
|
||||
private var currentScrollListeners: List<RecyclerView.OnScrollListener> = emptyList()
|
||||
private var recyclerView: RecyclerView? = null
|
||||
private var currentOnRecyclerViewReady: RecyclerViewReadyCallback? = null
|
||||
|
||||
init {
|
||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures and activates the contact search. Must be called exactly once from the host
|
||||
* fragment's `onViewCreated`. The [viewModel] must be created and held by the caller so it
|
||||
@@ -106,6 +107,10 @@ class ContactSearchView : AbstractComposeView {
|
||||
this.viewModel = viewModel // triggers recomposition
|
||||
}
|
||||
|
||||
override fun canScrollVertically(direction: Int): Boolean {
|
||||
return recyclerView?.canScrollVertically(direction) ?: super.canScrollVertically(direction)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val vm = viewModel ?: return
|
||||
@@ -123,7 +128,10 @@ class ContactSearchView : AbstractComposeView {
|
||||
contentBottomPadding = currentContentBottomPadding,
|
||||
adapterFactory = currentAdapterFactory,
|
||||
scrollListeners = currentScrollListeners,
|
||||
onRecyclerViewReady = currentOnRecyclerViewReady
|
||||
onRecyclerViewReady = RecyclerViewReadyCallback { recyclerView ->
|
||||
this@ContactSearchView.recyclerView = recyclerView
|
||||
currentOnRecyclerViewReady?.onRecyclerViewReady(recyclerView)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,14 +303,10 @@ object ContactDiscovery {
|
||||
}
|
||||
|
||||
if (NotificationChannels.supported()) {
|
||||
SignalDatabase.recipients.getRecipientsWithNotificationChannels().use { reader ->
|
||||
var recipient: Recipient? = reader.getNext()
|
||||
|
||||
while (recipient != null) {
|
||||
NotificationChannels.getInstance().updateContactChannelName(recipient)
|
||||
recipient = reader.getNext()
|
||||
}
|
||||
}
|
||||
SignalDatabase
|
||||
.recipients
|
||||
.getRecipientsWithNotificationChannels()
|
||||
.forEach { NotificationChannels.getInstance().updateContactChannelName(Recipient.resolved(it.id)) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.AbstractComposeView
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -209,7 +210,12 @@ private fun ConversationHeaderContent(
|
||||
.padding(top = AvatarOverlapAbove)
|
||||
.width(277.dp)
|
||||
.then(
|
||||
if (hasWallpaper) {
|
||||
if (isReleaseNotes) {
|
||||
Modifier
|
||||
.clip(BorderShape)
|
||||
.background(colorResource(R.color.release_notes_header_background))
|
||||
.border(width = 2.dp, color = colorResource(R.color.release_notes_header_border), shape = BorderShape)
|
||||
} else if (hasWallpaper) {
|
||||
Modifier
|
||||
.clip(BorderShape)
|
||||
.background(if (isSystemInDarkTheme()) SignalTheme.colors.colorTransparentInverse5 else SignalTheme.colors.colorTransparent5)
|
||||
|
||||
@@ -70,11 +70,13 @@ import com.google.common.collect.Sets;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.signal.core.ui.util.ThemeUtil;
|
||||
import org.signal.core.ui.view.Stub;
|
||||
import org.signal.core.util.BidiUtil;
|
||||
import org.signal.core.util.DimensionUnit;
|
||||
import org.signal.core.util.StringUtil;
|
||||
import org.signal.core.util.Util;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.core.ui.view.Stub;
|
||||
import org.signal.ringrtc.CallLinkRootKey;
|
||||
import org.thoughtcrime.securesms.BindableConversationItem;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
@@ -147,14 +149,12 @@ import org.thoughtcrime.securesms.util.LongClickMovementMethod;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
import org.thoughtcrime.securesms.util.PlaceholderURLSpan;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.util.ProjectionList;
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig;
|
||||
import org.thoughtcrime.securesms.util.SearchUtil;
|
||||
import org.signal.core.ui.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
import org.thoughtcrime.securesms.util.UrlClickHandler;
|
||||
import org.signal.core.util.Util;
|
||||
import org.thoughtcrime.securesms.util.VibrateUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.views.NullableStub;
|
||||
@@ -204,6 +204,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
private Optional<MessageRecord> nextMessageRecord;
|
||||
private Locale locale;
|
||||
private boolean groupThread;
|
||||
private boolean isReleaseNotes;
|
||||
private LiveRecipient author;
|
||||
private RequestManager requestManager;
|
||||
private Optional<MessageRecord> previousMessage;
|
||||
@@ -290,6 +291,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
private final ProjectionList colorizerProjections = new ProjectionList(3);
|
||||
private boolean isBound = false;
|
||||
|
||||
private RelativeLayout.LayoutParams normalBubbleParams = null;
|
||||
|
||||
private final Runnable shrinkBubble = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -412,6 +415,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
this.batchSelected = batchSelected;
|
||||
this.conversationRecipient = conversationRecipient.live();
|
||||
this.groupThread = conversationRecipient.isGroup();
|
||||
this.isReleaseNotes = conversationRecipient.isReleaseNotes();
|
||||
this.author = messageRecord.getFromRecipient().live();
|
||||
this.canPlayContent = false;
|
||||
this.mediaItem = null;
|
||||
@@ -419,6 +423,9 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
this.displayMode = displayMode;
|
||||
this.previousMessage = previousMessageRecord;
|
||||
|
||||
lastFooterDecisionLineWidth = -1;
|
||||
lastFooterWasCollapsed = false;
|
||||
|
||||
setGutterSizes(messageRecord, groupThread);
|
||||
setMessageShape(messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
|
||||
setMediaAttributes(messageRecord, previousMessageRecord, nextMessageRecord, groupThread, hasWallpaper, isMessageRequestAccepted, allowedToPlayInline);
|
||||
@@ -595,7 +602,14 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
int collapsedTopMargin = -1 * (dateView.getMeasuredHeight() + ViewUtil.dpToPx(4));
|
||||
|
||||
if (bodyText.isSingleLine() && !messageRecord.isFailed()) {
|
||||
int maxBubbleWidth = hasBigImageLinkPreview(messageRecord) || hasThumbnail(messageRecord) ? readDimen(R.dimen.media_bubble_max_width) : getMaxBubbleWidth();
|
||||
int maxBubbleWidth = hasBigImageLinkPreview(messageRecord) || hasThumbnail(messageRecord) ? readDimen(R.dimen.media_bubble_max_width) : getMaxBubbleWidth();
|
||||
if (hasThumbnail(messageRecord) || hasBigImageLinkPreview(messageRecord)) {
|
||||
int thumbnailWidth = mediaThumbnailStub.resolved() ? mediaThumbnailStub.require().getMeasuredWidth() : 0;
|
||||
if (thumbnailWidth > 0) {
|
||||
maxBubbleWidth = Math.min(maxBubbleWidth, thumbnailWidth);
|
||||
}
|
||||
}
|
||||
|
||||
int bodyMargins = ViewUtil.getLeftMargin(bodyText) + ViewUtil.getRightMargin(bodyText);
|
||||
int sizeWithMargins = bodyText.getMeasuredWidth() + TEXT_FOOTER_SPACING + footerWidth + bodyMargins;
|
||||
int minSize = Math.min(maxBubbleWidth, Math.max(bodyText.getMeasuredWidth() + TEXT_FOOTER_SPACING + footerWidth + bodyMargins, bodyBubble.getMeasuredWidth()));
|
||||
@@ -629,10 +643,12 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
|
||||
if (lineWidthChangedSlightly) {
|
||||
if (lastFooterWasCollapsed && ViewUtil.getTopMargin(footer) != collapsedTopMargin) {
|
||||
ViewUtil.setTopMargin(footer, collapsedTopMargin, false);
|
||||
ViewUtil.setBottomMargin(footer, collapsedBottomMargin, false);
|
||||
updatingFooter = true;
|
||||
needsMeasure = true;
|
||||
if (requiredSpace - FOOTER_POSITION_THRESHOLD <= availableSpace) {
|
||||
ViewUtil.setTopMargin(footer, collapsedTopMargin, false);
|
||||
ViewUtil.setBottomMargin(footer, collapsedBottomMargin, false);
|
||||
updatingFooter = true;
|
||||
needsMeasure = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (requiredSpace + FOOTER_POSITION_THRESHOLD <= availableSpace) {
|
||||
@@ -656,7 +672,10 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
boolean lineWidthChangedSlightly = Math.abs(currentLineWidth - lastFooterDecisionLineWidth) <= FOOTER_POSITION_THRESHOLD;
|
||||
|
||||
if (lineWidthChangedSlightly && lastFooterWasCollapsed) {
|
||||
shouldRevert = false;
|
||||
int currentRequiredSpace = currentLineWidth + TEXT_FOOTER_SPACING + footer.getMeasuredWidth();
|
||||
if (currentRequiredSpace - FOOTER_POSITION_THRESHOLD <= bodyText.getMeasuredWidth()) {
|
||||
shouldRevert = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -695,6 +714,17 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
}
|
||||
}
|
||||
|
||||
if (!isViewOnceMessage(messageRecord) && (hasThumbnail(messageRecord) || hasBigImageLinkPreview(messageRecord))) {
|
||||
int thumbnailWidth = mediaThumbnailStub.require().getMeasuredWidth();
|
||||
if (thumbnailWidth > 0 && bodyBubble.getMeasuredWidth() > thumbnailWidth) {
|
||||
bodyBubble.getLayoutParams().width = thumbnailWidth;
|
||||
updatingFooter = false;
|
||||
lastFooterDecisionLineWidth = -1;
|
||||
lastFooterWasCollapsed = false;
|
||||
needsMeasure = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsMeasure) {
|
||||
if (measureCalls < MAX_MEASURE_CALLS) {
|
||||
measureCalls++;
|
||||
@@ -772,6 +802,9 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
}
|
||||
|
||||
private @ColorInt int getDefaultBubbleColor(boolean hasWallpaper) {
|
||||
if (isReleaseNotes) {
|
||||
return ContextCompat.getColor(context, R.color.release_notes_bubble);
|
||||
}
|
||||
return hasWallpaper ? defaultBubbleColorForWallpaper : defaultBubbleColor;
|
||||
}
|
||||
|
||||
@@ -919,9 +952,18 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
footer.setOnlyShowSendingStatus(messageRecord.isRemoteDelete(), messageRecord);
|
||||
} else {
|
||||
bodyBubble.getBackground().setColorFilter(getDefaultBubbleColor(hasWallpaper), PorterDuff.Mode.SRC_IN);
|
||||
footer.setTextColor(colorizer.getIncomingFooterTextColor(context, hasWallpaper));
|
||||
footer.setIconColor(colorizer.getIncomingFooterIconColor(context, hasWallpaper));
|
||||
footer.setRevealDotColor(colorizer.getIncomingFooterIconColor(context, hasWallpaper));
|
||||
if (isReleaseNotes) {
|
||||
int releaseNotesTextColor = ContextCompat.getColor(context, R.color.release_notes_bubble_text);
|
||||
bodyText.setTextColor(releaseNotesTextColor);
|
||||
bodyText.setLinkTextColor(releaseNotesTextColor);
|
||||
footer.setTextColor(releaseNotesTextColor);
|
||||
footer.setIconColor(releaseNotesTextColor);
|
||||
footer.setRevealDotColor(releaseNotesTextColor);
|
||||
} else {
|
||||
footer.setTextColor(colorizer.getIncomingFooterTextColor(context, hasWallpaper));
|
||||
footer.setIconColor(colorizer.getIncomingFooterIconColor(context, hasWallpaper));
|
||||
footer.setRevealDotColor(colorizer.getIncomingFooterIconColor(context, hasWallpaper));
|
||||
}
|
||||
footer.setOnlyShowSendingStatus(false, messageRecord);
|
||||
}
|
||||
|
||||
@@ -1718,8 +1760,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
int end = messageBody.getSpanEnd(placeholder);
|
||||
URLSpan span = new InterceptableLongClickCopyLinkSpan(placeholder.getValue(),
|
||||
urlClickListener,
|
||||
ContextCompat.getColor(getContext(), R.color.signal_accent_primary),
|
||||
false);
|
||||
ContextCompat.getColor(getContext(), isReleaseNotes ? R.color.release_notes_bubble_text : R.color.signal_accent_primary),
|
||||
isReleaseNotes);
|
||||
|
||||
messageBody.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
@@ -1848,7 +1890,11 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
}
|
||||
|
||||
private void setGutterSizes(@NonNull MessageRecord current, boolean isGroupThread) {
|
||||
if (isGroupThread && current.isOutgoing()) {
|
||||
if (isReleaseNotes) {
|
||||
int gutter = readDimen(R.dimen.conversation_individual_right_gutter);
|
||||
ViewUtil.setPaddingStart(this, gutter);
|
||||
ViewUtil.setPaddingEnd(this, gutter);
|
||||
} else if (isGroupThread && current.isOutgoing()) {
|
||||
ViewUtil.setPaddingStart(this, readDimen(R.dimen.conversation_group_left_gutter));
|
||||
ViewUtil.setPaddingEnd(this, readDimen(R.dimen.conversation_individual_right_gutter));
|
||||
} else if (current.isOutgoing()) {
|
||||
@@ -1858,6 +1904,18 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
ViewUtil.setPaddingStart(this, readDimen(R.dimen.conversation_individual_received_left_gutter));
|
||||
ViewUtil.setPaddingEnd(this, readDimen(R.dimen.conversation_individual_right_gutter));
|
||||
}
|
||||
|
||||
if (isReleaseNotes && normalBubbleParams == null) {
|
||||
RelativeLayout.LayoutParams bubbleParams = (RelativeLayout.LayoutParams) bodyBubble.getLayoutParams();
|
||||
|
||||
normalBubbleParams = new RelativeLayout.LayoutParams(bubbleParams);
|
||||
bubbleParams.addRule(RelativeLayout.CENTER_HORIZONTAL);
|
||||
bubbleParams.setMarginStart(0);
|
||||
bodyBubble.setLayoutParams(bubbleParams);
|
||||
} else if (normalBubbleParams != null && !isReleaseNotes) {
|
||||
bodyBubble.setLayoutParams(normalBubbleParams);
|
||||
normalBubbleParams = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void setReactions(@NonNull MessageRecord current) {
|
||||
|
||||
@@ -167,6 +167,14 @@ internal object ConversationOptionsMenu {
|
||||
|
||||
if (recipient.isReleaseNotes) {
|
||||
hideMenuItem(menu, R.id.menu_add_shortcut)
|
||||
menu.findItem(R.id.menu_mute_notifications)?.apply {
|
||||
setIcon(R.drawable.symbol_bell_24)
|
||||
setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
|
||||
}
|
||||
menu.findItem(R.id.menu_unmute_notifications)?.apply {
|
||||
setIcon(R.drawable.symbol_bell_slash_24)
|
||||
setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
|
||||
}
|
||||
}
|
||||
|
||||
if (!SignalStore.labs.individualChatPlaintextExport) {
|
||||
|
||||
+2
-2
@@ -17,12 +17,12 @@ import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.bumptech.glide.RequestManager;
|
||||
|
||||
import org.signal.core.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.avatar.view.AvatarView;
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView;
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.util.DrawableUtil;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
@@ -119,7 +119,7 @@ public class ConversationTitleView extends ConstraintLayout {
|
||||
if (recipient != null && recipient.isBlocked()) {
|
||||
startDrawable = ContextUtil.requireDrawable(getContext(), R.drawable.symbol_block_16);
|
||||
startDrawable.setBounds(0, 0, ViewUtil.dpToPx(18), ViewUtil.dpToPx(18));
|
||||
} else if (recipient != null && recipient.isMuted()) {
|
||||
} else if (recipient != null && recipient.isMuted() && !recipient.isReleaseNotes()) {
|
||||
startDrawable = ContextUtil.requireDrawable(getContext(), R.drawable.ic_bell_disabled_16);
|
||||
startDrawable.setBounds(0, 0, ViewUtil.dpToPx(18), ViewUtil.dpToPx(18));
|
||||
}
|
||||
|
||||
+13
-6
@@ -221,13 +221,16 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
observeDisplayBody(lifecycleOwner, spannableMessage);
|
||||
observeDisplayBodyWithTimer(lifecycleOwner);
|
||||
|
||||
boolean donationRequest = conversationMessage.getMessageRecord().isReleaseChannelDonationRequest();
|
||||
|
||||
present(conversationMessage, nextMessageRecord, conversationRecipient, isMessageRequestAccepted);
|
||||
presentTimer(updateDescription);
|
||||
presentBackground(shouldCollapse(messageRecord, previousMessageRecord),
|
||||
shouldCollapse(messageRecord, nextMessageRecord),
|
||||
hasWallpaper);
|
||||
hasWallpaper,
|
||||
donationRequest);
|
||||
|
||||
presentActionButton(hasWallpaper, conversationMessage.getMessageRecord().isReleaseChannelDonationRequest());
|
||||
presentActionButton(hasWallpaper, donationRequest);
|
||||
presentCollapsedHead(conversationMessage.getMessageRecord().getCollapsedState());
|
||||
|
||||
updateSelectedState();
|
||||
@@ -785,7 +788,7 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
(messageRecord.isGroupV2JoinRequest(toBlock.requireServiceId()) && previousMessageRecord.map(m -> m.isCollapsedGroupV2JoinUpdate(toBlock.requireServiceId())).orElse(false));
|
||||
}
|
||||
|
||||
private void presentBackground(boolean collapseAbove, boolean collapseBelow, boolean hasWallpaper) {
|
||||
private void presentBackground(boolean collapseAbove, boolean collapseBelow, boolean hasWallpaper, boolean isDonationRequest) {
|
||||
int marginDefault = getContext().getResources().getDimensionPixelOffset(R.dimen.conversation_update_vertical_margin);
|
||||
int marginCollapsed = 0;
|
||||
int paddingDefault = getContext().getResources().getDimensionPixelOffset(R.dimen.conversation_update_vertical_padding);
|
||||
@@ -843,7 +846,11 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
ViewUtil.updateLayoutParams(background, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
if (hasWallpaper) {
|
||||
background.setBackgroundResource(R.drawable.conversation_update_wallpaper_background_singular);
|
||||
if (isDonationRequest) {
|
||||
background.setBackgroundResource(R.drawable.conversation_update_release_note_background);
|
||||
} else {
|
||||
background.setBackgroundResource(R.drawable.conversation_update_wallpaper_background_singular);
|
||||
}
|
||||
} else {
|
||||
background.setBackground(null);
|
||||
}
|
||||
@@ -852,8 +859,8 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
|
||||
private void presentActionButton(boolean hasWallpaper, boolean isBoostRequest) {
|
||||
if (isBoostRequest) {
|
||||
actionButton.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(getContext(), org.signal.core.ui.R.color.signal_colorSecondaryContainer)));
|
||||
actionButton.setTextColor(ColorStateList.valueOf(ContextCompat.getColor(getContext(), org.signal.core.ui.R.color.signal_colorOnSecondaryContainer)));
|
||||
actionButton.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(getContext(), R.color.release_notes_cta_background)));
|
||||
actionButton.setTextColor(ColorStateList.valueOf(ContextCompat.getColor(getContext(), org.signal.core.ui.R.color.signal_colorOnSurface)));
|
||||
} else if (hasWallpaper) {
|
||||
actionButton.setBackgroundTintList(AppCompatResources.getColorStateList(getContext(), R.color.conversation_update_item_button_background_wallpaper));
|
||||
actionButton.setTextColor(AppCompatResources.getColorStateList(getContext(), R.color.conversation_update_item_button_text_color_wallpaper));
|
||||
|
||||
+22
-12
@@ -29,6 +29,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -53,6 +54,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.recipients.ui.RecipientLookupFailureMessage
|
||||
import org.thoughtcrime.securesms.recipients.ui.RecipientPicker
|
||||
import org.thoughtcrime.securesms.recipients.ui.RecipientPicker.DisplayMode
|
||||
import org.thoughtcrime.securesms.recipients.ui.RecipientPickerCallbacks
|
||||
import org.thoughtcrime.securesms.recipients.ui.RecipientPickerScaffold
|
||||
import org.thoughtcrime.securesms.recipients.ui.RecipientSelection
|
||||
@@ -310,6 +312,7 @@ private fun NewConversationRecipientPicker(
|
||||
searchQuery = uiState.searchQuery,
|
||||
isRefreshing = uiState.isRefreshingContacts,
|
||||
shouldResetContactsList = uiState.shouldResetContactsList,
|
||||
displayModes = setOf(DisplayMode.PUSH, DisplayMode.ACTIVE_GROUPS, DisplayMode.INACTIVE_GROUPS, DisplayMode.SELF),
|
||||
callbacks = remember(callbacks) {
|
||||
RecipientPickerCallbacks(
|
||||
listActions = callbacks,
|
||||
@@ -333,20 +336,21 @@ private fun UserMessagesHost(
|
||||
snackbarHostState: SnackbarHostState
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val resources = LocalResources.current
|
||||
|
||||
when (userMessage) {
|
||||
null -> {}
|
||||
|
||||
is UserMessage.Info.RecipientRemoved -> LaunchedEffect(userMessage) {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = context.getString(R.string.NewConversationActivity__s_has_been_removed, userMessage.recipient.getDisplayName(context))
|
||||
message = resources.getString(R.string.NewConversationActivity__s_has_been_removed, userMessage.recipient.getDisplayName(context))
|
||||
)
|
||||
onDismiss(userMessage)
|
||||
}
|
||||
|
||||
is UserMessage.Info.RecipientBlocked -> LaunchedEffect(userMessage) {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = context.getString(R.string.NewConversationActivity__s_has_been_blocked, userMessage.recipient.getDisplayName(context))
|
||||
message = resources.getString(R.string.NewConversationActivity__s_has_been_blocked, userMessage.recipient.getDisplayName(context))
|
||||
)
|
||||
onDismiss(userMessage)
|
||||
}
|
||||
@@ -358,18 +362,24 @@ private fun UserMessagesHost(
|
||||
)
|
||||
}
|
||||
|
||||
is UserMessage.Info.UserAlreadyInAnotherCall -> LaunchedEffect(userMessage) {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = context.getString(R.string.CommunicationActions__you_are_already_in_a_call)
|
||||
)
|
||||
onDismiss(userMessage)
|
||||
is UserMessage.Info.UserAlreadyInAnotherCall -> {
|
||||
val youAreAlreadyInACall = stringResource(R.string.CommunicationActions__you_are_already_in_a_call)
|
||||
LaunchedEffect(userMessage) {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = youAreAlreadyInACall
|
||||
)
|
||||
onDismiss(userMessage)
|
||||
}
|
||||
}
|
||||
|
||||
is UserMessage.Info.ContactsRefreshFailed -> LaunchedEffect(userMessage) {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = context.getString(R.string.ContactSelectionListFragment_error_retrieving_contacts_check_your_network_connection)
|
||||
)
|
||||
onDismiss(userMessage)
|
||||
is UserMessage.Info.ContactsRefreshFailed -> {
|
||||
val errorRetrievingContacts = stringResource(R.string.ContactSelectionListFragment_error_retrieving_contacts_check_your_network_connection)
|
||||
LaunchedEffect(userMessage) {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = errorRetrievingContacts
|
||||
)
|
||||
onDismiss(userMessage)
|
||||
}
|
||||
}
|
||||
|
||||
is UserMessage.Prompt.ConfirmRemoveRecipient -> Dialogs.SimpleAlertDialog(
|
||||
|
||||
+6
-1
@@ -100,6 +100,8 @@ class ScheduledMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment
|
||||
setCondensedMode(ConversationItemDisplayMode.Condensed(ConversationItemDisplayMode.MessageMode.SCHEDULED))
|
||||
}
|
||||
|
||||
val stickyHeaders = StickyHeaderDecoration(messageAdapter, false, false, ConversationAdapter.HEADER_TYPE_INLINE_DATE)
|
||||
|
||||
val list: RecyclerView = view.findViewById<RecyclerView>(R.id.scheduled_list).apply {
|
||||
layoutManager = SmoothScrollingLinearLayoutManager(requireContext(), true)
|
||||
adapter = messageAdapter
|
||||
@@ -107,7 +109,7 @@ class ScheduledMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment
|
||||
|
||||
doOnNextLayout {
|
||||
// Adding this without waiting for a layout pass would result in an indeterminate amount of padding added to the top of the view
|
||||
addItemDecoration(StickyHeaderDecoration(messageAdapter, false, false, ConversationAdapter.HEADER_TYPE_INLINE_DATE))
|
||||
addItemDecoration(stickyHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +129,9 @@ class ScheduledMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment
|
||||
} else if (!list.canScrollVertically(1)) {
|
||||
list.layoutManager?.scrollToPosition(0)
|
||||
}
|
||||
|
||||
stickyHeaders.invalidateCache()
|
||||
list.invalidateItemDecorations()
|
||||
}
|
||||
recyclerViewColorizer.setChatColors(conversationRecipient.chatColors)
|
||||
}
|
||||
|
||||
+1
-1
@@ -12,11 +12,11 @@ import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.signal.core.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.components.FromTextView;
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.util.DrawableUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.adapter.AlwaysChangedDiffUtil;
|
||||
|
||||
+34
-3
@@ -8,8 +8,10 @@ import android.view.Window
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.lifecycle.enableSavedStateHandles
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import io.reactivex.rxjava3.subjects.Subject
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.logging.Log.tag
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
@@ -17,10 +19,14 @@ import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.GooglePayComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.GooglePayRepository
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsNavHostFragment
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents
|
||||
import org.thoughtcrime.securesms.jobs.ConversationShortcutUpdateJob
|
||||
import org.thoughtcrime.securesms.main.MainNavigationChatDetailRouter
|
||||
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation
|
||||
import org.thoughtcrime.securesms.messagedetails.MessageDetailsFragment
|
||||
import org.thoughtcrime.securesms.util.ConfigurationUtil
|
||||
import org.thoughtcrime.securesms.util.Debouncer
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||
@@ -29,12 +35,11 @@ import java.util.concurrent.TimeUnit
|
||||
/**
|
||||
* Wrapper activity for ConversationFragment.
|
||||
*/
|
||||
open class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, GooglePayComponent {
|
||||
open class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, GooglePayComponent, MainNavigationChatDetailRouter {
|
||||
|
||||
companion object {
|
||||
private val TAG = tag(ConversationActivity::class.java)
|
||||
|
||||
private const val STATE_WATERMARK = "share_data_watermark"
|
||||
private const val MESSAGE_DETAILS_FRAGMENT_TAG = "MessageDetailsFragment"
|
||||
}
|
||||
|
||||
private val theme = DynamicNoActionBarTheme()
|
||||
@@ -137,6 +142,32 @@ open class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaCo
|
||||
.commitNowAllowingStateLoss()
|
||||
}
|
||||
|
||||
override fun exitDetailLocation() {
|
||||
if (!supportFragmentManager.popBackStackImmediate()) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun goToChatDetail(location: MainNavigationDetailLocation.Chats) {
|
||||
when (location) {
|
||||
is MainNavigationDetailLocation.Chats.ConversationSettings -> {
|
||||
lifecycleScope.launch {
|
||||
val args = ConversationSettingsNavHostFragment.createArgs(location.recipientId)
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.fragment_container, ConversationSettingsNavHostFragment::class.java, args)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
is MainNavigationDetailLocation.Chats.MessageDetails -> {
|
||||
MessageDetailsFragment.create(location.messageId, location.recipientId)
|
||||
.show(supportFragmentManager, MESSAGE_DETAILS_FRAGMENT_TAG)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
|
||||
return motionEventRelay.offer(ev) || super.dispatchTouchEvent(ev)
|
||||
}
|
||||
|
||||
+3
-2
@@ -12,6 +12,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.children
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -22,7 +23,7 @@ import org.signal.core.util.toOptional
|
||||
import org.thoughtcrime.securesms.BindableConversationItem
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.Unbindable
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsNavigator
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapterBridge
|
||||
import org.thoughtcrime.securesms.conversation.ConversationHeaderCallbacks
|
||||
@@ -580,7 +581,7 @@ class ConversationAdapterV2(
|
||||
|
||||
override fun onGroupSettingsClicked() {
|
||||
val recipient = conversationBanner.recipientInfo?.recipient ?: return
|
||||
context.startActivity(ConversationSettingsActivity.forGroup(context, recipient.requireGroupId()))
|
||||
ConversationSettingsNavigator.navigate(context as FragmentActivity, recipient)
|
||||
}
|
||||
|
||||
override fun onShowGroupDescriptionClicked(groupName: String, description: String, linkifyWebLinks: Boolean) {
|
||||
|
||||
+95
-98
@@ -18,6 +18,7 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.graphics.Rect
|
||||
@@ -42,6 +43,7 @@ import android.view.WindowManager
|
||||
import android.view.animation.AnimationUtils
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.TextView.OnEditorActionListener
|
||||
import android.widget.Toast
|
||||
@@ -49,7 +51,9 @@ import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
@@ -59,6 +63,7 @@ import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.doOnPreDraw
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentResultListener
|
||||
@@ -124,6 +129,7 @@ import org.signal.core.util.concurrent.addTo
|
||||
import org.signal.core.util.dp
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.orNull
|
||||
import org.signal.core.util.requireDrawable
|
||||
import org.signal.core.util.requireParcelableCompat
|
||||
import org.signal.core.util.setActionItemTint
|
||||
import org.signal.donations.InAppPaymentType
|
||||
@@ -149,6 +155,7 @@ import org.thoughtcrime.securesms.components.InputAwareConstraintLayout
|
||||
import org.thoughtcrime.securesms.components.InputPanel
|
||||
import org.thoughtcrime.securesms.components.InsetAwareConstraintLayout
|
||||
import org.thoughtcrime.securesms.components.ProgressCardDialogFragment
|
||||
import org.thoughtcrime.securesms.components.RotatedTiledDrawable
|
||||
import org.thoughtcrime.securesms.components.ScrollToPositionDelegate
|
||||
import org.thoughtcrime.securesms.components.SendButton
|
||||
import org.thoughtcrime.securesms.components.SignalProgressDialog
|
||||
@@ -165,7 +172,6 @@ import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.CheckoutFlowActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalFragment
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.snackbars.makeSnackbar
|
||||
import org.thoughtcrime.securesms.components.spoiler.SpoilerAnnotation
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft
|
||||
@@ -184,7 +190,6 @@ import org.thoughtcrime.securesms.conversation.ConversationAdapter
|
||||
import org.thoughtcrime.securesms.conversation.ConversationArgs
|
||||
import org.thoughtcrime.securesms.conversation.ConversationBottomSheetCallback
|
||||
import org.thoughtcrime.securesms.conversation.ConversationData
|
||||
import org.thoughtcrime.securesms.conversation.ConversationHeaderView
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents.ConversationScreenType
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItem
|
||||
@@ -287,16 +292,15 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModelV2
|
||||
import org.thoughtcrime.securesms.longmessage.LongMessageFragment
|
||||
import org.thoughtcrime.securesms.main.MainNavigationChatDetailRouter
|
||||
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation
|
||||
import org.thoughtcrime.securesms.main.MainNavigationListLocation
|
||||
import org.thoughtcrime.securesms.main.MainNavigationRouter
|
||||
import org.thoughtcrime.securesms.main.MainNavigationViewModel
|
||||
import org.thoughtcrime.securesms.main.MainSnackbarHostKey
|
||||
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaPreviewV2Activity
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult
|
||||
import org.thoughtcrime.securesms.messagedetails.MessageDetailsFragment
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository
|
||||
import org.thoughtcrime.securesms.mms.AttachmentManager
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide
|
||||
@@ -344,7 +348,6 @@ import org.thoughtcrime.securesms.stories.StoryViewerArgs
|
||||
import org.thoughtcrime.securesms.stories.viewer.StoryViewerActivity
|
||||
import org.thoughtcrime.securesms.util.BubbleUtil
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.ContextUtil
|
||||
import org.thoughtcrime.securesms.util.ConversationUtil
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.Debouncer
|
||||
@@ -573,11 +576,11 @@ class ConversationFragment :
|
||||
private lateinit var attachmentManager: AttachmentManager
|
||||
private lateinit var multiselectItemDecoration: MultiselectItemDecoration
|
||||
private lateinit var openableGiftItemDecoration: OpenableGiftItemDecoration
|
||||
private lateinit var threadHeaderMarginDecoration: ThreadHeaderMarginDecoration
|
||||
private lateinit var conversationHeaderPositionDecoration: ConversationHeaderPositionDecoration
|
||||
private lateinit var conversationItemDecorations: ConversationItemDecorations
|
||||
private lateinit var optionsMenuCallback: ConversationOptionsMenuCallback
|
||||
|
||||
private var mainNavRouter: MainNavigationRouter? = null
|
||||
private lateinit var chatRouter: MainNavigationChatDetailRouter
|
||||
|
||||
private var animationsAllowed = false
|
||||
private var pinnedShortcutReceiver: BroadcastReceiver? = null
|
||||
@@ -601,6 +604,8 @@ class ConversationFragment :
|
||||
private var firstPinRender: Boolean = true
|
||||
private var skipNextBackPressHandling: Boolean = false
|
||||
private var collapsibleEventScrollPosition: CollapsibleEventScrollPosition? = null
|
||||
private var releaseNotesLayoutApplied: Boolean = false
|
||||
private var releaseNotesWallpaperApplied: Boolean = false
|
||||
|
||||
private val jumpAndPulseScrollStrategy = object : ScrollToPositionDelegate.ScrollStrategy {
|
||||
override fun performScroll(recyclerView: RecyclerView, layoutManager: LinearLayoutManager, position: Int, smooth: Boolean) {
|
||||
@@ -654,7 +659,7 @@ class ConversationFragment :
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
mainNavRouter = context as? MainNavigationRouter
|
||||
chatRouter = context as MainNavigationChatDetailRouter
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -694,6 +699,7 @@ class ConversationFragment :
|
||||
requireActivity(),
|
||||
binding.toolbarBackground,
|
||||
viewModel::wallpaperSnapshot,
|
||||
{ viewModel.recipientSnapshot?.isReleaseNotes == true },
|
||||
viewLifecycleOwner,
|
||||
incognito = args.isIncognito
|
||||
)
|
||||
@@ -761,10 +767,10 @@ class ConversationFragment :
|
||||
|
||||
binding.toolbar.addOnLayoutChangeListener { _, _, _, _, bottom, _, _, _, oldBottom ->
|
||||
binding.conversationItemRecycler.padding(top = bottom)
|
||||
if (bottom != oldBottom && ::threadHeaderMarginDecoration.isInitialized) {
|
||||
if (bottom != oldBottom && ::conversationHeaderPositionDecoration.isInitialized) {
|
||||
val newMargin = bottom + 16.dp
|
||||
if (threadHeaderMarginDecoration.toolbarMargin != newMargin) {
|
||||
threadHeaderMarginDecoration.toolbarMargin = newMargin
|
||||
if (conversationHeaderPositionDecoration.toolbarMargin != newMargin) {
|
||||
conversationHeaderPositionDecoration.toolbarMargin = newMargin
|
||||
binding.conversationItemRecycler.invalidateItemDecorations()
|
||||
}
|
||||
}
|
||||
@@ -1562,6 +1568,10 @@ class ConversationFragment :
|
||||
presentConversationTitle(inputReadyState.conversationRecipient)
|
||||
|
||||
val disabledInputView = binding.conversationDisabledInput
|
||||
val isReleaseNotes = inputReadyState.conversationRecipient.isReleaseNotes
|
||||
if (isReleaseNotes) {
|
||||
applyReleaseNotesLayout()
|
||||
}
|
||||
|
||||
var inputDisabled = true
|
||||
when {
|
||||
@@ -1572,22 +1582,42 @@ class ConversationFragment :
|
||||
inputReadyState.isRequestingMember == true -> disabledInputView.showAsRequestingMember()
|
||||
inputReadyState.isActiveGroup == false -> disabledInputView.showAsNoLongerAMember()
|
||||
inputReadyState.isAnnouncementGroup == true && inputReadyState.isAdmin == false -> disabledInputView.showAsAnnouncementGroupAdminsOnly()
|
||||
inputReadyState.conversationRecipient.isReleaseNotes -> disabledInputView.showAsReleaseNotesChannel(inputReadyState.conversationRecipient)
|
||||
isReleaseNotes -> Unit
|
||||
inputReadyState.shouldShowInviteToSignal() -> disabledInputView.showAsInviteToSignal(requireContext(), inputReadyState.conversationRecipient, inputReadyState.threadContainsSms)
|
||||
else -> inputDisabled = false
|
||||
}
|
||||
|
||||
inputPanel.setHideForMessageRequestState(inputDisabled)
|
||||
|
||||
if (inputDisabled) {
|
||||
if (inputDisabled && !isReleaseNotes) {
|
||||
binding.navBar.setBackgroundColor(disabledInputView.color)
|
||||
} else {
|
||||
} else if (!inputDisabled) {
|
||||
disabledInputView.clear()
|
||||
}
|
||||
|
||||
composeText.setMessageSendType(MessageSendType.SignalMessageSendType)
|
||||
}
|
||||
|
||||
private fun applyReleaseNotesLayout() {
|
||||
if (releaseNotesLayoutApplied) {
|
||||
return
|
||||
}
|
||||
releaseNotesLayoutApplied = true
|
||||
|
||||
binding.conversationReleaseNotesFloatingLabel.visible = true
|
||||
binding.conversationDisabledInput.visible = false
|
||||
|
||||
val navBarInset = ViewCompat.getRootWindowInsets(binding.root)?.getInsets(WindowInsetsCompat.Type.navigationBars())?.bottom ?: 0
|
||||
binding.conversationItemRecycler.updatePadding(bottom = ViewUtil.dpToPx(72) + navBarInset)
|
||||
binding.navBar.setBackgroundColor(Color.TRANSPARENT)
|
||||
|
||||
ConstraintSet().apply {
|
||||
clone(binding.root)
|
||||
connect(binding.conversationItemRecyclerFrame.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
|
||||
applyTo(binding.root)
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentIdentityRecordsState(identityRecordsState: IdentityRecordsState) {
|
||||
binding.conversationTitleView.root.setVerified(identityRecordsState.isVerified)
|
||||
|
||||
@@ -1713,16 +1743,13 @@ class ConversationFragment :
|
||||
}
|
||||
|
||||
private fun onRecipientChanged(recipient: Recipient) {
|
||||
presentWallpaper(recipient.wallpaper)
|
||||
presentWallpaper(recipient)
|
||||
presentConversationTitle(recipient)
|
||||
presentChatColors(recipient.chatColors)
|
||||
invalidateOptionsMenu()
|
||||
updateMessageRequestAcceptedState(!viewModel.hasMessageRequestState)
|
||||
|
||||
recyclerViewColorizer.setChatColors(recipient.chatColors)
|
||||
if (adapter.onHasWallpaperChanged(hasWallpaper = recipient.wallpaper != null)) {
|
||||
conversationItemDecorations.hasWallpaper = recipient.wallpaper != null
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
@@ -1791,7 +1818,7 @@ class ConversationFragment :
|
||||
|
||||
private fun presentNavigationIconForBubble() {
|
||||
binding.toolbar.navigationIcon = DrawableUtil.tint(
|
||||
ContextUtil.requireDrawable(requireContext(), R.drawable.ic_notification),
|
||||
requireContext().requireDrawable(R.drawable.ic_notification),
|
||||
ContextCompat.getColor(requireContext(), R.color.signal_accent_primary)
|
||||
)
|
||||
|
||||
@@ -1842,17 +1869,19 @@ class ConversationFragment :
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentWallpaper(chatWallpaper: ChatWallpaper?) {
|
||||
if (chatWallpaper != null) {
|
||||
chatWallpaper.loadInto(binding.conversationWallpaper)
|
||||
ChatWallpaperDimLevelUtil.applyDimLevelForNightMode(binding.conversationWallpaperDim, chatWallpaper)
|
||||
private fun presentWallpaper(recipient: Recipient) {
|
||||
val chatWallpaper = recipient.wallpaper
|
||||
if (recipient.isReleaseNotes) {
|
||||
applyReleaseNotesWallpaper()
|
||||
} else {
|
||||
binding.conversationWallpaperDim.visible = false
|
||||
applyChatWallpaper(chatWallpaper)
|
||||
}
|
||||
|
||||
val wallpaperEnabled = chatWallpaper != null || recipient.isReleaseNotes
|
||||
|
||||
val toolbarTint = ContextCompat.getColor(
|
||||
requireContext(),
|
||||
if (chatWallpaper != null) {
|
||||
if (wallpaperEnabled) {
|
||||
CoreUiR.color.signal_colorNeutralInverse
|
||||
} else {
|
||||
CoreUiR.color.signal_colorOnSurface
|
||||
@@ -1863,7 +1892,6 @@ class ConversationFragment :
|
||||
binding.toolbar.setActionItemTint(toolbarTint)
|
||||
binding.toolbar.navigationIcon?.setTint(toolbarTint)
|
||||
|
||||
val wallpaperEnabled = chatWallpaper != null
|
||||
binding.conversationWallpaper.visible = wallpaperEnabled
|
||||
binding.scrollToBottom.setWallpaperEnabled(wallpaperEnabled)
|
||||
binding.scrollToMention.setWallpaperEnabled(wallpaperEnabled)
|
||||
@@ -1872,6 +1900,7 @@ class ConversationFragment :
|
||||
|
||||
val stateChanged = adapter.onHasWallpaperChanged(wallpaperEnabled)
|
||||
conversationItemDecorations.hasWallpaper = wallpaperEnabled
|
||||
conversationItemDecorations.isReleaseNotes = recipient.isReleaseNotes
|
||||
if (stateChanged) {
|
||||
binding.conversationItemRecycler.invalidateItemDecorations()
|
||||
}
|
||||
@@ -1888,12 +1917,39 @@ class ConversationFragment :
|
||||
)
|
||||
|
||||
if (!inputPanel.isHidden) {
|
||||
setNavBarBackgroundColor(chatWallpaper)
|
||||
setNavBarBackgroundColor(wallpaperEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setNavBarBackgroundColor(chatWallpaper: ChatWallpaper?) {
|
||||
val navColor = if (chatWallpaper != null) {
|
||||
private fun applyReleaseNotesWallpaper() {
|
||||
if (releaseNotesWallpaperApplied) {
|
||||
return
|
||||
}
|
||||
releaseNotesWallpaperApplied = true
|
||||
|
||||
val tinted = DrawableUtil.tint(
|
||||
AppCompatResources.getDrawable(requireContext(), R.drawable.release_chat_background)!!,
|
||||
ContextCompat.getColor(requireContext(), R.color.release_notes_background_pattern)
|
||||
)
|
||||
val bitmap = DrawableUtil.toBitmap(tinted, tinted.intrinsicWidth, tinted.intrinsicHeight)
|
||||
|
||||
binding.conversationWallpaper.scaleType = ImageView.ScaleType.MATRIX
|
||||
binding.conversationWallpaper.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.release_notes_background))
|
||||
binding.conversationWallpaper.setImageDrawable(RotatedTiledDrawable(bitmap, -45f))
|
||||
binding.conversationWallpaperDim.visible = false
|
||||
}
|
||||
|
||||
private fun applyChatWallpaper(chatWallpaper: ChatWallpaper?) {
|
||||
if (chatWallpaper != null) {
|
||||
chatWallpaper.loadInto(binding.conversationWallpaper)
|
||||
ChatWallpaperDimLevelUtil.applyDimLevelForNightMode(binding.conversationWallpaperDim, chatWallpaper)
|
||||
} else {
|
||||
binding.conversationWallpaperDim.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun setNavBarBackgroundColor(hasWallpaper: Boolean) {
|
||||
val navColor = if (hasWallpaper) {
|
||||
R.color.conversation_navigation_wallpaper
|
||||
} else {
|
||||
CoreUiR.color.signal_colorBackground
|
||||
@@ -2237,12 +2293,11 @@ class ConversationFragment :
|
||||
}
|
||||
)
|
||||
|
||||
threadHeaderMarginDecoration = ThreadHeaderMarginDecoration()
|
||||
conversationHeaderPositionDecoration = ConversationHeaderPositionDecoration()
|
||||
|
||||
val statusBarInset = ViewCompat.getRootWindowInsets(binding.root)?.getInsets(WindowInsetsCompat.Type.systemBars())?.top ?: 0
|
||||
threadHeaderMarginDecoration.toolbarMargin = statusBarInset + resources.getDimensionPixelSize(R.dimen.signal_m3_toolbar_height) + 16.dp
|
||||
binding.conversationItemRecycler.addItemDecoration(threadHeaderMarginDecoration)
|
||||
binding.conversationItemRecycler.addItemDecoration(ConversationHeaderPositionDecoration())
|
||||
conversationHeaderPositionDecoration.toolbarMargin = statusBarInset + resources.getDimensionPixelSize(R.dimen.signal_m3_toolbar_height) + 16.dp
|
||||
binding.conversationItemRecycler.addItemDecoration(conversationHeaderPositionDecoration)
|
||||
|
||||
conversationItemDecorations = ConversationItemDecorations(hasWallpaper = args.hasWallpaper)
|
||||
binding.conversationItemRecycler.addItemDecoration(conversationItemDecorations, 0)
|
||||
@@ -3035,7 +3090,7 @@ class ConversationFragment :
|
||||
|
||||
private fun handleDisplayDetails(conversationMessage: ConversationMessage) {
|
||||
val recipientSnapshot = viewModel.recipientSnapshot ?: return
|
||||
navigateTo(MainNavigationDetailLocation.Chats.MessageDetails(recipientSnapshot.id, MessageId(conversationMessage.messageRecord.id)))
|
||||
chatRouter.goToChatDetail(MainNavigationDetailLocation.Chats.MessageDetails(recipientSnapshot.id, MessageId(conversationMessage.messageRecord.id)))
|
||||
}
|
||||
|
||||
private fun handleDeleteMessages(messageParts: Set<MultiselectPart>) {
|
||||
@@ -3330,7 +3385,8 @@ class ConversationFragment :
|
||||
|
||||
private fun presentComposeDivider() {
|
||||
val isAtBottom = isScrolledToBottom()
|
||||
if (isAtBottom && !wasAtBottom) {
|
||||
val suppress = viewModel.recipientSnapshot?.isReleaseNotes == true
|
||||
if ((isAtBottom && !wasAtBottom) || suppress) {
|
||||
ViewUtil.fadeOut(binding.composeDivider, 50, View.INVISIBLE)
|
||||
} else if (wasAtBottom && !isAtBottom) {
|
||||
ViewUtil.fadeIn(binding.composeDivider, 500)
|
||||
@@ -3574,7 +3630,7 @@ class ConversationFragment :
|
||||
} else if (messageRecord.hasFailedWithNetworkFailures()) {
|
||||
ConversationDialogs.displayMessageCouldNotBeSentDialog(requireContext(), messageRecord)
|
||||
} else {
|
||||
navigateTo(MainNavigationDetailLocation.Chats.MessageDetails(recipientId, MessageId(messageRecord.id)))
|
||||
chatRouter.goToChatDetail(MainNavigationDetailLocation.Chats.MessageDetails(recipientId, MessageId(messageRecord.id)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4248,7 +4304,7 @@ class ConversationFragment :
|
||||
|
||||
override fun handleManageGroup() {
|
||||
viewModel.recipientSnapshot?.let { recipient ->
|
||||
navigateTo(MainNavigationDetailLocation.Chats.ConversationSettings(recipient.id))
|
||||
chatRouter.goToChatDetail(MainNavigationDetailLocation.Chats.ConversationSettings(recipient.id))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4285,7 +4341,7 @@ class ConversationFragment :
|
||||
override fun handleConversationSettings() {
|
||||
viewModel.recipientSnapshot?.let { recipient ->
|
||||
if (!viewModel.hasMessageRequestState || recipient.isBlocked) {
|
||||
navigateTo(MainNavigationDetailLocation.Chats.ConversationSettings(recipient.id))
|
||||
chatRouter.goToChatDetail(MainNavigationDetailLocation.Chats.ConversationSettings(recipient.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4352,50 +4408,6 @@ class ConversationFragment :
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Routes to the appropriate destination based on the current window configuration.
|
||||
*
|
||||
* In split-pane mode, delegates to the [MainNavigationRouter] to display content in the detail pane. Otherwise, opens the destination as a standalone screen.
|
||||
*/
|
||||
private fun navigateTo(location: MainNavigationDetailLocation.Chats) {
|
||||
val router = mainNavRouter
|
||||
if (router != null && resources.isSplitPane()) {
|
||||
router.goTo(location)
|
||||
} else {
|
||||
when (location) {
|
||||
is MainNavigationDetailLocation.Chats.MessageDetails -> navigateToMessageDetailsStandalone(location)
|
||||
is MainNavigationDetailLocation.Chats.ConversationSettings -> navigateToConversationSettingsStandalone(viewModel.recipientSnapshot!!)
|
||||
is MainNavigationDetailLocation.Chats.Conversation -> error("ConversationFragment shouldn't navigate to another conversation - use the main navigation infrastructure instead.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens message details as a standalone (single-pane) screen. Use [navigateTo] as the entry point.
|
||||
*/
|
||||
private fun navigateToMessageDetailsStandalone(location: MainNavigationDetailLocation.Chats.MessageDetails) {
|
||||
MessageDetailsFragment.create(location.messageId, location.recipientId)
|
||||
.show(requireActivity().supportFragmentManager, MESSAGE_DETAILS_TAG)
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens conversation settings as a standalone (single-pane) screen.
|
||||
*/
|
||||
private fun navigateToConversationSettingsStandalone(recipient: Recipient) {
|
||||
val intent = if (recipient.isPushGroup) {
|
||||
ConversationSettingsActivity.forGroup(requireContext(), recipient.requireGroupId())
|
||||
} else {
|
||||
ConversationSettingsActivity.forRecipient(requireContext(), recipient.id)
|
||||
}
|
||||
|
||||
val bundle = ConversationSettingsActivity.createTransitionBundle(
|
||||
requireActivity(),
|
||||
binding.conversationTitleView.root.findViewById(R.id.contact_photo_image),
|
||||
binding.toolbar
|
||||
)
|
||||
requireActivity().startActivity(intent, bundle)
|
||||
}
|
||||
|
||||
private inner class OnReactionsSelectedListener : ConversationReactionOverlay.OnReactionSelectedListener {
|
||||
override fun onReactionSelected(messageRecord: MessageRecord, emoji: String?) {
|
||||
reactionDelegate.hide()
|
||||
@@ -4734,10 +4746,6 @@ class ConversationFragment :
|
||||
launchIntent = this@ConversationFragment::startActivity
|
||||
)
|
||||
}
|
||||
|
||||
override fun onUnmuteReleaseNotesChannel() {
|
||||
viewModel.muteConversation(0L)
|
||||
}
|
||||
}
|
||||
|
||||
//endregion
|
||||
@@ -5126,7 +5134,7 @@ class ConversationFragment :
|
||||
}
|
||||
|
||||
override fun onInputHidden() {
|
||||
setNavBarBackgroundColor(viewModel.wallpaperSnapshot)
|
||||
setNavBarBackgroundColor(viewModel.wallpaperSnapshot != null || viewModel.recipientSnapshot?.isReleaseNotes == true)
|
||||
viewModel.setIsMediaKeyboardShowing(false)
|
||||
}
|
||||
|
||||
@@ -5248,17 +5256,6 @@ class ConversationFragment :
|
||||
}
|
||||
}
|
||||
|
||||
private inner class ThreadHeaderMarginDecoration : RecyclerView.ItemDecoration() {
|
||||
var toolbarMargin: Int = 0
|
||||
|
||||
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
if (view is ConversationHeaderView) {
|
||||
outRect.top = toolbarMargin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class VoiceMessageRecordingSessionCallbacks : VoiceMessageRecordingDelegate.SessionCallback {
|
||||
override fun onSessionWillBegin() {
|
||||
getVoiceNoteMediaController().pausePlayback()
|
||||
|
||||
+29
-19
@@ -7,35 +7,45 @@ package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import androidx.core.view.children
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.thoughtcrime.securesms.conversation.ConversationHeaderView
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Adjusts the Conversation's recycler view translationY so that the conversation header
|
||||
* is pinned to the top of the visible area when content is too short to
|
||||
* fill the screen.
|
||||
* Reserves space above the [ConversationHeaderView] for the toolbar and adjusts the conversation RecyclerView's translationY so the header is pinned below the
|
||||
* toolbar when content is short enough to fit the viewport. The toolbar margin is only contributed when a translation is actually going to be applied; when
|
||||
* content overflows, no margin is added and no translation is applied.
|
||||
*/
|
||||
class ConversationHeaderPositionDecoration : RecyclerView.ItemDecoration() {
|
||||
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
if (parent.childCount == 0 || parent.canScrollVertically(-1) || parent.canScrollVertically(1)) {
|
||||
parent.translationY = 0f
|
||||
} else {
|
||||
val threadHeaderView: ConversationHeaderView = parent.children
|
||||
.filterIsInstance<ConversationHeaderView>()
|
||||
.firstOrNull() ?: run {
|
||||
parent.translationY = 0f
|
||||
return
|
||||
}
|
||||
private val bounds = Rect()
|
||||
|
||||
// A decorator adds the margin for the toolbar, margin is the difference of the bounds "height" and the view height
|
||||
val bounds = Rect()
|
||||
parent.getDecoratedBoundsWithMargins(threadHeaderView, bounds)
|
||||
val toolbarMargin = bounds.bottom - bounds.top - threadHeaderView.height
|
||||
var toolbarMargin: Int = 0
|
||||
|
||||
val childTop: Int = threadHeaderView.top - toolbarMargin
|
||||
parent.translationY = min(0, -childTop).toFloat()
|
||||
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
if (view is ConversationHeaderView && !parent.canScrollVertically(1)) {
|
||||
outRect.top = toolbarMargin
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
if (parent.canScrollVertically(1)) {
|
||||
parent.translationY = 0f
|
||||
return
|
||||
}
|
||||
|
||||
val threadHeaderView: ConversationHeaderView = parent.children
|
||||
.filterIsInstance<ConversationHeaderView>()
|
||||
.firstOrNull() ?: run {
|
||||
parent.translationY = 0f
|
||||
return
|
||||
}
|
||||
|
||||
parent.getDecoratedBoundsWithMargins(threadHeaderView, bounds)
|
||||
val margin = bounds.bottom - bounds.top - threadHeaderView.height
|
||||
val childTop: Int = threadHeaderView.top - margin
|
||||
parent.translationY = min(0, -childTop).toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
+10
-1
@@ -59,6 +59,12 @@ class ConversationItemDecorations(hasWallpaper: Boolean = false, private val sch
|
||||
unreadViewHolder?.updateForWallpaper()
|
||||
}
|
||||
|
||||
var isReleaseNotes: Boolean = false
|
||||
set(value) {
|
||||
field = value
|
||||
headerCache.values.forEach { it.updateForWallpaper() }
|
||||
}
|
||||
|
||||
var selfRecipientId: RecipientId? = null
|
||||
|
||||
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||
@@ -307,7 +313,10 @@ class ConversationItemDecorations(hasWallpaper: Boolean = false, private val sch
|
||||
}
|
||||
|
||||
fun updateForWallpaper() {
|
||||
if (hasWallpaper) {
|
||||
if (isReleaseNotes) {
|
||||
date.setBackgroundResource(R.drawable.release_notes_date_header_background)
|
||||
date.setTextColor(ContextCompat.getColor(itemView.context, CoreUiR.color.signal_colorOnSurfaceVariant))
|
||||
} else if (hasWallpaper) {
|
||||
date.setBackgroundResource(R.drawable.wallpaper_bubble_background_18)
|
||||
date.setTextColor(ContextCompat.getColor(itemView.context, CoreUiR.color.signal_colorNeutralInverse))
|
||||
} else {
|
||||
|
||||
+11
-2
@@ -16,6 +16,7 @@ class ConversationToolbarOnScrollHelper(
|
||||
activity: FragmentActivity,
|
||||
toolbarBackground: View,
|
||||
private val wallpaperProvider: () -> ChatWallpaper?,
|
||||
private val releaseNotesProvider: () -> Boolean,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
private val incognito: Boolean = false
|
||||
) : Material3OnScrollHelper(
|
||||
@@ -25,10 +26,18 @@ class ConversationToolbarOnScrollHelper(
|
||||
setStatusBarColor = {}
|
||||
) {
|
||||
override val activeColorSet: ColorSet
|
||||
get() = if (incognito) ColorSet(R.color.conversation_toolbar_color_incognito) else ColorSet(getActiveToolbarColor(wallpaperProvider() != null))
|
||||
get() = when {
|
||||
incognito -> ColorSet(R.color.conversation_toolbar_color_incognito)
|
||||
releaseNotesProvider() -> ColorSet(R.color.release_notes_toolbar_scrolled)
|
||||
else -> ColorSet(getActiveToolbarColor(wallpaperProvider() != null))
|
||||
}
|
||||
|
||||
override val inactiveColorSet: ColorSet
|
||||
get() = if (incognito) ColorSet(R.color.conversation_toolbar_color_incognito) else ColorSet(getInactiveToolbarColor(wallpaperProvider() != null))
|
||||
get() = when {
|
||||
incognito -> ColorSet(R.color.conversation_toolbar_color_incognito)
|
||||
releaseNotesProvider() -> ColorSet(R.color.release_notes_toolbar_transparent)
|
||||
else -> ColorSet(getInactiveToolbarColor(wallpaperProvider() != null))
|
||||
}
|
||||
|
||||
@ColorRes
|
||||
private fun getActiveToolbarColor(hasWallpaper: Boolean): Int {
|
||||
|
||||
@@ -48,7 +48,6 @@ class DisabledInputView @JvmOverloads constructor(
|
||||
private var requestingGroup: View? = null
|
||||
private var announcementGroupOnly: TextView? = null
|
||||
private var inviteToSignal: View? = null
|
||||
private var releaseNoteChannel: View? = null
|
||||
private var incognitoView: View? = null
|
||||
|
||||
private var currentView: View? = null
|
||||
@@ -187,21 +186,6 @@ class DisabledInputView @JvmOverloads constructor(
|
||||
)
|
||||
}
|
||||
|
||||
fun showAsReleaseNotesChannel(recipient: Recipient) {
|
||||
releaseNoteChannel = show(
|
||||
existingView = releaseNoteChannel,
|
||||
create = { inflater.inflate(R.layout.conversation_activity_unmute, this, false) },
|
||||
bind = {
|
||||
if (recipient.isMuted) {
|
||||
visible = true
|
||||
findViewById<View>(R.id.conversation_activity_unmute_button).setOnClickListener { listener?.onUnmuteReleaseNotesChannel() }
|
||||
} else {
|
||||
visible = false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun setWallpaperEnabled(wallpaperEnabled: Boolean) {
|
||||
color = ContextCompat.getColor(context, if (wallpaperEnabled) R.color.wallpaper_bubble_color else CoreUiR.color.signal_colorBackground)
|
||||
setBackgroundColor(color)
|
||||
@@ -272,7 +256,6 @@ class DisabledInputView @JvmOverloads constructor(
|
||||
fun onBlockClicked()
|
||||
fun onUnblockClicked()
|
||||
fun onInviteToSignal(recipient: Recipient)
|
||||
fun onUnmuteReleaseNotesChannel()
|
||||
fun onReportSpamClicked()
|
||||
}
|
||||
}
|
||||
|
||||
+9
-2
@@ -444,14 +444,21 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
V2ConversationItemUtils.linkifyUrlLinks(messageBody, conversationContext.selectedItems.isEmpty(), conversationContext.clickListener::onUrlClicked)
|
||||
|
||||
if (conversationMessage.hasStyleLinks()) {
|
||||
val isReleaseNotes = conversationMessage.threadRecipient.isReleaseNotes
|
||||
val linkColor = if (isReleaseNotes) {
|
||||
themeDelegate.getBodyTextColor(conversationMessage)
|
||||
} else {
|
||||
ContextCompat.getColor(getContext(), R.color.signal_accent_primary)
|
||||
}
|
||||
val underline = isReleaseNotes
|
||||
messageBody.getSpans(0, messageBody.length, PlaceholderURLSpan::class.java).forEach { placeholder ->
|
||||
val start = messageBody.getSpanStart(placeholder)
|
||||
val end = messageBody.getSpanEnd(placeholder)
|
||||
val span: URLSpan = InterceptableLongClickCopyLinkSpan(
|
||||
placeholder.value,
|
||||
conversationContext.clickListener::onUrlClicked,
|
||||
ContextCompat.getColor(getContext(), R.color.signal_accent_primary),
|
||||
false
|
||||
linkColor,
|
||||
underline
|
||||
)
|
||||
|
||||
messageBody.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
|
||||
+9
@@ -44,6 +44,10 @@ class V2ConversationItemTheme(
|
||||
return conversationContext.getColorizer().getIncomingFooterTextColor(context, conversationContext.hasWallpaper())
|
||||
}
|
||||
|
||||
if (!conversationMessage.messageRecord.isOutgoing && conversationMessage.threadRecipient.isReleaseNotes) {
|
||||
return ContextCompat.getColor(context, R.color.release_notes_bubble_text)
|
||||
}
|
||||
|
||||
return getColor(
|
||||
conversationMessage,
|
||||
conversationContext.getColorizer()::getOutgoingFooterTextColor,
|
||||
@@ -55,6 +59,9 @@ class V2ConversationItemTheme(
|
||||
fun getBodyTextColor(
|
||||
conversationMessage: ConversationMessage
|
||||
): Int {
|
||||
if (!conversationMessage.messageRecord.isOutgoing && conversationMessage.threadRecipient.isReleaseNotes) {
|
||||
return ContextCompat.getColor(context, R.color.release_notes_bubble_text)
|
||||
}
|
||||
return getColor(
|
||||
conversationMessage,
|
||||
conversationContext.getColorizer()::getOutgoingBodyTextColor,
|
||||
@@ -79,6 +86,8 @@ class V2ConversationItemTheme(
|
||||
): Int {
|
||||
return if (conversationMessage.messageRecord.isOutgoing) {
|
||||
Color.TRANSPARENT
|
||||
} else if (conversationMessage.threadRecipient.isReleaseNotes) {
|
||||
ContextCompat.getColor(context, R.color.release_notes_bubble)
|
||||
} else {
|
||||
if (conversationContext.hasWallpaper()) {
|
||||
ContextCompat.getColor(context, R.color.conversation_item_recv_bubble_color_wallpaper)
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ fun Fragment.listenToEventBusWhileResumed(
|
||||
.collectLatest {
|
||||
if (!resources.isSplitPane()) {
|
||||
when (it) {
|
||||
is MainNavigationDetailLocation.Chats.Conversation -> unsubscribe()
|
||||
is MainNavigationDetailLocation.Conversation -> unsubscribe()
|
||||
MainNavigationDetailLocation.Empty -> subscribe()
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
+1
-2
@@ -47,6 +47,7 @@ import com.bumptech.glide.RequestManager;
|
||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
|
||||
import com.makeramen.roundedimageview.RoundedDrawable;
|
||||
|
||||
import org.signal.core.util.ContextUtil;
|
||||
import org.signal.core.util.DimensionUnit;
|
||||
import org.signal.core.util.StringUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
@@ -72,14 +73,12 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.database.model.UpdateDescription;
|
||||
import org.thoughtcrime.securesms.fonts.SignalSymbols.Glyph;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.glide.targets.GlideLiveDataTarget;
|
||||
import org.signal.glide.decryptableuri.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.search.MessageResult;
|
||||
import org.thoughtcrime.securesms.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user