mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-15 14:33:19 +01:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9ae6d5d9b | ||
|
|
7b5ebea9c3 |
7
.github/workflows/android.yml
vendored
7
.github/workflows/android.yml
vendored
@@ -14,12 +14,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: set up JDK 11
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 11
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
@@ -33,7 +32,7 @@ jobs:
|
||||
|
||||
- name: Archive reports for failed build
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: reports
|
||||
path: '*/build/reports'
|
||||
|
||||
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build image
|
||||
run: cd reproducible-builds && docker build -t signal-android . && cd ..
|
||||
|
||||
|
||||
@@ -10,6 +10,12 @@ apply plugin: 'kotlin-parcelize'
|
||||
apply from: 'static-ips.gradle'
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url "https://raw.github.com/signalapp/maven/master/circular-progress-button/releases/"
|
||||
content {
|
||||
includeGroupByRegex "com\\.github\\.dmytrodanylyk\\.circular-progress-button\\.*"
|
||||
}
|
||||
}
|
||||
maven {
|
||||
url "https://raw.github.com/signalapp/maven/master/sqlcipher/release/"
|
||||
content {
|
||||
@@ -57,15 +63,15 @@ ktlint {
|
||||
version = "0.43.2"
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 1080
|
||||
def canonicalVersionName = "5.42.2"
|
||||
def canonicalVersionCode = 1064
|
||||
def canonicalVersionName = "5.40.4.1"
|
||||
|
||||
def postFixSize = 100
|
||||
def abiPostFix = ['universal' : 0,
|
||||
'armeabi-v7a' : 1,
|
||||
'arm64-v8a' : 2,
|
||||
'x86' : 3,
|
||||
'x86_64' : 4]
|
||||
def abiPostFix = ['universal' : 5,
|
||||
'armeabi-v7a' : 6,
|
||||
'arm64-v8a' : 7,
|
||||
'x86' : 8,
|
||||
'x86_64' : 9]
|
||||
|
||||
def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties') ]
|
||||
|
||||
@@ -151,8 +157,6 @@ android {
|
||||
exclude 'META-INF/LICENSE'
|
||||
exclude 'META-INF/NOTICE'
|
||||
exclude 'META-INF/proguard/androidx-annotations.pro'
|
||||
exclude 'libsignal_jni.dylib'
|
||||
exclude 'signal_jni.dll'
|
||||
}
|
||||
|
||||
|
||||
@@ -470,6 +474,7 @@ dependencies {
|
||||
implementation libs.materialish.progress
|
||||
implementation libs.greenrobot.eventbus
|
||||
implementation libs.waitingdots
|
||||
implementation libs.floatingactionbutton
|
||||
implementation libs.google.zxing.android.integration
|
||||
implementation libs.time.duration.picker
|
||||
implementation libs.google.zxing.core
|
||||
@@ -495,6 +500,7 @@ dependencies {
|
||||
implementation libs.lottie
|
||||
|
||||
implementation libs.stickyheadergrid
|
||||
implementation libs.circular.progress.button
|
||||
|
||||
implementation libs.signal.android.database.sqlcipher
|
||||
implementation libs.androidx.sqlite
|
||||
|
||||
@@ -80,6 +80,25 @@ class DistributionListDatabaseTest {
|
||||
Assert.assertEquals(StoryType.STORY_WITHOUT_REPLIES, storyType)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenStoryExistsAndMarkedNoReplies_getAllListsForContactSelectionUi_returnsStoryWithoutReplies() {
|
||||
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
|
||||
Assert.assertNotNull(id)
|
||||
distributionDatabase.setAllowsReplies(id!!, false)
|
||||
|
||||
val records = distributionDatabase.getAllListsForContactSelectionUi(null, false)
|
||||
Assert.assertFalse(records.first().allowsReplies)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenStoryExists_getAllListsForContactSelectionUi_returnsStoryWithReplies() {
|
||||
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
|
||||
Assert.assertNotNull(id)
|
||||
|
||||
val records = distributionDatabase.getAllListsForContactSelectionUi(null, false)
|
||||
Assert.assertTrue(records.first().allowsReplies)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun givenStoryDoesNotExist_getStoryType_throwsIllegalStateException() {
|
||||
distributionDatabase.getStoryType(DistributionListId.from(12))
|
||||
|
||||
@@ -1,810 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.core.content.contentValuesOf
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.lang.AssertionError
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.lang.IllegalStateException
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RecipientDatabaseTest_processPnpTupleToChangeSet {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val databaseRule = SignalDatabaseRule(deleteAllThreadsOnEachRun = false)
|
||||
|
||||
private lateinit var db: RecipientDatabase
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
db = SignalDatabase.recipients
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noMatch_e164Only() {
|
||||
val changeSet = db.processPnpTupleToChangeSet(E164_A, null, null, pniVerified = false)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpInsert(E164_A, null, null)
|
||||
),
|
||||
changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noMatch_e164AndPni() {
|
||||
val changeSet = db.processPnpTupleToChangeSet(E164_A, PNI_A, null, pniVerified = false)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpInsert(E164_A, PNI_A, null)
|
||||
),
|
||||
changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noMatch_aciOnly() {
|
||||
val changeSet = db.processPnpTupleToChangeSet(null, null, ACI_A, pniVerified = false)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpInsert(null, null, ACI_A)
|
||||
),
|
||||
changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun noMatch_pniOnly() {
|
||||
db.processPnpTupleToChangeSet(null, PNI_A, null, pniVerified = false)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun noMatch_noData() {
|
||||
db.processPnpTupleToChangeSet(null, null, null, pniVerified = false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noMatch_allFields() {
|
||||
val changeSet = db.processPnpTupleToChangeSet(E164_A, PNI_A, ACI_A, pniVerified = false)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpInsert(E164_A, PNI_A, ACI_A)
|
||||
),
|
||||
changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fullMatch() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_A, PNI_A, ACI_A),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyE164Matches() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_A, null, null),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetPni(result.id, PNI_A),
|
||||
PnpOperation.SetAci(result.id, ACI_A)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyE164Matches_pniChanges_noAciProvided_existingPniSession() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_A, PNI_B, null, pniSession = true),
|
||||
Update(E164_A, PNI_A, null),
|
||||
Output(E164_A, PNI_A, null)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetPni(result.id, PNI_A),
|
||||
PnpOperation.SessionSwitchoverInsert(result.id)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyE164Matches_pniChanges_noAciProvided_noPniSession() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_A, PNI_B, null),
|
||||
Update(E164_A, PNI_A, null),
|
||||
Output(E164_A, PNI_A, null)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetPni(result.id, PNI_A)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun e164AndPniMatches_noExistingSession() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_A, PNI_A, null),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetAci(result.id, ACI_A)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun e164AndPniMatches_existingPniSession() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_A, PNI_A, null, pniSession = true),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetAci(result.id, ACI_A),
|
||||
PnpOperation.SessionSwitchoverInsert(result.id)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun e164AndAciMatches() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_A, null, ACI_A),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetPni(result.id, PNI_A)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyPniMatches_noExistingSession() {
|
||||
val result = applyAndAssert(
|
||||
Input(null, PNI_A, null),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetE164(result.id, E164_A),
|
||||
PnpOperation.SetAci(result.id, ACI_A)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyPniMatches_existingPniSession() {
|
||||
val result = applyAndAssert(
|
||||
Input(null, PNI_A, null, pniSession = true),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetE164(result.id, E164_A),
|
||||
PnpOperation.SetAci(result.id, ACI_A),
|
||||
PnpOperation.SessionSwitchoverInsert(result.id)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyPniMatches_existingPniSession_changeNumber() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_B, PNI_A, null, pniSession = true),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetE164(result.id, E164_A),
|
||||
PnpOperation.SetAci(result.id, ACI_A),
|
||||
PnpOperation.ChangeNumberInsert(
|
||||
recipientId = result.id,
|
||||
oldE164 = E164_B,
|
||||
newE164 = E164_A
|
||||
),
|
||||
PnpOperation.SessionSwitchoverInsert(result.id)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pniAndAciMatches() {
|
||||
val result = applyAndAssert(
|
||||
Input(null, PNI_A, ACI_A),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetE164(result.id, E164_A),
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pniAndAciMatches_changeNumber() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_B, PNI_A, ACI_A),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetE164(result.id, E164_A),
|
||||
PnpOperation.ChangeNumberInsert(
|
||||
recipientId = result.id,
|
||||
oldE164 = E164_B,
|
||||
newE164 = E164_A
|
||||
)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyAciMatches() {
|
||||
val result = applyAndAssert(
|
||||
Input(null, null, ACI_A),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetE164(result.id, E164_A),
|
||||
PnpOperation.SetPni(result.id, PNI_A)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyAciMatches_changeNumber() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_B, null, ACI_A),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetE164(result.id, E164_A),
|
||||
PnpOperation.SetPni(result.id, PNI_A),
|
||||
PnpOperation.ChangeNumberInsert(
|
||||
recipientId = result.id,
|
||||
oldE164 = E164_B,
|
||||
newE164 = E164_A
|
||||
)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164Only_pniOnly_aciOnly() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, null, null),
|
||||
Input(null, PNI_A, null),
|
||||
Input(null, null, ACI_A)
|
||||
),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.thirdId),
|
||||
operations = listOf(
|
||||
PnpOperation.Merge(
|
||||
primaryId = result.firstId,
|
||||
secondaryId = result.secondId
|
||||
),
|
||||
PnpOperation.Merge(
|
||||
primaryId = result.thirdId,
|
||||
secondaryId = result.firstId
|
||||
)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164Only_pniOnly_noAciProvided() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, null, null),
|
||||
Input(null, PNI_A, null),
|
||||
),
|
||||
Update(E164_A, PNI_A, null),
|
||||
Output(E164_A, PNI_A, null)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.firstId),
|
||||
operations = listOf(
|
||||
PnpOperation.Merge(
|
||||
primaryId = result.firstId,
|
||||
secondaryId = result.secondId
|
||||
)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164Only_pniOnly_aciProvidedButNoAciRecord() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, null, null),
|
||||
Input(null, PNI_A, null),
|
||||
),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.firstId),
|
||||
operations = listOf(
|
||||
PnpOperation.Merge(
|
||||
primaryId = result.firstId,
|
||||
secondaryId = result.secondId
|
||||
),
|
||||
PnpOperation.SetAci(
|
||||
recipientId = result.firstId,
|
||||
aci = ACI_A
|
||||
)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164Only_pniAndE164_noAciProvided() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, null, null),
|
||||
Input(E164_B, PNI_A, null),
|
||||
),
|
||||
Update(E164_A, PNI_A, null),
|
||||
Output(E164_A, PNI_A, null)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.firstId),
|
||||
operations = listOf(
|
||||
PnpOperation.RemovePni(result.secondId),
|
||||
PnpOperation.SetPni(
|
||||
recipientId = result.firstId,
|
||||
pni = PNI_A
|
||||
),
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164AndPni_pniOnly_noAciProvided() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, PNI_B, null),
|
||||
Input(null, PNI_A, null),
|
||||
),
|
||||
Update(E164_A, PNI_A, null),
|
||||
Output(E164_A, PNI_A, null)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.firstId),
|
||||
operations = listOf(
|
||||
PnpOperation.RemovePni(result.firstId),
|
||||
PnpOperation.Merge(
|
||||
primaryId = result.firstId,
|
||||
secondaryId = result.secondId
|
||||
),
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164AndPni_e164AndPni_noAciProvided_noSessions() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, PNI_B, null),
|
||||
Input(E164_B, PNI_A, null),
|
||||
),
|
||||
Update(E164_A, PNI_A, null),
|
||||
Output(E164_A, PNI_A, null)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.firstId),
|
||||
operations = listOf(
|
||||
PnpOperation.RemovePni(result.secondId),
|
||||
PnpOperation.SetPni(result.firstId, PNI_A)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164AndPni_e164AndPni_noAciProvided_sessionsExist() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, PNI_B, null, pniSession = true),
|
||||
Input(E164_B, PNI_A, null, pniSession = true),
|
||||
),
|
||||
Update(E164_A, PNI_A, null),
|
||||
Output(E164_A, PNI_A, null)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.firstId),
|
||||
operations = listOf(
|
||||
PnpOperation.RemovePni(result.secondId),
|
||||
PnpOperation.SetPni(result.firstId, PNI_A),
|
||||
PnpOperation.SessionSwitchoverInsert(result.secondId),
|
||||
PnpOperation.SessionSwitchoverInsert(result.firstId)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164AndPni_aciOnly() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, PNI_A, null),
|
||||
Input(null, null, ACI_A),
|
||||
),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.secondId),
|
||||
operations = listOf(
|
||||
PnpOperation.Merge(
|
||||
primaryId = result.secondId,
|
||||
secondaryId = result.firstId
|
||||
),
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164AndPni_aciOnly_e164RecordHasSeparateE164() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_B, PNI_A, null),
|
||||
Input(null, null, ACI_A),
|
||||
),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.secondId),
|
||||
operations = listOf(
|
||||
PnpOperation.RemovePni(result.firstId),
|
||||
PnpOperation.Update(
|
||||
recipientId = result.secondId,
|
||||
e164 = E164_A,
|
||||
pni = PNI_A,
|
||||
aci = ACI_A
|
||||
)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164AndPni_e164AndPniAndAci_changeNumber() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, PNI_A, null),
|
||||
Input(E164_B, PNI_B, ACI_A),
|
||||
),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.secondId),
|
||||
operations = listOf(
|
||||
PnpOperation.RemovePni(result.secondId),
|
||||
PnpOperation.RemoveE164(result.secondId),
|
||||
PnpOperation.Merge(
|
||||
primaryId = result.secondId,
|
||||
secondaryId = result.firstId
|
||||
),
|
||||
PnpOperation.ChangeNumberInsert(
|
||||
recipientId = result.secondId,
|
||||
oldE164 = E164_B,
|
||||
newE164 = E164_A
|
||||
)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164AndPni_e164Aci_changeNumber() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, PNI_A, null),
|
||||
Input(E164_B, null, ACI_A),
|
||||
),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.secondId),
|
||||
operations = listOf(
|
||||
PnpOperation.RemoveE164(result.secondId),
|
||||
PnpOperation.Merge(
|
||||
primaryId = result.secondId,
|
||||
secondaryId = result.firstId
|
||||
),
|
||||
PnpOperation.ChangeNumberInsert(
|
||||
recipientId = result.secondId,
|
||||
oldE164 = E164_B,
|
||||
newE164 = E164_A
|
||||
)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
private fun insert(e164: String?, pni: PNI?, aci: ACI?): RecipientId {
|
||||
val id: Long = SignalDatabase.rawDatabase.insert(
|
||||
RecipientDatabase.TABLE_NAME,
|
||||
null,
|
||||
contentValuesOf(
|
||||
RecipientDatabase.PHONE to e164,
|
||||
RecipientDatabase.SERVICE_ID to (aci ?: pni)?.toString(),
|
||||
RecipientDatabase.PNI_COLUMN to pni?.toString(),
|
||||
RecipientDatabase.REGISTERED to RecipientDatabase.RegisteredState.REGISTERED.id
|
||||
)
|
||||
)
|
||||
|
||||
return RecipientId.from(id)
|
||||
}
|
||||
|
||||
private fun insertMockSessionFor(account: ServiceId, address: ServiceId) {
|
||||
SignalDatabase.rawDatabase.insert(
|
||||
SessionDatabase.TABLE_NAME, null,
|
||||
contentValuesOf(
|
||||
SessionDatabase.ACCOUNT_ID to account.toString(),
|
||||
SessionDatabase.ADDRESS to address.toString(),
|
||||
SessionDatabase.DEVICE to 1,
|
||||
SessionDatabase.RECORD to Util.getSecretBytes(32)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
data class Input(val e164: String?, val pni: PNI?, val aci: ACI?, val pniSession: Boolean = false, val aciSession: Boolean = false)
|
||||
data class Update(val e164: String?, val pni: PNI?, val aci: ACI?, val pniVerified: Boolean = false)
|
||||
data class Output(val e164: String?, val pni: PNI?, val aci: ACI?)
|
||||
data class PnpMatchResult(val ids: List<RecipientId>, val changeSet: PnpChangeSet) {
|
||||
val id
|
||||
get() = if (ids.size == 1) {
|
||||
ids[0]
|
||||
} else {
|
||||
throw IllegalStateException("There are multiple IDs, but you assumed 1!")
|
||||
}
|
||||
|
||||
val firstId
|
||||
get() = ids[0]
|
||||
|
||||
val secondId
|
||||
get() = ids[1]
|
||||
|
||||
val thirdId
|
||||
get() = ids[2]
|
||||
}
|
||||
|
||||
private fun applyAndAssert(input: Input, update: Update, output: Output): PnpMatchResult {
|
||||
return applyAndAssert(listOf(input), update, output)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method that will call insert your recipients, call [RecipientDatabase.processPnpTupleToChangeSet] with your params,
|
||||
* and then verify your output matches what you expect.
|
||||
*
|
||||
* It results the inserted ID's and changeset for additional verification.
|
||||
*
|
||||
* But basically this is here to make the tests more readable. It gives you a clear list of:
|
||||
* - input
|
||||
* - update
|
||||
* - output
|
||||
*
|
||||
* that you can spot check easily.
|
||||
*
|
||||
* Important: The output will only include records that contain fields from the input. That means
|
||||
* for:
|
||||
*
|
||||
* Input: E164_B, PNI_A, null
|
||||
* Update: E164_A, PNI_A, null
|
||||
*
|
||||
* You will get:
|
||||
* Output: E164_A, PNI_A, null
|
||||
*
|
||||
* Even though there was an update that will also result in the row (E164_B, null, null)
|
||||
*/
|
||||
private fun applyAndAssert(input: List<Input>, update: Update, output: Output): PnpMatchResult {
|
||||
val ids = input.map { insert(it.e164, it.pni, it.aci) }
|
||||
|
||||
input
|
||||
.filter { it.pniSession }
|
||||
.forEach { insertMockSessionFor(databaseRule.localAci, it.pni!!) }
|
||||
|
||||
input
|
||||
.filter { it.aciSession }
|
||||
.forEach { insertMockSessionFor(databaseRule.localAci, it.aci!!) }
|
||||
|
||||
val byE164 = update.e164?.let { db.getByE164(it).orElse(null) }
|
||||
val byPniSid = update.pni?.let { db.getByServiceId(it).orElse(null) }
|
||||
val byAciSid = update.aci?.let { db.getByServiceId(it).orElse(null) }
|
||||
|
||||
val data = PnpDataSet(
|
||||
e164 = update.e164,
|
||||
pni = update.pni,
|
||||
aci = update.aci,
|
||||
byE164 = byE164,
|
||||
byPniSid = byPniSid,
|
||||
byPniOnly = update.pni?.let { db.getByPni(it).orElse(null) },
|
||||
byAciSid = byAciSid,
|
||||
e164Record = byE164?.let { db.getRecord(it) },
|
||||
pniSidRecord = byPniSid?.let { db.getRecord(it) },
|
||||
aciSidRecord = byAciSid?.let { db.getRecord(it) }
|
||||
)
|
||||
val changeSet = db.processPnpTupleToChangeSet(update.e164, update.pni, update.aci, pniVerified = update.pniVerified)
|
||||
|
||||
val finalData = data.perform(changeSet.operations)
|
||||
|
||||
val finalRecords = setOfNotNull(finalData.e164Record, finalData.pniSidRecord, finalData.aciSidRecord)
|
||||
assertEquals("There's still multiple records in the resulting record set! $finalRecords", 1, finalRecords.size)
|
||||
|
||||
finalRecords.firstOrNull { record -> record.e164 == output.e164 && record.pni == output.pni && record.serviceId == (output.aci ?: output.pni) }
|
||||
?: throw AssertionError("Expected output was not found in the result set! Expected: $output")
|
||||
|
||||
return PnpMatchResult(
|
||||
ids = ids,
|
||||
changeSet = changeSet
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ACI_A = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
|
||||
val ACI_B = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
|
||||
|
||||
val PNI_A = PNI.from(UUID.fromString("154b8d92-c960-4f6c-8385-671ad2ffb999"))
|
||||
val PNI_B = PNI.from(UUID.fromString("ba92b1fb-cd55-40bf-adda-c35a85375533"))
|
||||
|
||||
const val E164_A = "+12221234567"
|
||||
const val E164_B = "+13331234567"
|
||||
}
|
||||
}
|
||||
@@ -122,62 +122,4 @@ class SQLiteDatabaseTest {
|
||||
assertTrue(hasRun1.get())
|
||||
assertFalse(hasRun2.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runPostSuccessfulTransaction_runsAndPerformsAnotherTransaction() {
|
||||
val hasRun = AtomicBoolean(false)
|
||||
|
||||
db.beginTransaction()
|
||||
|
||||
db.runPostSuccessfulTransaction {
|
||||
try {
|
||||
db.beginTransaction()
|
||||
hasRun.set(true)
|
||||
db.setTransactionSuccessful()
|
||||
} finally {
|
||||
db.endTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
assertFalse(hasRun.get())
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
|
||||
assertTrue(hasRun.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runPostSuccessfulTransaction_runsAndPerformsAnotherTransactionAndRunPostNested() {
|
||||
val hasRun1 = AtomicBoolean(false)
|
||||
val hasRun2 = AtomicBoolean(false)
|
||||
|
||||
db.beginTransaction()
|
||||
|
||||
db.runPostSuccessfulTransaction {
|
||||
db.beginTransaction()
|
||||
|
||||
db.runPostSuccessfulTransaction {
|
||||
assertTrue(hasRun1.get())
|
||||
assertFalse(hasRun2.get())
|
||||
hasRun2.set(true)
|
||||
}
|
||||
|
||||
assertFalse(hasRun1.get())
|
||||
hasRun1.set(true)
|
||||
assertFalse(hasRun2.get())
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
}
|
||||
|
||||
assertFalse(hasRun1.get())
|
||||
assertFalse(hasRun2.get())
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
|
||||
assertTrue(hasRun1.get())
|
||||
assertTrue(hasRun2.get())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.signal.core.util.CursorUtil
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
class ThreadDatabaseTest_pinned {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val databaseRule = SignalDatabaseRule()
|
||||
|
||||
private lateinit var recipient: Recipient
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAPinnedThread_whenIDeleteTheLastMessage_thenIDoNotDeleteOrUnpinTheThread() {
|
||||
// GIVEN
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
val messageId = MmsHelper.insert(recipient = recipient, threadId = threadId)
|
||||
SignalDatabase.threads.pinConversations(listOf(threadId))
|
||||
|
||||
// WHEN
|
||||
SignalDatabase.mms.deleteMessage(messageId)
|
||||
|
||||
// THEN
|
||||
val pinned = SignalDatabase.threads.pinnedThreadIds
|
||||
assertTrue(threadId in pinned)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAPinnedThread_whenIDeleteTheLastMessage_thenIExpectTheThreadInUnarchivedCount() {
|
||||
// GIVEN
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
val messageId = MmsHelper.insert(recipient = recipient, threadId = threadId)
|
||||
SignalDatabase.threads.pinConversations(listOf(threadId))
|
||||
|
||||
// WHEN
|
||||
SignalDatabase.mms.deleteMessage(messageId)
|
||||
|
||||
// THEN
|
||||
val unarchivedCount = SignalDatabase.threads.unarchivedConversationListCount
|
||||
assertEquals(1, unarchivedCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAPinnedThread_whenIDeleteTheLastMessage_thenIExpectPinnedThreadInUnarchivedList() {
|
||||
// GIVEN
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
val messageId = MmsHelper.insert(recipient = recipient, threadId = threadId)
|
||||
SignalDatabase.threads.pinConversations(listOf(threadId))
|
||||
|
||||
// WHEN
|
||||
SignalDatabase.mms.deleteMessage(messageId)
|
||||
|
||||
// THEN
|
||||
SignalDatabase.threads.getUnarchivedConversationList(true, 0, 1).use {
|
||||
it.moveToFirst()
|
||||
assertEquals(threadId, CursorUtil.requireLong(it, ThreadDatabase.ID))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import org.junit.rules.TestWatcher
|
||||
import org.junit.runner.Description
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Sets up bare-minimum to allow writing unit tests against the database,
|
||||
* including setting up the local ACI and PNI pair.
|
||||
*
|
||||
* @param deleteAllThreadsOnEachRun Run deleteAllThreads between each unit test
|
||||
*/
|
||||
class SignalDatabaseRule(
|
||||
private val deleteAllThreadsOnEachRun: Boolean = true
|
||||
) : TestWatcher() {
|
||||
|
||||
val localAci: ACI = ACI.from(UUID.randomUUID())
|
||||
val localPni: PNI = PNI.from(UUID.randomUUID())
|
||||
|
||||
override fun starting(description: Description?) {
|
||||
deleteAllThreads()
|
||||
|
||||
SignalStore.account().setAci(localAci)
|
||||
SignalStore.account().setPni(localPni)
|
||||
}
|
||||
|
||||
override fun finished(description: Description?) {
|
||||
deleteAllThreads()
|
||||
}
|
||||
|
||||
private fun deleteAllThreads() {
|
||||
if (deleteAllThreadsOnEachRun) {
|
||||
SignalDatabase.mms.deleteAllThreads()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -224,7 +224,7 @@
|
||||
<data android:scheme="sgnl"
|
||||
android:host="addstickers" />
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
@@ -258,7 +258,7 @@
|
||||
android:noHistory="true"
|
||||
android:theme="@style/Signal.Transparent">
|
||||
|
||||
<intent-filter android:autoVerify="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
@@ -275,7 +275,7 @@
|
||||
android:host="signal.group"/>
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:autoVerify="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
@@ -285,7 +285,7 @@
|
||||
android:host="signal.tube" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:autoVerify="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
@@ -46,8 +46,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
boolean hasWallpaper,
|
||||
boolean isMessageRequestAccepted,
|
||||
boolean canPlayInline,
|
||||
@NonNull Colorizer colorizer,
|
||||
boolean isCondensedMode);
|
||||
@NonNull Colorizer colorizer);
|
||||
|
||||
@NonNull ConversationMessage getConversationMessage();
|
||||
|
||||
@@ -62,13 +61,12 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
}
|
||||
|
||||
default void updateSelectedState() {
|
||||
// Intentionally Blank.
|
||||
// Intentionall Blank.
|
||||
}
|
||||
|
||||
interface EventListener {
|
||||
void onQuoteClicked(MmsMessageRecord messageRecord);
|
||||
void onLinkPreviewClicked(@NonNull LinkPreview linkPreview);
|
||||
void onQuotedIndicatorClicked(@NonNull MessageRecord messageRecord);
|
||||
void onMoreTextClicked(@NonNull RecipientId conversationRecipientId, long messageId, boolean isMms);
|
||||
void onStickerClicked(@NonNull StickerLocator stickerLocator);
|
||||
void onViewOnceMessageClicked(@NonNull MmsMessageRecord messageRecord);
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.thoughtcrime.securesms;
|
||||
|
||||
|
||||
import android.Manifest;
|
||||
import android.animation.LayoutTransition;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -31,6 +32,7 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
@@ -41,7 +43,6 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator;
|
||||
@@ -53,14 +54,15 @@ import androidx.transition.TransitionManager;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.material.chip.ChipGroup;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.pnikosis.materialishprogress.ProgressWheel;
|
||||
|
||||
import org.signal.core.util.concurrent.SimpleTask;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
|
||||
import org.thoughtcrime.securesms.components.recyclerview.ToolbarShadowAnimationHelper;
|
||||
import org.thoughtcrime.securesms.contacts.AbstractContactsCursorLoader;
|
||||
import org.thoughtcrime.securesms.contacts.ContactChipViewModel;
|
||||
import org.thoughtcrime.securesms.contacts.ContactChip;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
|
||||
@@ -68,25 +70,22 @@ import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.HeaderAction;
|
||||
import org.thoughtcrime.securesms.contacts.LetterHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContacts;
|
||||
import org.thoughtcrime.securesms.contacts.selection.ContactSelectionArguments;
|
||||
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.sharing.ShareContact;
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.adapter.FixedViewsAdapter;
|
||||
import org.thoughtcrime.securesms.util.adapter.RecyclerViewConcatenateAdapterStickyHeader;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModelList;
|
||||
import org.signal.core.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -96,9 +95,6 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import kotlin.Unit;
|
||||
|
||||
/**
|
||||
* Fragment for selecting a one or more contacts from a list.
|
||||
*
|
||||
@@ -110,7 +106,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = Log.tag(ContactSelectionListFragment.class);
|
||||
|
||||
private static final int CHIP_GROUP_EMPTY_CHILD_COUNT = 0;
|
||||
private static final int CHIP_GROUP_EMPTY_CHILD_COUNT = 1;
|
||||
private static final int CHIP_GROUP_REVEAL_DURATION_MS = 150;
|
||||
|
||||
public static final int NO_LIMIT = Integer.MAX_VALUE;
|
||||
@@ -138,13 +134,12 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
private RecyclerView recyclerView;
|
||||
private RecyclerViewFastScroller fastScroller;
|
||||
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
|
||||
private RecyclerView chipRecycler;
|
||||
private ChipGroup chipGroup;
|
||||
private HorizontalScrollView chipGroupScrollContainer;
|
||||
private OnSelectionLimitReachedListener onSelectionLimitReachedListener;
|
||||
private AbstractContactsCursorLoaderFactoryProvider cursorFactoryProvider;
|
||||
private MappingAdapter contactChipAdapter;
|
||||
private ContactChipViewModel contactChipViewModel;
|
||||
private LifecycleDisposable lifecycleDisposable;
|
||||
|
||||
private View shadowView;
|
||||
private ToolbarShadowAnimationHelper toolbarShadowAnimationHelper;
|
||||
private HeaderActionProvider headerActionProvider;
|
||||
private TextView headerActionView;
|
||||
|
||||
@@ -255,12 +250,17 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
showContactsButton = view.findViewById(R.id.show_contacts_button);
|
||||
showContactsDescription = view.findViewById(R.id.show_contacts_description);
|
||||
showContactsProgress = view.findViewById(R.id.progress);
|
||||
chipRecycler = view.findViewById(R.id.chipRecycler);
|
||||
chipGroup = view.findViewById(R.id.chipGroup);
|
||||
chipGroupScrollContainer = view.findViewById(R.id.chipGroupScrollContainer);
|
||||
constraintLayout = view.findViewById(R.id.container);
|
||||
shadowView = view.findViewById(R.id.toolbar_shadow);
|
||||
headerActionView = view.findViewById(R.id.header_action);
|
||||
|
||||
toolbarShadowAnimationHelper = new ToolbarShadowAnimationHelper(shadowView);
|
||||
|
||||
final LinearLayoutManager layoutManager = new LinearLayoutManager(requireContext());
|
||||
|
||||
recyclerView.addOnScrollListener(toolbarShadowAnimationHelper);
|
||||
recyclerView.setLayoutManager(layoutManager);
|
||||
recyclerView.setItemAnimator(new DefaultItemAnimator() {
|
||||
@Override
|
||||
@@ -269,18 +269,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
}
|
||||
});
|
||||
|
||||
contactChipViewModel = new ViewModelProvider(this).get(ContactChipViewModel.class);
|
||||
contactChipAdapter = new MappingAdapter();
|
||||
lifecycleDisposable = new LifecycleDisposable();
|
||||
|
||||
lifecycleDisposable.bindTo(getViewLifecycleOwner());
|
||||
SelectedContacts.register(contactChipAdapter, this::onChipCloseIconClicked);
|
||||
chipRecycler.setAdapter(contactChipAdapter);
|
||||
|
||||
Disposable disposable = contactChipViewModel.getState().subscribe(this::handleSelectedContactsChanged);
|
||||
|
||||
lifecycleDisposable.add(disposable);
|
||||
|
||||
Intent intent = requireActivity().getIntent();
|
||||
Bundle arguments = safeArguments();
|
||||
|
||||
@@ -403,8 +391,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
null,
|
||||
new ListClickListener(),
|
||||
isMulti,
|
||||
currentSelection,
|
||||
safeArguments().getInt(ContactSelectionArguments.CHECKBOX_RESOURCE, R.drawable.contact_selection_checkbox));
|
||||
currentSelection);
|
||||
|
||||
RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader();
|
||||
|
||||
@@ -747,22 +734,75 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
private void markContactUnselected(@NonNull SelectedContact selectedContact) {
|
||||
cursorRecyclerViewAdapter.removeFromSelectedContacts(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
contactChipViewModel.remove(selectedContact);
|
||||
removeChipForContact(selectedContact);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onSelectionChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSelectedContactsChanged(@NonNull List<SelectedContacts.Model> selectedContacts) {
|
||||
contactChipAdapter.submitList(new MappingModelList(selectedContacts), this::smoothScrollChipsToEnd);
|
||||
private void removeChipForContact(@NonNull SelectedContact contact) {
|
||||
for (int i = chipGroup.getChildCount() - 1; i >= 0; i--) {
|
||||
View v = chipGroup.getChildAt(i);
|
||||
if (v instanceof ContactChip && contact.matches(((ContactChip) v).getContact())) {
|
||||
chipGroup.removeView(v);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedContacts.isEmpty()) {
|
||||
if (getChipCount() == 0) {
|
||||
setChipGroupVisibility(ConstraintSet.GONE);
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
private void addChipForSelectedContact(@NonNull SelectedContact selectedContact) {
|
||||
SimpleTask.run(getViewLifecycleOwner().getLifecycle(),
|
||||
() -> Recipient.resolved(selectedContact.getOrCreateRecipientId(requireContext())),
|
||||
resolved -> addChipForRecipient(resolved, selectedContact));
|
||||
}
|
||||
|
||||
private void addChipForRecipient(@NonNull Recipient recipient, @NonNull SelectedContact selectedContact) {
|
||||
final ContactChip chip = new ContactChip(requireContext());
|
||||
|
||||
if (getChipCount() == 0) {
|
||||
setChipGroupVisibility(ConstraintSet.VISIBLE);
|
||||
}
|
||||
|
||||
chip.setText(recipient.getShortDisplayName(requireContext()));
|
||||
chip.setContact(selectedContact);
|
||||
chip.setCloseIconVisible(true);
|
||||
chip.setOnCloseIconClickListener(view -> {
|
||||
markContactUnselected(selectedContact);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactDeselected(Optional.of(recipient.getId()), recipient.getE164().orElse(null));
|
||||
}
|
||||
});
|
||||
|
||||
chipGroup.getLayoutTransition().addTransitionListener(new LayoutTransition.TransitionListener() {
|
||||
@Override
|
||||
public void startTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
|
||||
if (getView() == null || !requireView().isAttachedToWindow()) {
|
||||
Log.w(TAG, "Fragment's view was detached before the animation completed.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (view == chip && transitionType == LayoutTransition.APPEARING) {
|
||||
chipGroup.getLayoutTransition().removeTransitionListener(this);
|
||||
registerChipRecipientObserver(chip, recipient.live());
|
||||
chipGroup.post(ContactSelectionListFragment.this::smoothScrollChipsToEnd);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
chip.setAvatar(glideRequests, recipient, () -> addChip(chip));
|
||||
}
|
||||
|
||||
private void addChip(@NonNull ContactChip chip) {
|
||||
chipGroup.addView(chip);
|
||||
if (selectionWarningLimitReachedExactly()) {
|
||||
if (onSelectionLimitReachedListener != null) {
|
||||
onSelectionLimitReachedListener.onSuggestedLimitReached(selectionLimit.getRecommendedLimit());
|
||||
@@ -772,27 +812,23 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
}
|
||||
}
|
||||
|
||||
private void addChipForSelectedContact(@NonNull SelectedContact selectedContact) {
|
||||
SimpleTask.run(getViewLifecycleOwner().getLifecycle(),
|
||||
() -> Recipient.resolved(selectedContact.getOrCreateRecipientId(requireContext())),
|
||||
resolved -> contactChipViewModel.add(selectedContact));
|
||||
}
|
||||
|
||||
private Unit onChipCloseIconClicked(SelectedContacts.Model model) {
|
||||
markContactUnselected(model.getSelectedContact());
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactDeselected(Optional.of(model.getRecipient().getId()), model.getRecipient().getE164().orElse(null));
|
||||
}
|
||||
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
|
||||
private int getChipCount() {
|
||||
int count = contactChipViewModel.getCount() - CHIP_GROUP_EMPTY_CHILD_COUNT;
|
||||
int count = chipGroup.getChildCount() - CHIP_GROUP_EMPTY_CHILD_COUNT;
|
||||
if (count < 0) throw new AssertionError();
|
||||
return count;
|
||||
}
|
||||
|
||||
private void registerChipRecipientObserver(@NonNull ContactChip chip, @Nullable LiveRecipient recipient) {
|
||||
if (recipient != null) {
|
||||
recipient.observe(getViewLifecycleOwner(), resolved -> {
|
||||
if (chip.isAttachedToWindow()) {
|
||||
chip.setAvatar(glideRequests, resolved, null);
|
||||
chip.setText(resolved.getShortDisplayName(chip.getContext()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void setChipGroupVisibility(int visibility) {
|
||||
if (!safeArguments().getBoolean(DISPLAY_CHIPS, requireActivity().getIntent().getBooleanExtra(DISPLAY_CHIPS, true))) {
|
||||
return;
|
||||
@@ -806,7 +842,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
|
||||
ConstraintSet constraintSet = new ConstraintSet();
|
||||
constraintSet.clone(constraintLayout);
|
||||
constraintSet.setVisibility(R.id.chipRecycler, visibility);
|
||||
constraintSet.setVisibility(R.id.chipGroupScrollContainer, visibility);
|
||||
constraintSet.applyTo(constraintLayout);
|
||||
}
|
||||
|
||||
@@ -815,8 +851,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
}
|
||||
|
||||
private void smoothScrollChipsToEnd() {
|
||||
int x = ViewUtil.isLtr(chipRecycler) ? chipRecycler.getWidth() : 0;
|
||||
chipRecycler.smoothScrollBy(x, 0);
|
||||
int x = ViewUtil.isLtr(chipGroupScrollContainer) ? chipGroup.getWidth() : 0;
|
||||
chipGroupScrollContainer.smoothScrollTo(x, 0);
|
||||
}
|
||||
|
||||
public interface OnContactSelectedListener {
|
||||
|
||||
@@ -21,7 +21,7 @@ import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.melnykov.fab.FloatingActionButton;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.loaders.DeviceListLoader;
|
||||
|
||||
@@ -125,10 +125,10 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
private void updateTabVisibility() {
|
||||
if (Stories.isFeatureEnabled()) {
|
||||
findViewById(R.id.conversation_list_tabs).setVisibility(View.VISIBLE);
|
||||
WindowUtil.setNavigationBarColor(this, ContextCompat.getColor(this, R.color.signal_colorSurface2));
|
||||
WindowUtil.setNavigationBarColor(getWindow(), ContextCompat.getColor(this, R.color.signal_colorSecondaryContainer));
|
||||
} else {
|
||||
findViewById(R.id.conversation_list_tabs).setVisibility(View.GONE);
|
||||
WindowUtil.setNavigationBarColor(this, ContextCompat.getColor(this, R.color.signal_colorBackground));
|
||||
WindowUtil.setNavigationBarColor(getWindow(), ContextCompat.getColor(this, R.color.signal_background_primary));
|
||||
conversationListTabsViewModel.onChatsSelected();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.util.CharacterCalculator;
|
||||
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
|
||||
public class TransportOption implements Parcelable {
|
||||
|
||||
public enum Type {
|
||||
SMS,
|
||||
TEXTSECURE
|
||||
}
|
||||
|
||||
private final int drawable;
|
||||
private final int backgroundColor;
|
||||
private final @NonNull String text;
|
||||
private final @NonNull Type type;
|
||||
private final @NonNull String composeHint;
|
||||
private final @NonNull CharacterCalculator characterCalculator;
|
||||
private final @NonNull Optional<CharSequence> simName;
|
||||
private final @NonNull Optional<Integer> simSubscriptionId;
|
||||
|
||||
public TransportOption(@NonNull Type type,
|
||||
@DrawableRes int drawable,
|
||||
int backgroundColor,
|
||||
@NonNull String text,
|
||||
@NonNull String composeHint,
|
||||
@NonNull CharacterCalculator characterCalculator)
|
||||
{
|
||||
this(type, drawable, backgroundColor, text, composeHint, characterCalculator,
|
||||
Optional.empty(), Optional.empty());
|
||||
}
|
||||
|
||||
public TransportOption(@NonNull Type type,
|
||||
@DrawableRes int drawable,
|
||||
int backgroundColor,
|
||||
@NonNull String text,
|
||||
@NonNull String composeHint,
|
||||
@NonNull CharacterCalculator characterCalculator,
|
||||
@NonNull Optional<CharSequence> simName,
|
||||
@NonNull Optional<Integer> simSubscriptionId)
|
||||
{
|
||||
this.type = type;
|
||||
this.drawable = drawable;
|
||||
this.backgroundColor = backgroundColor;
|
||||
this.text = text;
|
||||
this.composeHint = composeHint;
|
||||
this.characterCalculator = characterCalculator;
|
||||
this.simName = simName;
|
||||
this.simSubscriptionId = simSubscriptionId;
|
||||
}
|
||||
|
||||
TransportOption(Parcel in) {
|
||||
this(Type.valueOf(in.readString()),
|
||||
in.readInt(),
|
||||
in.readInt(),
|
||||
in.readString(),
|
||||
in.readString(),
|
||||
CharacterCalculator.readFromParcel(in),
|
||||
Optional.ofNullable(TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in)),
|
||||
in.readInt() == 1 ? Optional.of(in.readInt()) : Optional.empty());
|
||||
}
|
||||
|
||||
public @NonNull Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public boolean isType(Type type) {
|
||||
return this.type == type;
|
||||
}
|
||||
|
||||
public boolean isSms() {
|
||||
return type == Type.SMS;
|
||||
}
|
||||
|
||||
public CharacterState calculateCharacters(String messageBody) {
|
||||
return characterCalculator.calculateCharacters(messageBody);
|
||||
}
|
||||
|
||||
public @DrawableRes int getDrawable() {
|
||||
return drawable;
|
||||
}
|
||||
|
||||
public int getBackgroundColor() {
|
||||
return backgroundColor;
|
||||
}
|
||||
|
||||
public @NonNull String getComposeHint() {
|
||||
return composeHint;
|
||||
}
|
||||
|
||||
public @NonNull String getDescription() {
|
||||
return text;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Optional<CharSequence> getSimName() {
|
||||
return simName;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Optional<Integer> getSimSubscriptionId() {
|
||||
return simSubscriptionId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(type.name());
|
||||
dest.writeInt(drawable);
|
||||
dest.writeInt(backgroundColor);
|
||||
dest.writeString(text);
|
||||
dest.writeString(composeHint);
|
||||
CharacterCalculator.writeToParcel(dest, characterCalculator);
|
||||
TextUtils.writeToParcel(simName.orElse(null), dest, flags);
|
||||
|
||||
if (simSubscriptionId.isPresent()) {
|
||||
dest.writeInt(1);
|
||||
dest.writeInt(simSubscriptionId.get());
|
||||
} else {
|
||||
dest.writeInt(0);
|
||||
}
|
||||
}
|
||||
|
||||
public static final Creator<TransportOption> CREATOR = new Creator<TransportOption>() {
|
||||
@Override
|
||||
public TransportOption createFromParcel(Parcel in) {
|
||||
return new TransportOption(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransportOption[] newArray(int size) {
|
||||
return new TransportOption[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.util.CharacterCalculator;
|
||||
import org.thoughtcrime.securesms.util.MmsCharacterCalculator;
|
||||
import org.thoughtcrime.securesms.util.PushCharacterCalculator;
|
||||
import org.thoughtcrime.securesms.util.SmsCharacterCalculator;
|
||||
import org.thoughtcrime.securesms.util.dualsim.SubscriptionInfoCompat;
|
||||
import org.thoughtcrime.securesms.util.dualsim.SubscriptionManagerCompat;
|
||||
import org.whispersystems.signalservice.api.util.OptionalUtil;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.thoughtcrime.securesms.TransportOption.Type;
|
||||
|
||||
public class TransportOptions {
|
||||
|
||||
private static final String TAG = Log.tag(TransportOptions.class);
|
||||
|
||||
private final List<OnTransportChangedListener> listeners = new LinkedList<>();
|
||||
private final Context context;
|
||||
private final List<TransportOption> enabledTransports;
|
||||
|
||||
private Type defaultTransportType = Type.SMS;
|
||||
private Optional<Integer> defaultSubscriptionId = Optional.empty();
|
||||
private Optional<TransportOption> selectedOption = Optional.empty();
|
||||
|
||||
private final Optional<Integer> systemSubscriptionId;
|
||||
|
||||
public TransportOptions(Context context, boolean media) {
|
||||
this.context = context;
|
||||
this.enabledTransports = initializeAvailableTransports(media);
|
||||
this.systemSubscriptionId = new SubscriptionManagerCompat(context).getPreferredSubscriptionId();
|
||||
}
|
||||
|
||||
public void reset(boolean media) {
|
||||
List<TransportOption> transportOptions = initializeAvailableTransports(media);
|
||||
|
||||
this.enabledTransports.clear();
|
||||
this.enabledTransports.addAll(transportOptions);
|
||||
|
||||
if (selectedOption.isPresent() && !isEnabled(selectedOption.get())) {
|
||||
setSelectedTransport(null);
|
||||
} else {
|
||||
this.defaultTransportType = Type.SMS;
|
||||
this.defaultSubscriptionId = Optional.empty();
|
||||
|
||||
notifyTransportChangeListeners();
|
||||
}
|
||||
}
|
||||
|
||||
public void setDefaultTransport(Type type) {
|
||||
this.defaultTransportType = type;
|
||||
|
||||
if (!selectedOption.isPresent()) {
|
||||
notifyTransportChangeListeners();
|
||||
}
|
||||
}
|
||||
|
||||
public void setDefaultSubscriptionId(Optional<Integer> subscriptionId) {
|
||||
if (defaultSubscriptionId.equals(subscriptionId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.defaultSubscriptionId = subscriptionId;
|
||||
|
||||
if (!selectedOption.isPresent()) {
|
||||
notifyTransportChangeListeners();
|
||||
}
|
||||
}
|
||||
|
||||
public void setSelectedTransport(@Nullable TransportOption transportOption) {
|
||||
this.selectedOption = Optional.ofNullable(transportOption);
|
||||
notifyTransportChangeListeners();
|
||||
}
|
||||
|
||||
public boolean isManualSelection() {
|
||||
return this.selectedOption.isPresent();
|
||||
}
|
||||
|
||||
public @NonNull TransportOption getSelectedTransport() {
|
||||
if (selectedOption.isPresent()) return selectedOption.get();
|
||||
|
||||
if (defaultTransportType == Type.SMS) {
|
||||
TransportOption transportOption = findEnabledSmsTransportOption(OptionalUtil.or(defaultSubscriptionId, systemSubscriptionId));
|
||||
if (transportOption != null) {
|
||||
return transportOption;
|
||||
}
|
||||
}
|
||||
|
||||
for (TransportOption transportOption : enabledTransports) {
|
||||
if (transportOption.getType() == defaultTransportType) {
|
||||
return transportOption;
|
||||
}
|
||||
}
|
||||
|
||||
throw new AssertionError("No options of default type!");
|
||||
}
|
||||
|
||||
public static @NonNull TransportOption getPushTransportOption(@NonNull Context context) {
|
||||
return new TransportOption(Type.TEXTSECURE,
|
||||
R.drawable.ic_send_lock_24,
|
||||
context.getResources().getColor(R.color.core_ultramarine),
|
||||
context.getString(R.string.ConversationActivity_transport_signal),
|
||||
context.getString(R.string.conversation_activity__type_message_push),
|
||||
new PushCharacterCalculator());
|
||||
|
||||
}
|
||||
|
||||
private @Nullable TransportOption findEnabledSmsTransportOption(Optional<Integer> subscriptionId) {
|
||||
if (subscriptionId.isPresent()) {
|
||||
final int subId = subscriptionId.get();
|
||||
|
||||
for (TransportOption transportOption : enabledTransports) {
|
||||
if (transportOption.getType() == Type.SMS &&
|
||||
subId == transportOption.getSimSubscriptionId().orElse(-1)) {
|
||||
return transportOption;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void disableTransport(Type type) {
|
||||
TransportOption selected = selectedOption.orElse(null);
|
||||
|
||||
Iterator<TransportOption> iterator = enabledTransports.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
TransportOption option = iterator.next();
|
||||
|
||||
if (option.isType(type)) {
|
||||
if (selected == option) {
|
||||
setSelectedTransport(null);
|
||||
}
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<TransportOption> getEnabledTransports() {
|
||||
return enabledTransports;
|
||||
}
|
||||
|
||||
public void addOnTransportChangedListener(OnTransportChangedListener listener) {
|
||||
this.listeners.add(listener);
|
||||
}
|
||||
|
||||
private List<TransportOption> initializeAvailableTransports(boolean isMediaMessage) {
|
||||
List<TransportOption> results = new LinkedList<>();
|
||||
|
||||
if (isMediaMessage) {
|
||||
results.addAll(getTransportOptionsForSimCards(context.getString(R.string.ConversationActivity_transport_insecure_mms),
|
||||
context.getString(R.string.conversation_activity__type_message_mms_insecure),
|
||||
new MmsCharacterCalculator()));
|
||||
} else {
|
||||
results.addAll(getTransportOptionsForSimCards(context.getString(R.string.ConversationActivity_transport_insecure_sms),
|
||||
context.getString(R.string.conversation_activity__type_message_sms_insecure),
|
||||
new SmsCharacterCalculator()));
|
||||
}
|
||||
|
||||
results.add(getPushTransportOption(context));
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private @NonNull List<TransportOption> getTransportOptionsForSimCards(@NonNull String text,
|
||||
@NonNull String composeHint,
|
||||
@NonNull CharacterCalculator characterCalculator)
|
||||
{
|
||||
List<TransportOption> results = new LinkedList<>();
|
||||
SubscriptionManagerCompat subscriptionManager = new SubscriptionManagerCompat(context);
|
||||
Collection<SubscriptionInfoCompat> subscriptions;
|
||||
|
||||
if (Permissions.hasAll(context, Manifest.permission.READ_PHONE_STATE)) {
|
||||
subscriptions = subscriptionManager.getActiveAndReadySubscriptionInfos();
|
||||
} else {
|
||||
subscriptions = Collections.emptyList();
|
||||
}
|
||||
|
||||
if (subscriptions.size() < 2) {
|
||||
results.add(new TransportOption(Type.SMS, R.drawable.ic_send_unlock_24,
|
||||
context.getResources().getColor(R.color.core_grey_50),
|
||||
text, composeHint, characterCalculator));
|
||||
} else {
|
||||
for (SubscriptionInfoCompat subscriptionInfo : subscriptions) {
|
||||
results.add(new TransportOption(Type.SMS, R.drawable.ic_send_unlock_24,
|
||||
context.getResources().getColor(R.color.core_grey_50),
|
||||
text, composeHint, characterCalculator,
|
||||
Optional.of(subscriptionInfo.getDisplayName()),
|
||||
Optional.of(subscriptionInfo.getSubscriptionId())));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private void notifyTransportChangeListeners() {
|
||||
for (OnTransportChangedListener listener : listeners) {
|
||||
listener.onChange(getSelectedTransport(), selectedOption.isPresent());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isEnabled(TransportOption transportOption) {
|
||||
for (TransportOption option : enabledTransports) {
|
||||
if (option.equals(transportOption)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public interface OnTransportChangedListener {
|
||||
public void onChange(TransportOption newTransport, boolean manuallySelected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.PorterDuff.Mode;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TransportOptionsAdapter extends BaseAdapter {
|
||||
|
||||
private final LayoutInflater inflater;
|
||||
|
||||
private List<TransportOption> enabledTransports;
|
||||
|
||||
public TransportOptionsAdapter(@NonNull Context context,
|
||||
@NonNull List<TransportOption> enabledTransports)
|
||||
{
|
||||
super();
|
||||
this.inflater = LayoutInflater.from(context);
|
||||
this.enabledTransports = enabledTransports;
|
||||
}
|
||||
|
||||
public void setEnabledTransports(List<TransportOption> enabledTransports) {
|
||||
this.enabledTransports = enabledTransports;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return enabledTransports.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return enabledTransports.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
if (convertView == null) {
|
||||
convertView = inflater.inflate(R.layout.transport_selection_list_item, parent, false);
|
||||
}
|
||||
|
||||
TransportOption transport = (TransportOption) getItem(position);
|
||||
ImageView imageView = convertView.findViewById(R.id.icon);
|
||||
TextView textView = convertView.findViewById(R.id.text);
|
||||
TextView subtextView = convertView.findViewById(R.id.subtext);
|
||||
|
||||
imageView.getBackground().setColorFilter(transport.getBackgroundColor(), Mode.MULTIPLY);
|
||||
imageView.setImageResource(transport.getDrawable());
|
||||
textView.setText(transport.getDescription());
|
||||
|
||||
if (transport.getSimName().isPresent()) {
|
||||
subtextView.setText(transport.getSimName().get());
|
||||
subtextView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
subtextView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
return convertView;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ListView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.ListPopupWindow;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class TransportOptionsPopup extends ListPopupWindow implements ListView.OnItemClickListener {
|
||||
|
||||
private final TransportOptionsAdapter adapter;
|
||||
private final SelectedListener listener;
|
||||
|
||||
public TransportOptionsPopup(@NonNull Context context, @NonNull View anchor, @NonNull SelectedListener listener) {
|
||||
super(context);
|
||||
this.listener = listener;
|
||||
this.adapter = new TransportOptionsAdapter(context, new LinkedList<TransportOption>());
|
||||
|
||||
setVerticalOffset(context.getResources().getDimensionPixelOffset(R.dimen.transport_selection_popup_yoff));
|
||||
setHorizontalOffset(context.getResources().getDimensionPixelOffset(R.dimen.transport_selection_popup_xoff));
|
||||
setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
|
||||
setModal(true);
|
||||
setAnchorView(anchor);
|
||||
setAdapter(adapter);
|
||||
setContentWidth(context.getResources().getDimensionPixelSize(R.dimen.transport_selection_popup_width));
|
||||
|
||||
setOnItemClickListener(this);
|
||||
}
|
||||
|
||||
public void display(List<TransportOption> enabledTransports) {
|
||||
adapter.setEnabledTransports(enabledTransports);
|
||||
adapter.notifyDataSetChanged();
|
||||
show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
listener.onSelected((TransportOption)adapter.getItem(position));
|
||||
}
|
||||
|
||||
public interface SelectedListener {
|
||||
void onSelected(TransportOption option);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -81,7 +81,14 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
}
|
||||
|
||||
clearButton.visible = state.canClear
|
||||
saveButton.isClickable = state.canSave
|
||||
|
||||
val wasEnabled = saveButton.isEnabled
|
||||
saveButton.isEnabled = state.canSave
|
||||
if (wasEnabled != state.canSave) {
|
||||
val alpha = if (state.canSave) 1f else 0.5f
|
||||
saveButton.animate().cancel()
|
||||
saveButton.animate().alpha(alpha)
|
||||
}
|
||||
|
||||
val items = state.selectableAvatars.map { AvatarPickerItem.Model(it, it == state.currentAvatar) }
|
||||
val selectedPosition = items.indexOfFirst { it.isSelected }
|
||||
@@ -97,11 +104,6 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
photoButton.setOnIconClickedListener { openGallery() }
|
||||
textButton.setOnIconClickedListener { openTextEditor(null) }
|
||||
saveButton.setOnClickListener { v ->
|
||||
if (!saveButton.isEnabled) {
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
saveButton.isEnabled = false
|
||||
viewModel.save(
|
||||
{
|
||||
setFragmentResult(
|
||||
|
||||
@@ -159,12 +159,12 @@ public class BackupDialog {
|
||||
public static void showVerifyBackupPassphraseDialog(@NonNull Context context) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.enter_backup_passphrase_dialog, null);
|
||||
EditText prompt = view.findViewById(R.id.restore_passphrase_input);
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.BackupDialog_enter_backup_passphrase_to_verify)
|
||||
.setView(view)
|
||||
.setPositiveButton(R.string.BackupDialog_verify, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.BackupDialog_enter_backup_passphrase_to_verify)
|
||||
.setView(view)
|
||||
.setPositiveButton(R.string.BackupDialog_verify, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
|
||||
Button positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
positiveButton.setEnabled(false);
|
||||
|
||||
@@ -31,7 +31,7 @@ object ExpiredGiftSheetConfiguration {
|
||||
textPref(
|
||||
title = DSLSettingsText.from(
|
||||
stringId = R.string.ExpiredGiftSheetConfiguration__your_gift_badge_has_expired,
|
||||
DSLSettingsText.CenterModifier, DSLSettingsText.TitleLargeModifier
|
||||
DSLSettingsText.CenterModifier, DSLSettingsText.Title2BoldModifier
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -109,10 +109,6 @@ class GiftFlowConfirmationFragment :
|
||||
textInputViewHolder.onAttachedToWindow()
|
||||
|
||||
inputAwareLayout.addOnKeyboardShownListener {
|
||||
if (emojiKeyboard.isEmojiSearchMode) {
|
||||
return@addOnKeyboardShownListener
|
||||
}
|
||||
|
||||
inputAwareLayout.hideAttachedInput(true)
|
||||
emojiToggle.setImageResource(R.drawable.ic_emoji_smiley_24)
|
||||
}
|
||||
@@ -131,25 +127,6 @@ class GiftFlowConfirmationFragment :
|
||||
}
|
||||
)
|
||||
|
||||
textInputViewHolder.bind(
|
||||
TextInput.MultilineModel(
|
||||
text = viewModel.snapshot.additionalMessage,
|
||||
hint = DSLSettingsText.from(R.string.GiftFlowConfirmationFragment__add_a_message),
|
||||
onTextChanged = {
|
||||
viewModel.setAdditionalMessage(it)
|
||||
},
|
||||
onEmojiToggleClicked = {
|
||||
if (inputAwareLayout.isKeyboardOpen || (!inputAwareLayout.isKeyboardOpen && !inputAwareLayout.isInputOpen)) {
|
||||
inputAwareLayout.show(it, emojiKeyboard)
|
||||
emojiToggle.setImageResource(R.drawable.ic_keyboard_24)
|
||||
} else {
|
||||
inputAwareLayout.showSoftkey(it)
|
||||
emojiToggle.setImageResource(R.drawable.ic_emoji_smiley_24)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
lifecycleDisposable += viewModel.state.observeOn(AndroidSchedulers.mainThread()).subscribe { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
|
||||
@@ -165,6 +142,25 @@ class GiftFlowConfirmationFragment :
|
||||
} else {
|
||||
processingDonationPaymentDialog.dismiss()
|
||||
}
|
||||
|
||||
textInputViewHolder.bind(
|
||||
TextInput.MultilineModel(
|
||||
text = state.additionalMessage,
|
||||
hint = DSLSettingsText.from(R.string.GiftFlowConfirmationFragment__add_a_message),
|
||||
onTextChanged = {
|
||||
viewModel.setAdditionalMessage(it)
|
||||
},
|
||||
onEmojiToggleClicked = {
|
||||
if (inputAwareLayout.isKeyboardOpen || (!inputAwareLayout.isKeyboardOpen && !inputAwareLayout.isInputOpen)) {
|
||||
inputAwareLayout.show(it, emojiKeyboard)
|
||||
emojiToggle.setImageResource(R.drawable.ic_keyboard_24)
|
||||
} else {
|
||||
inputAwareLayout.showSoftkey(it)
|
||||
emojiToggle.setImageResource(R.drawable.ic_emoji_smiley_24)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
||||
@@ -253,10 +249,6 @@ class GiftFlowConfirmationFragment :
|
||||
)
|
||||
}
|
||||
|
||||
override fun onToolbarNavigationClicked() {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
|
||||
override fun openEmojiSearch() {
|
||||
emojiKeyboard.onOpenEmojiSearch()
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.models.Ne
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadingCircle
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
@@ -51,11 +50,6 @@ class GiftFlowStartFragment : DSLSettingsFragment(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
ViewUtil.hideKeyboard(requireContext(), requireView())
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: GiftFlowState): DSLConfiguration {
|
||||
return configure {
|
||||
customPref(
|
||||
|
||||
@@ -58,7 +58,7 @@ class GiftThanksSheet : DSLSettingsBottomSheetFragment() {
|
||||
private fun getConfiguration(recipient: Recipient): DSLConfiguration {
|
||||
return configure {
|
||||
textPref(
|
||||
title = DSLSettingsText.from(R.string.SubscribeThanksForYourSupportBottomSheetDialogFragment__thanks_for_your_support, DSLSettingsText.TitleLargeModifier, DSLSettingsText.CenterModifier)
|
||||
title = DSLSettingsText.from(R.string.SubscribeThanksForYourSupportBottomSheetDialogFragment__thanks_for_your_support, DSLSettingsText.Title2BoldModifier, DSLSettingsText.CenterModifier)
|
||||
)
|
||||
|
||||
noPadTextPref(
|
||||
|
||||
@@ -158,7 +158,7 @@ class ViewReceivedGiftBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
charSequence = requireContext().getString(R.string.ViewReceivedGiftBottomSheet__s_sent_you_a_gift, state.recipient.getShortDisplayName(requireContext())),
|
||||
DSLSettingsText.CenterModifier, DSLSettingsText.TitleLargeModifier
|
||||
DSLSettingsText.CenterModifier, DSLSettingsText.Title2BoldModifier
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ class ViewSentGiftBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
stringId = R.string.ViewSentGiftBottomSheet__thanks_for_your_support,
|
||||
DSLSettingsText.CenterModifier, DSLSettingsText.TitleLargeModifier
|
||||
DSLSettingsText.CenterModifier, DSLSettingsText.Title2BoldModifier
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.badges.self.featured
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -12,12 +11,13 @@ import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.Badges.displayBadges
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.models.BadgePreview
|
||||
import org.thoughtcrime.securesms.components.recyclerview.OnScrollAnimationHelper
|
||||
import org.thoughtcrime.securesms.components.recyclerview.ToolbarShadowAnimationHelper
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
|
||||
|
||||
/**
|
||||
* Fragment which allows user to select one of their badges to be their "Featured" badge.
|
||||
@@ -46,8 +46,8 @@ class SelectFeaturedBadgeFragment : DSLSettingsFragment(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMaterial3OnScrollHelper(toolbar: Toolbar?): Material3OnScrollHelper? {
|
||||
return Material3OnScrollHelper(requireActivity(), scrollShadow)
|
||||
override fun getOnScrollAnimationHelper(toolbarShadow: View): OnScrollAnimationHelper {
|
||||
return ToolbarShadowAnimationHelper(scrollShadow)
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms.badges.self.none
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.viewModels
|
||||
import org.signal.core.util.DimensionUnit
|
||||
@@ -41,7 +40,7 @@ class BecomeASustainerFragment : DSLSettingsBottomSheetFragment() {
|
||||
title = DSLSettingsText.from(
|
||||
R.string.BecomeASustainerFragment__get_badges,
|
||||
DSLSettingsText.CenterModifier,
|
||||
DSLSettingsText.TitleLargeModifier
|
||||
DSLSettingsText.Title2BoldModifier
|
||||
)
|
||||
)
|
||||
|
||||
@@ -50,15 +49,13 @@ class BecomeASustainerFragment : DSLSettingsBottomSheetFragment() {
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
R.string.BecomeASustainerFragment__signal_is_a_non_profit,
|
||||
DSLSettingsText.CenterModifier,
|
||||
DSLSettingsText.TextAppearanceModifier(R.style.Signal_Text_BodyMedium),
|
||||
DSLSettingsText.ColorModifier(ContextCompat.getColor(requireContext(), R.color.signal_colorOnSurfaceVariant))
|
||||
DSLSettingsText.CenterModifier
|
||||
)
|
||||
)
|
||||
|
||||
space(DimensionUnit.DP.toPixels(32f).toInt())
|
||||
space(DimensionUnit.DP.toPixels(77f).toInt())
|
||||
|
||||
tonalButton(
|
||||
primaryButton(
|
||||
text = DSLSettingsText.from(
|
||||
R.string.BecomeASustainerMegaphone__become_a_sustainer
|
||||
),
|
||||
@@ -68,7 +65,7 @@ class BecomeASustainerFragment : DSLSettingsBottomSheetFragment() {
|
||||
}
|
||||
)
|
||||
|
||||
space(DimensionUnit.DP.toPixels(32f).toInt())
|
||||
space(DimensionUnit.DP.toPixels(8f).toInt())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -171,6 +171,6 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
|
||||
throw new IllegalArgumentException("Unsupported event type " + event);
|
||||
}
|
||||
|
||||
Snackbar.make(view, getString(messageResId, displayName), Snackbar.LENGTH_SHORT).show();
|
||||
Snackbar.make(view, getString(messageResId, displayName), Snackbar.LENGTH_SHORT).setTextColor(Color.WHITE).show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,8 +87,6 @@ public final class AudioView extends FrameLayout {
|
||||
|
||||
public AudioView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
setLayoutDirection(LAYOUT_DIRECTION_LTR);
|
||||
|
||||
TypedArray typedArray = null;
|
||||
try {
|
||||
typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.AudioView, 0, 0);
|
||||
|
||||
@@ -3,6 +3,9 @@ package org.thoughtcrime.securesms.components;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
@@ -39,6 +42,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||
import org.thoughtcrime.securesms.util.BlurTransformation;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
@@ -54,8 +58,24 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = Log.tag(AvatarImageView.class);
|
||||
|
||||
private static final Paint LIGHT_THEME_OUTLINE_PAINT = new Paint();
|
||||
private static final Paint DARK_THEME_OUTLINE_PAINT = new Paint();
|
||||
|
||||
static {
|
||||
LIGHT_THEME_OUTLINE_PAINT.setColor(Color.argb((int) (255 * 0.2), 0, 0, 0));
|
||||
LIGHT_THEME_OUTLINE_PAINT.setStyle(Paint.Style.STROKE);
|
||||
LIGHT_THEME_OUTLINE_PAINT.setStrokeWidth(1);
|
||||
LIGHT_THEME_OUTLINE_PAINT.setAntiAlias(true);
|
||||
|
||||
DARK_THEME_OUTLINE_PAINT.setColor(Color.argb((int) (255 * 0.2), 255, 255, 255));
|
||||
DARK_THEME_OUTLINE_PAINT.setStyle(Paint.Style.STROKE);
|
||||
DARK_THEME_OUTLINE_PAINT.setStrokeWidth(1);
|
||||
DARK_THEME_OUTLINE_PAINT.setAntiAlias(true);
|
||||
}
|
||||
|
||||
private int size;
|
||||
private boolean inverted;
|
||||
private Paint outlinePaint;
|
||||
private OnClickListener listener;
|
||||
private Recipient.FallbackPhotoProvider fallbackPhotoProvider;
|
||||
private boolean blurred;
|
||||
@@ -85,6 +105,8 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
typedArray.recycle();
|
||||
}
|
||||
|
||||
outlinePaint = ThemeUtil.isDarkTheme(context) ? DARK_THEME_OUTLINE_PAINT : LIGHT_THEME_OUTLINE_PAINT;
|
||||
|
||||
unknownRecipientDrawable = new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20).asDrawable(context, AvatarColor.UNKNOWN, inverted);
|
||||
blurred = false;
|
||||
chatColors = null;
|
||||
@@ -95,6 +117,20 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
super.setClipBounds(clipBounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
float width = getWidth() - getPaddingRight() - getPaddingLeft();
|
||||
float height = getHeight() - getPaddingBottom() - getPaddingTop();
|
||||
float cx = width / 2f;
|
||||
float cy = height / 2f;
|
||||
float radius = Math.min(cx, cy) - (outlinePaint.getStrokeWidth() / 2f);
|
||||
|
||||
canvas.translate(getPaddingLeft(), getPaddingTop());
|
||||
canvas.drawCircle(cx, cy, radius, outlinePaint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnClickListener(OnClickListener listener) {
|
||||
this.listener = listener;
|
||||
|
||||
@@ -28,12 +28,12 @@ import androidx.core.view.inputmethod.InputContentInfoCompat;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiEditText;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionDeleter;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionValidatorWatcher;
|
||||
import org.thoughtcrime.securesms.conversation.MessageSendType;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
@@ -201,13 +201,13 @@ public class ComposeText extends EmojiEditText {
|
||||
return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
|
||||
}
|
||||
|
||||
public void setMessageSendType(MessageSendType messageSendType) {
|
||||
public void setTransport(TransportOption transport) {
|
||||
final boolean useSystemEmoji = SignalStore.settings().isPreferSystemEmoji();
|
||||
|
||||
int imeOptions = (getImeOptions() & ~EditorInfo.IME_MASK_ACTION) | EditorInfo.IME_ACTION_SEND;
|
||||
int inputType = getInputType();
|
||||
|
||||
if (isLandscape()) setImeActionLabel(getContext().getString(messageSendType.getComposeHintRes()), EditorInfo.IME_ACTION_SEND);
|
||||
if (isLandscape()) setImeActionLabel(transport.getComposeHint(), EditorInfo.IME_ACTION_SEND);
|
||||
else setImeActionLabel(null, 0);
|
||||
|
||||
if (useSystemEmoji) {
|
||||
@@ -215,9 +215,9 @@ public class ComposeText extends EmojiEditText {
|
||||
}
|
||||
|
||||
setImeOptions(imeOptions);
|
||||
setHint(getContext().getString(messageSendType.getComposeHintRes()),
|
||||
messageSendType.getSimName() != null
|
||||
? getContext().getString(R.string.conversation_activity__from_sim_name, messageSendType.getSimName())
|
||||
setHint(transport.getComposeHint(),
|
||||
transport.getSimName().isPresent()
|
||||
? getContext().getString(R.string.conversation_activity__from_sim_name, transport.getSimName().get())
|
||||
: null);
|
||||
setInputType(inputType);
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ import android.widget.ImageView;
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.Px;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
@@ -31,12 +31,12 @@ public class ConversationItemThumbnail extends FrameLayout {
|
||||
private ImageView shade;
|
||||
private ConversationItemFooter footer;
|
||||
private CornerMask cornerMask;
|
||||
private Outliner outliner;
|
||||
private Outliner pulseOutliner;
|
||||
private boolean borderless;
|
||||
private int[] normalBounds;
|
||||
private int[] gifBounds;
|
||||
private int minimumThumbnailWidth;
|
||||
private int maximumThumbnailHeight;
|
||||
|
||||
public ConversationItemThumbnail(Context context) {
|
||||
super(context);
|
||||
@@ -61,6 +61,9 @@ public class ConversationItemThumbnail extends FrameLayout {
|
||||
this.shade = findViewById(R.id.conversation_thumbnail_shade);
|
||||
this.footer = findViewById(R.id.conversation_thumbnail_footer);
|
||||
this.cornerMask = new CornerMask(this);
|
||||
this.outliner = new Outliner();
|
||||
|
||||
outliner.setColor(ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_20));
|
||||
|
||||
int gifWidth = ViewUtil.dpToPx(260);
|
||||
if (attrs != null) {
|
||||
@@ -85,8 +88,7 @@ public class ConversationItemThumbnail extends FrameLayout {
|
||||
Integer.MAX_VALUE
|
||||
};
|
||||
|
||||
minimumThumbnailWidth = -1;
|
||||
maximumThumbnailHeight = -1;
|
||||
minimumThumbnailWidth = -1;
|
||||
}
|
||||
|
||||
@SuppressWarnings("SuspiciousNameCombination")
|
||||
@@ -96,6 +98,10 @@ public class ConversationItemThumbnail extends FrameLayout {
|
||||
|
||||
if (!borderless) {
|
||||
cornerMask.mask(canvas);
|
||||
|
||||
if (album.getVisibility() != VISIBLE) {
|
||||
outliner.draw(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
if (pulseOutliner != null) {
|
||||
@@ -144,18 +150,14 @@ public class ConversationItemThumbnail extends FrameLayout {
|
||||
|
||||
public void setCorners(int topLeft, int topRight, int bottomRight, int bottomLeft) {
|
||||
cornerMask.setRadii(topLeft, topRight, bottomRight, bottomLeft);
|
||||
outliner.setRadii(topLeft, topRight, bottomRight, bottomLeft);
|
||||
}
|
||||
|
||||
public void setMinimumThumbnailWidth(@Px int width) {
|
||||
public void setMinimumThumbnailWidth(int width) {
|
||||
minimumThumbnailWidth = width;
|
||||
thumbnail.setMinimumThumbnailWidth(width);
|
||||
}
|
||||
|
||||
public void setMaximumThumbnailHeight(@Px int height) {
|
||||
maximumThumbnailHeight = height;
|
||||
thumbnail.setMaximumThumbnailHeight(height);
|
||||
}
|
||||
|
||||
public void setBorderless(boolean borderless) {
|
||||
this.borderless = borderless;
|
||||
}
|
||||
@@ -178,10 +180,6 @@ public class ConversationItemThumbnail extends FrameLayout {
|
||||
if (minimumThumbnailWidth != -1) {
|
||||
thumbnail.setMinimumThumbnailWidth(minimumThumbnailWidth);
|
||||
}
|
||||
|
||||
if (maximumThumbnailHeight != -1) {
|
||||
thumbnail.setMaximumThumbnailHeight(maximumThumbnailHeight);
|
||||
}
|
||||
}
|
||||
|
||||
thumbnail.setVisibility(VISIBLE);
|
||||
|
||||
@@ -7,12 +7,9 @@ import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.airbnb.lottie.SimpleColorFilter;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public final class ConversationScrollToView extends FrameLayout {
|
||||
@@ -46,18 +43,6 @@ public final class ConversationScrollToView extends FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
public void setWallpaperEnabled(boolean hasWallpaper) {
|
||||
if (hasWallpaper) {
|
||||
scrollButton.setBackgroundResource(R.drawable.scroll_to_bottom_background_wallpaper);
|
||||
} else {
|
||||
scrollButton.setBackgroundResource(R.drawable.scroll_to_bottom_background_normal);
|
||||
}
|
||||
}
|
||||
|
||||
public void setUnreadCountBackgroundTint(@ColorInt int tint) {
|
||||
unreadCount.getBackground().setColorFilter(new SimpleColorFilter(tint));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnClickListener(@Nullable OnClickListener l) {
|
||||
scrollButton.setOnClickListener(l);
|
||||
|
||||
@@ -68,13 +68,11 @@ public class ConversationTypingView extends ConstraintLayout {
|
||||
}
|
||||
|
||||
if (hasWallpaper) {
|
||||
bubble.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.conversation_item_recv_bubble_color_wallpaper));
|
||||
typistCount.getBackground().setColorFilter(ContextCompat.getColor(getContext(), R.color.conversation_item_recv_bubble_color_wallpaper), PorterDuff.Mode.SRC_IN);
|
||||
indicator.setDotTint(ContextCompat.getColor(getContext(), R.color.conversation_typing_indicator_foreground_tint_wallpaper));
|
||||
bubble.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.conversation_item_wallpaper_bubble_color));
|
||||
typistCount.getBackground().setColorFilter(ContextCompat.getColor(getContext(), R.color.conversation_item_wallpaper_bubble_color), PorterDuff.Mode.SRC_IN);
|
||||
} else {
|
||||
bubble.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.conversation_item_recv_bubble_color_normal));
|
||||
typistCount.getBackground().setColorFilter(ContextCompat.getColor(getContext(), R.color.conversation_item_recv_bubble_color_normal), PorterDuff.Mode.SRC_IN);
|
||||
indicator.setDotTint(ContextCompat.getColor(getContext(), R.color.conversation_typing_indicator_foreground_tint_normal));
|
||||
bubble.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.signal_background_secondary));
|
||||
typistCount.getBackground().setColorFilter(ContextCompat.getColor(getContext(), R.color.signal_background_secondary), PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
|
||||
indicator.startAnimation();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.ContextThemeWrapper
|
||||
@@ -32,8 +31,6 @@ abstract class FixedRoundedCornerBottomSheetDialogFragment : BottomSheetDialogFr
|
||||
@ColorInt
|
||||
protected var backgroundColor: Int = Color.TRANSPARENT
|
||||
|
||||
private lateinit var dialogBackground: MaterialShapeDrawable
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setStyle(STYLE_NORMAL, themeResId)
|
||||
@@ -49,11 +46,11 @@ abstract class FixedRoundedCornerBottomSheetDialogFragment : BottomSheetDialogFr
|
||||
.setTopRightCorner(CornerFamily.ROUNDED, ViewUtil.dpToPx(requireContext(), 18).toFloat())
|
||||
.build()
|
||||
|
||||
dialogBackground = MaterialShapeDrawable(shapeAppearanceModel)
|
||||
val dialogBackground = MaterialShapeDrawable(shapeAppearanceModel)
|
||||
|
||||
val bottomSheetStyle = ThemeUtil.getThemedResourceId(ContextThemeWrapper(requireContext(), themeResId), R.attr.bottomSheetStyle)
|
||||
backgroundColor = ThemeUtil.getThemedColor(ContextThemeWrapper(requireContext(), bottomSheetStyle), R.attr.backgroundTint)
|
||||
dialogBackground.fillColor = ColorStateList.valueOf(backgroundColor)
|
||||
dialogBackground.setTint(backgroundColor)
|
||||
|
||||
dialog.behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.content.Context;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.style.CharacterStyle;
|
||||
import android.util.AttributeSet;
|
||||
@@ -47,11 +49,13 @@ public class FromTextView extends SimpleEmojiTextView {
|
||||
|
||||
public void setText(Recipient recipient, @Nullable CharSequence fromString, boolean read, @Nullable String suffix) {
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
SpannableString fromSpan = new SpannableString(fromString);
|
||||
fromSpan.setSpan(getFontSpan(!read), 0, fromSpan.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
|
||||
if (recipient.isSelf()) {
|
||||
builder.append(getContext().getString(R.string.note_to_self));
|
||||
} else {
|
||||
builder.append(fromString);
|
||||
builder.append(fromSpan);
|
||||
}
|
||||
|
||||
if (suffix != null) {
|
||||
@@ -81,4 +85,8 @@ public class FromTextView extends SimpleEmojiTextView {
|
||||
|
||||
return mutedDrawable;
|
||||
}
|
||||
|
||||
private CharacterStyle getFontSpan(boolean isBold) {
|
||||
return isBold ? SpanUtil.getBoldSpan() : SpanUtil.getNormalSpan();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationSet;
|
||||
import android.view.animation.Interpolator;
|
||||
import android.view.animation.TranslateAnimation;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
@@ -78,8 +77,8 @@ public class InputPanel extends LinearLayout
|
||||
private LinkPreviewView linkPreview;
|
||||
private EmojiToggle mediaKeyboard;
|
||||
private ComposeText composeText;
|
||||
private ImageButton quickCameraToggle;
|
||||
private ImageButton quickAudioToggle;
|
||||
private View quickCameraToggle;
|
||||
private View quickAudioToggle;
|
||||
private AnimatingToggle buttonToggle;
|
||||
private SendButton sendButton;
|
||||
private View recordingContainer;
|
||||
@@ -95,7 +94,6 @@ public class InputPanel extends LinearLayout
|
||||
private @Nullable Listener listener;
|
||||
private boolean emojiVisible;
|
||||
|
||||
private boolean hideForMessageRequestState;
|
||||
private boolean hideForGroupState;
|
||||
private boolean hideForBlockedState;
|
||||
private boolean hideForSearch;
|
||||
@@ -184,7 +182,7 @@ public class InputPanel extends LinearLayout
|
||||
@NonNull SlideDeck attachments,
|
||||
@NonNull QuoteModel.Type quoteType)
|
||||
{
|
||||
this.quoteView.setQuote(glideRequests, id, author, body, false, attachments, null, quoteType);
|
||||
this.quoteView.setQuote(glideRequests, id, author, body, false, attachments, null, null, quoteType);
|
||||
|
||||
int originalHeight = this.quoteView.getVisibility() == VISIBLE ? this.quoteView.getMeasuredHeight()
|
||||
: 0;
|
||||
@@ -324,39 +322,13 @@ public class InputPanel extends LinearLayout
|
||||
}
|
||||
|
||||
public void setWallpaperEnabled(boolean enabled) {
|
||||
final int iconTint;
|
||||
final int textColor;
|
||||
final int textHintColor;
|
||||
|
||||
if (enabled) {
|
||||
iconTint = getContext().getResources().getColor(R.color.signal_colorOnSurface);
|
||||
textColor = getContext().getResources().getColor(R.color.signal_colorOnSurface);
|
||||
textHintColor = getContext().getResources().getColor(R.color.signal_colorOnSurfaceVariant);
|
||||
|
||||
setBackground(null);
|
||||
setBackground(new ColorDrawable(getContext().getResources().getColor(R.color.wallpaper_compose_background)));
|
||||
composeContainer.setBackground(Objects.requireNonNull(ContextCompat.getDrawable(getContext(), R.drawable.compose_background_wallpaper)));
|
||||
quickAudioToggle.setColorFilter(iconTint);
|
||||
quickCameraToggle.setColorFilter(iconTint);
|
||||
} else {
|
||||
iconTint = getContext().getResources().getColor(R.color.signal_colorOnSurface);
|
||||
textColor = getContext().getResources().getColor(R.color.signal_colorOnSurface);
|
||||
textHintColor = getContext().getResources().getColor(R.color.signal_colorOnSurfaceVariant);
|
||||
|
||||
setBackground(new ColorDrawable(getContext().getResources().getColor(R.color.signal_colorSurface)));
|
||||
setBackground(new ColorDrawable(getContext().getResources().getColor(R.color.signal_background_primary)));
|
||||
composeContainer.setBackground(Objects.requireNonNull(ContextCompat.getDrawable(getContext(), R.drawable.compose_background)));
|
||||
}
|
||||
|
||||
mediaKeyboard.setColorFilter(iconTint);
|
||||
quickAudioToggle.setColorFilter(iconTint);
|
||||
quickCameraToggle.setColorFilter(iconTint);
|
||||
composeText.setTextColor(textColor);
|
||||
composeText.setHintTextColor(textHintColor);
|
||||
quoteView.setWallpaperEnabled(enabled);
|
||||
}
|
||||
|
||||
public void setHideForMessageRequestState(boolean hideForMessageRequestState) {
|
||||
this.hideForMessageRequestState = hideForMessageRequestState;
|
||||
updateVisibility();
|
||||
}
|
||||
|
||||
public void setHideForGroupState(boolean hideForGroupState) {
|
||||
@@ -556,7 +528,7 @@ public class InputPanel extends LinearLayout
|
||||
}
|
||||
|
||||
private void updateVisibility() {
|
||||
if (hideForGroupState || hideForBlockedState || hideForSearch || hideForSelection || hideForMessageRequestState) {
|
||||
if (hideForGroupState || hideForBlockedState || hideForSearch || hideForSelection) {
|
||||
setVisibility(GONE);
|
||||
} else {
|
||||
setVisibility(VISIBLE);
|
||||
|
||||
@@ -22,10 +22,8 @@ abstract class KeyboardEntryDialogFragment(@LayoutRes contentLayoutId: Int) :
|
||||
|
||||
protected open val withDim: Boolean = false
|
||||
|
||||
protected open val themeResId: Int = R.style.Theme_Signal_RoundedBottomSheet
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setStyle(STYLE_NORMAL, themeResId)
|
||||
setStyle(STYLE_NORMAL, R.style.Theme_Signal_RoundedBottomSheet)
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
@@ -50,6 +51,7 @@ public class LinkPreviewView extends FrameLayout {
|
||||
private int type;
|
||||
private int defaultRadius;
|
||||
private CornerMask cornerMask;
|
||||
private Outliner outliner;
|
||||
private CloseClickedListener closeClickedListener;
|
||||
|
||||
public LinkPreviewView(Context context) {
|
||||
@@ -76,6 +78,9 @@ public class LinkPreviewView extends FrameLayout {
|
||||
noPreview = findViewById(R.id.linkpreview_no_preview);
|
||||
defaultRadius = getResources().getDimensionPixelSize(R.dimen.thumbnail_default_radius);
|
||||
cornerMask = new CornerMask(this);
|
||||
outliner = new Outliner();
|
||||
|
||||
outliner.setColor(ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_20));
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.LinkPreviewView, 0, 0);
|
||||
@@ -107,6 +112,7 @@ public class LinkPreviewView extends FrameLayout {
|
||||
if (type == TYPE_COMPOSE) return;
|
||||
|
||||
cornerMask.mask(canvas);
|
||||
outliner.draw(canvas);
|
||||
}
|
||||
|
||||
public void setLoading() {
|
||||
@@ -128,10 +134,6 @@ public class LinkPreviewView extends FrameLayout {
|
||||
}
|
||||
|
||||
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail) {
|
||||
setLinkPreview(glideRequests, linkPreview, showThumbnail, true);
|
||||
}
|
||||
|
||||
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail, boolean showDescription) {
|
||||
spinner.setVisibility(GONE);
|
||||
noPreview.setVisibility(GONE);
|
||||
|
||||
@@ -142,7 +144,7 @@ public class LinkPreviewView extends FrameLayout {
|
||||
title.setVisibility(GONE);
|
||||
}
|
||||
|
||||
if (showDescription && !Util.isEmpty(linkPreview.getDescription())) {
|
||||
if (!Util.isEmpty(linkPreview.getDescription())) {
|
||||
description.setText(linkPreview.getDescription());
|
||||
description.setVisibility(VISIBLE);
|
||||
} else {
|
||||
@@ -183,9 +185,11 @@ public class LinkPreviewView extends FrameLayout {
|
||||
public void setCorners(int topStart, int topEnd) {
|
||||
if (ViewUtil.isRtl(this)) {
|
||||
cornerMask.setRadii(topEnd, topStart, 0, 0);
|
||||
outliner.setRadii(topEnd, topStart, 0, 0);
|
||||
thumbnail.setCorners(defaultRadius, topEnd, defaultRadius, defaultRadius);
|
||||
} else {
|
||||
cornerMask.setRadii(topStart, topEnd, 0, 0);
|
||||
outliner.setRadii(topStart, topEnd, 0, 0);
|
||||
thumbnail.setCorners(topStart, defaultRadius, defaultRadius, defaultRadius);
|
||||
}
|
||||
postInvalidate();
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.PointF
|
||||
import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewAnimationUtils
|
||||
import android.widget.EditText
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.animation.addListener
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
/**
|
||||
* Search Toolbar following the Signal Material3 design spec.
|
||||
*/
|
||||
class Material3SearchToolbar @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : ConstraintLayout(context, attrs) {
|
||||
|
||||
var listener: Listener? = null
|
||||
|
||||
private val input: EditText
|
||||
|
||||
private val circularRevealPoint = PointF()
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.material3_serarch_toolbar, this)
|
||||
|
||||
input = findViewById(R.id.search_input)
|
||||
|
||||
val close = findViewById<View>(R.id.search_close)
|
||||
val clear = findViewById<View>(R.id.search_clear)
|
||||
|
||||
close.setOnClickListener { collapse() }
|
||||
clear.setOnClickListener { input.setText("") }
|
||||
|
||||
input.addTextChangedListener(afterTextChanged = {
|
||||
clear.visible = !it.isNullOrBlank()
|
||||
listener?.onSearchTextChange(it?.toString() ?: "")
|
||||
})
|
||||
}
|
||||
|
||||
fun display(x: Float, y: Float) {
|
||||
if (Build.VERSION.SDK_INT < 21) {
|
||||
visibility = VISIBLE
|
||||
ViewUtil.focusAndShowKeyboard(input)
|
||||
} else if (!visible) {
|
||||
circularRevealPoint.set(x, y)
|
||||
|
||||
val animator = ViewAnimationUtils.createCircularReveal(this, x.toInt(), y.toInt(), 0f, width.toFloat())
|
||||
animator.duration = 400
|
||||
|
||||
visibility = VISIBLE
|
||||
ViewUtil.focusAndShowKeyboard(input)
|
||||
animator.start()
|
||||
}
|
||||
}
|
||||
|
||||
fun collapse() {
|
||||
if (visibility == VISIBLE) {
|
||||
listener?.onSearchClosed()
|
||||
ViewUtil.hideKeyboard(context, input)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
val animator = ViewAnimationUtils.createCircularReveal(this, circularRevealPoint.x.toInt(), circularRevealPoint.y.toInt(), width.toFloat(), 0f)
|
||||
animator.duration = 400
|
||||
|
||||
animator.addListener(onEnd = {
|
||||
visibility = INVISIBLE
|
||||
})
|
||||
animator.start()
|
||||
} else {
|
||||
visibility = INVISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun onSearchTextChange(text: String)
|
||||
fun onSearchClosed()
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ package org.thoughtcrime.securesms.components;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
@@ -16,6 +17,7 @@ import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.google.android.material.imageview.ShapeableImageView;
|
||||
@@ -28,7 +30,7 @@ import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.components.quotes.QuoteViewColorTheme;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
@@ -41,6 +43,7 @@ import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
import org.thoughtcrime.securesms.stories.StoryTextPostModel;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -76,7 +79,6 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
}
|
||||
}
|
||||
|
||||
private View background;
|
||||
private ViewGroup mainView;
|
||||
private ViewGroup footerView;
|
||||
private TextView authorView;
|
||||
@@ -101,7 +103,6 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
private int smallCornerRadius;
|
||||
private CornerMask cornerMask;
|
||||
private QuoteModel.Type quoteType;
|
||||
private boolean isWallpaperEnabled;
|
||||
|
||||
private int thumbHeight;
|
||||
private int thumbWidth;
|
||||
@@ -130,7 +131,6 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
private void initialize(@Nullable AttributeSet attrs) {
|
||||
inflate(getContext(), R.layout.quote_view, this);
|
||||
|
||||
this.background = findViewById(R.id.quote_background);
|
||||
this.mainView = findViewById(R.id.quote_main);
|
||||
this.footerView = findViewById(R.id.quote_missing_footer);
|
||||
this.authorView = findViewById(R.id.quote_author);
|
||||
@@ -151,12 +151,19 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
cornerMask = new CornerMask(this);
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.QuoteView, 0, 0);
|
||||
|
||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.QuoteView, 0, 0);
|
||||
int primaryColor = typedArray.getColor(R.styleable.QuoteView_quote_colorPrimary, Color.BLACK);
|
||||
int secondaryColor = typedArray.getColor(R.styleable.QuoteView_quote_colorSecondary, Color.BLACK);
|
||||
messageType = MessageType.fromCode(typedArray.getInt(R.styleable.QuoteView_message_type, 0));
|
||||
typedArray.recycle();
|
||||
|
||||
dismissView.setVisibility(messageType == MessageType.PREVIEW ? VISIBLE : GONE);
|
||||
|
||||
authorView.setTextColor(primaryColor);
|
||||
bodyView.setTextColor(primaryColor);
|
||||
attachmentNameView.setTextColor(primaryColor);
|
||||
mediaDescriptionText.setTextColor(secondaryColor);
|
||||
missingLinkText.setTextColor(primaryColor);
|
||||
}
|
||||
|
||||
setMessageType(messageType);
|
||||
@@ -204,6 +211,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
@Nullable CharSequence body,
|
||||
boolean originalMissing,
|
||||
@NonNull SlideDeck attachments,
|
||||
@Nullable ChatColors chatColors,
|
||||
@Nullable String storyReaction,
|
||||
@NonNull QuoteModel.Type quoteType)
|
||||
{
|
||||
@@ -220,7 +228,12 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
setQuoteText(resolveBody(body, quoteType), attachments, originalMissing, storyReaction);
|
||||
setQuoteAttachment(glideRequests, body, attachments, originalMissing);
|
||||
setQuoteMissingFooter(originalMissing);
|
||||
applyColorTheme();
|
||||
|
||||
if (Build.VERSION.SDK_INT < 21 && messageType == MessageType.INCOMING && chatColors != null) {
|
||||
this.setBackgroundColor(chatColors.asSingleColor());
|
||||
} else {
|
||||
this.setBackground(null);
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable CharSequence resolveBody(@Nullable CharSequence body, @NonNull QuoteModel.Type quoteType) {
|
||||
@@ -242,11 +255,6 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
setVisibility(GONE);
|
||||
}
|
||||
|
||||
public void setWallpaperEnabled(boolean isWallpaperEnabled) {
|
||||
this.isWallpaperEnabled = isWallpaperEnabled;
|
||||
applyColorTheme();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecipientChanged(@NonNull Recipient recipient) {
|
||||
setQuoteAuthor(recipient);
|
||||
@@ -261,6 +269,9 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
}
|
||||
|
||||
private void setQuoteAuthor(@NonNull Recipient author) {
|
||||
boolean outgoing = messageType != MessageType.INCOMING && messageType != MessageType.STORY_REPLY_INCOMING;
|
||||
boolean preview = messageType == MessageType.PREVIEW || messageType == MessageType.STORY_REPLY_PREVIEW;
|
||||
|
||||
if (isStoryReply()) {
|
||||
authorView.setText(author.isSelf() ? getContext().getString(R.string.QuoteView_your_story)
|
||||
: getContext().getString(R.string.QuoteView_s_story, author.getDisplayName(getContext())));
|
||||
@@ -268,6 +279,19 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
authorView.setText(author.isSelf() ? getContext().getString(R.string.QuoteView_you)
|
||||
: author.getDisplayName(getContext()));
|
||||
}
|
||||
|
||||
quoteBarView.setBackgroundColor(ContextCompat.getColor(getContext(), outgoing || isStoryReply() ? R.color.core_white : android.R.color.transparent));
|
||||
|
||||
int mainViewColor;
|
||||
if (preview) {
|
||||
mainViewColor = R.color.quote_preview_background;
|
||||
} else if (!outgoing && isStoryReply()) {
|
||||
mainViewColor = R.color.quote_incoming_story_background;
|
||||
} else {
|
||||
mainViewColor = R.color.quote_view_background;
|
||||
}
|
||||
|
||||
mainView.setBackgroundColor(ContextCompat.getColor(getContext(), mainViewColor));
|
||||
}
|
||||
|
||||
private boolean isStoryReply() {
|
||||
@@ -426,10 +450,15 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
attachmentContainerView.setVisibility(GONE);
|
||||
dismissView.setBackgroundDrawable(null);
|
||||
}
|
||||
|
||||
if (ThemeUtil.isDarkTheme(getContext())) {
|
||||
dismissView.setBackgroundResource(R.drawable.circle_alpha);
|
||||
}
|
||||
}
|
||||
|
||||
private void setQuoteMissingFooter(boolean missing) {
|
||||
footerView.setVisibility(missing && !isStoryReply() ? VISIBLE : GONE);
|
||||
footerView.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.quote_view_background));
|
||||
}
|
||||
|
||||
private @Nullable StoryTextPostModel getStoryTextPost(@Nullable CharSequence body) {
|
||||
@@ -486,20 +515,4 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private void applyColorTheme() {
|
||||
boolean isOutgoing = messageType != MessageType.INCOMING && messageType != MessageType.STORY_REPLY_INCOMING;
|
||||
boolean isPreview = messageType == MessageType.PREVIEW || messageType == MessageType.STORY_REPLY_PREVIEW;
|
||||
|
||||
QuoteViewColorTheme quoteViewColorTheme = QuoteViewColorTheme.resolveTheme(isOutgoing, isPreview, isWallpaperEnabled);
|
||||
|
||||
quoteBarView.setBackgroundColor(quoteViewColorTheme.getBarColor(getContext()));
|
||||
background.setBackgroundColor(quoteViewColorTheme.getBackgroundColor(getContext()));
|
||||
authorView.setTextColor(quoteViewColorTheme.getForegroundColor(getContext()));
|
||||
bodyView.setTextColor(quoteViewColorTheme.getForegroundColor(getContext()));
|
||||
attachmentNameView.setTextColor(quoteViewColorTheme.getForegroundColor(getContext()));
|
||||
mediaDescriptionText.setTextColor(quoteViewColorTheme.getForegroundColor(getContext()));
|
||||
missingLinkText.setTextColor(quoteViewColorTheme.getForegroundColor(getContext()));
|
||||
footerView.setBackgroundColor(quoteViewColorTheme.getBackgroundColor(getContext()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.AppCompatImageButton;
|
||||
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.TransportOptions;
|
||||
import org.thoughtcrime.securesms.TransportOptions.OnTransportChangedListener;
|
||||
import org.thoughtcrime.securesms.TransportOptionsPopup;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
|
||||
public class SendButton extends AppCompatImageButton
|
||||
implements TransportOptions.OnTransportChangedListener,
|
||||
TransportOptionsPopup.SelectedListener,
|
||||
View.OnLongClickListener
|
||||
{
|
||||
|
||||
private final TransportOptions transportOptions;
|
||||
|
||||
private Optional<TransportOptionsPopup> transportOptionsPopup = Optional.empty();
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public SendButton(Context context) {
|
||||
super(context);
|
||||
this.transportOptions = initializeTransportOptions(false);
|
||||
ViewUtil.mirrorIfRtl(this, getContext());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public SendButton(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
this.transportOptions = initializeTransportOptions(false);
|
||||
ViewUtil.mirrorIfRtl(this, getContext());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public SendButton(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
this.transportOptions = initializeTransportOptions(false);
|
||||
ViewUtil.mirrorIfRtl(this, getContext());
|
||||
}
|
||||
|
||||
private TransportOptions initializeTransportOptions(boolean media) {
|
||||
if (isInEditMode()) return null;
|
||||
|
||||
TransportOptions transportOptions = new TransportOptions(getContext(), media);
|
||||
transportOptions.addOnTransportChangedListener(this);
|
||||
|
||||
setOnLongClickListener(this);
|
||||
|
||||
return transportOptions;
|
||||
}
|
||||
|
||||
private TransportOptionsPopup getTransportOptionsPopup() {
|
||||
if (!transportOptionsPopup.isPresent()) {
|
||||
transportOptionsPopup = Optional.of(new TransportOptionsPopup(getContext(), this, this));
|
||||
}
|
||||
return transportOptionsPopup.get();
|
||||
}
|
||||
|
||||
public boolean isManualSelection() {
|
||||
return transportOptions.isManualSelection();
|
||||
}
|
||||
|
||||
public void addOnTransportChangedListener(OnTransportChangedListener listener) {
|
||||
transportOptions.addOnTransportChangedListener(listener);
|
||||
}
|
||||
|
||||
public TransportOption getSelectedTransport() {
|
||||
return transportOptions.getSelectedTransport();
|
||||
}
|
||||
|
||||
public void resetAvailableTransports(boolean isMediaMessage) {
|
||||
transportOptions.reset(isMediaMessage);
|
||||
}
|
||||
|
||||
public void disableTransport(TransportOption.Type type) {
|
||||
transportOptions.disableTransport(type);
|
||||
}
|
||||
|
||||
public void setDefaultTransport(TransportOption.Type type) {
|
||||
transportOptions.setDefaultTransport(type);
|
||||
}
|
||||
|
||||
public void setTransport(@NonNull TransportOption option) {
|
||||
transportOptions.setSelectedTransport(option);
|
||||
}
|
||||
|
||||
public void setDefaultSubscriptionId(Optional<Integer> subscriptionId) {
|
||||
transportOptions.setDefaultSubscriptionId(subscriptionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelected(TransportOption option) {
|
||||
transportOptions.setSelectedTransport(option);
|
||||
getTransportOptionsPopup().dismiss();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChange(TransportOption newTransport, boolean isManualSelection) {
|
||||
setImageResource(newTransport.getDrawable());
|
||||
setContentDescription(newTransport.getDescription());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
if (isEnabled() && transportOptions.getEnabledTransports().size() > 1) {
|
||||
getTransportOptionsPopup().display(transportOptions.getEnabledTransports());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.View.OnLongClickListener
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.AppCompatImageButton
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem
|
||||
import org.thoughtcrime.securesms.components.menu.SignalContextMenu
|
||||
import org.thoughtcrime.securesms.conversation.MessageSendType
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import java.lang.AssertionError
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
/**
|
||||
* The send button you see in a conversation.
|
||||
* Also encapsulates the long-press menu that allows users to switch [MessageSendType]s.
|
||||
*/
|
||||
class SendButton(context: Context, attributeSet: AttributeSet?) : AppCompatImageButton(context, attributeSet), OnLongClickListener {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(SendButton::class.java)
|
||||
}
|
||||
|
||||
private val listeners: MutableList<SendTypeChangedListener> = CopyOnWriteArrayList()
|
||||
|
||||
private var availableSendTypes: List<MessageSendType> = MessageSendType.getAllAvailable(context, false)
|
||||
private var activeMessageSendType: MessageSendType? = null
|
||||
private var defaultTransportType: MessageSendType.TransportType = MessageSendType.TransportType.SMS
|
||||
private var defaultSubscriptionId: Int? = null
|
||||
private var popupContainer: ViewGroup? = null
|
||||
|
||||
init {
|
||||
setOnLongClickListener(this)
|
||||
ViewUtil.mirrorIfRtl(this, getContext())
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the [selectedSendType] was chosen manually by the user, otherwise false.
|
||||
*/
|
||||
val isManualSelection: Boolean
|
||||
get() = activeMessageSendType != null
|
||||
|
||||
/**
|
||||
* The actively-selected send type.
|
||||
*/
|
||||
val selectedSendType: MessageSendType
|
||||
get() {
|
||||
activeMessageSendType?.let {
|
||||
return it
|
||||
}
|
||||
|
||||
if (defaultTransportType === MessageSendType.TransportType.SMS) {
|
||||
for (type in availableSendTypes) {
|
||||
if (type.usesSmsTransport && (defaultSubscriptionId == null || type.simSubscriptionId == defaultSubscriptionId)) {
|
||||
return type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (type in availableSendTypes) {
|
||||
if (type.transportType === defaultTransportType) {
|
||||
return type
|
||||
}
|
||||
}
|
||||
|
||||
Log.w(TAG, "No options of default type! Resetting. DefaultTransportType: $defaultTransportType, AllAvailable: ${availableSendTypes.map { it.transportType }}")
|
||||
|
||||
val signalType: MessageSendType? = availableSendTypes.firstOrNull { it.usesSignalTransport }
|
||||
if (signalType != null) {
|
||||
Log.w(TAG, "No options of default type, but Signal type is available. Switching. DefaultTransportType: $defaultTransportType, AllAvailable: ${availableSendTypes.map { it.transportType }}")
|
||||
defaultTransportType = MessageSendType.TransportType.SIGNAL
|
||||
onSelectionChanged(signalType, false)
|
||||
return signalType
|
||||
} else if (availableSendTypes.isEmpty()) {
|
||||
Log.w(TAG, "No send types available at all! Enabling the Signal transport.")
|
||||
defaultTransportType = MessageSendType.TransportType.SIGNAL
|
||||
availableSendTypes = listOf(MessageSendType.SignalMessageSendType)
|
||||
onSelectionChanged(MessageSendType.SignalMessageSendType, false)
|
||||
return MessageSendType.SignalMessageSendType
|
||||
} else {
|
||||
throw AssertionError("No options of default type! DefaultTransportType: $defaultTransportType, AllAvailable: ${availableSendTypes.map { it.transportType }}")
|
||||
}
|
||||
}
|
||||
|
||||
fun addOnSelectionChangedListener(listener: SendTypeChangedListener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
fun triggerSelectedChangedEvent() {
|
||||
onSelectionChanged(newType = selectedSendType, isManualSelection = false)
|
||||
}
|
||||
|
||||
fun resetAvailableTransports(isMediaMessage: Boolean) {
|
||||
availableSendTypes = MessageSendType.getAllAvailable(context, isMediaMessage)
|
||||
|
||||
if (!availableSendTypes.contains(activeMessageSendType)) {
|
||||
Log.w(TAG, "[resetAvailableTransports] The active send type is no longer available. Unsetting.")
|
||||
setSendType(null)
|
||||
} else {
|
||||
defaultTransportType = MessageSendType.TransportType.SMS
|
||||
defaultSubscriptionId = null
|
||||
onSelectionChanged(newType = selectedSendType, isManualSelection = false)
|
||||
}
|
||||
}
|
||||
|
||||
fun disableTransportType(type: MessageSendType.TransportType) {
|
||||
availableSendTypes = availableSendTypes.filterNot { it.transportType == type }
|
||||
}
|
||||
|
||||
fun setDefaultTransport(type: MessageSendType.TransportType) {
|
||||
if (defaultTransportType == type) {
|
||||
return
|
||||
}
|
||||
defaultTransportType = type
|
||||
onSelectionChanged(newType = selectedSendType, isManualSelection = false)
|
||||
}
|
||||
|
||||
fun setSendType(sendType: MessageSendType?) {
|
||||
if (activeMessageSendType == sendType) {
|
||||
return
|
||||
}
|
||||
activeMessageSendType = sendType
|
||||
onSelectionChanged(newType = selectedSendType, isManualSelection = true)
|
||||
}
|
||||
|
||||
fun setDefaultSubscriptionId(subscriptionId: Int?) {
|
||||
if (defaultSubscriptionId == subscriptionId) {
|
||||
return
|
||||
}
|
||||
defaultSubscriptionId = subscriptionId
|
||||
onSelectionChanged(newType = selectedSendType, isManualSelection = false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be called with a view that is acceptable for determining the bounds of the popup selector.
|
||||
*/
|
||||
fun setPopupContainer(container: ViewGroup) {
|
||||
popupContainer = container
|
||||
}
|
||||
|
||||
private fun onSelectionChanged(newType: MessageSendType, isManualSelection: Boolean) {
|
||||
setImageResource(newType.buttonDrawableRes)
|
||||
contentDescription = context.getString(newType.titleRes)
|
||||
|
||||
for (listener in listeners) {
|
||||
listener.onSendTypeChanged(newType, isManualSelection)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLongClick(v: View): Boolean {
|
||||
if (!isEnabled || availableSendTypes.size == 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
val currentlySelected: MessageSendType = selectedSendType
|
||||
|
||||
val items = availableSendTypes
|
||||
.filterNot { it == currentlySelected }
|
||||
.map { option ->
|
||||
ActionItem(
|
||||
iconRes = option.menuDrawableRes,
|
||||
title = option.getTitle(context),
|
||||
action = { setSendType(option) }
|
||||
)
|
||||
}
|
||||
|
||||
SignalContextMenu.Builder((parent as View), popupContainer!!)
|
||||
.preferredVerticalPosition(SignalContextMenu.VerticalPosition.ABOVE)
|
||||
.offsetY(ViewUtil.dpToPx(8))
|
||||
.show(items)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
interface SendTypeChangedListener {
|
||||
fun onSendTypeChanged(newType: MessageSendType, manuallySelected: Boolean)
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Px;
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
@@ -156,16 +155,11 @@ public class ThumbnailView extends FrameLayout {
|
||||
captionIcon.setScaleY(captionIconScale);
|
||||
}
|
||||
|
||||
public void setMinimumThumbnailWidth(@Px int width) {
|
||||
public void setMinimumThumbnailWidth(int width) {
|
||||
bounds[MIN_WIDTH] = width;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setMaximumThumbnailHeight(@Px int height) {
|
||||
bounds[MAX_HEIGHT] = height;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@SuppressWarnings("SuspiciousNameCombination")
|
||||
private void fillTargetDimensions(int[] targetDimens, int[] dimens, int[] bounds) {
|
||||
int dimensFilledCount = getNonZeroCount(dimens);
|
||||
|
||||
@@ -9,7 +9,6 @@ import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
@@ -55,16 +54,12 @@ public class TypingIndicatorView extends LinearLayout {
|
||||
int tint = typedArray.getColor(R.styleable.TypingIndicatorView_typingIndicator_tint, Color.WHITE);
|
||||
typedArray.recycle();
|
||||
|
||||
setDotTint(tint);
|
||||
dot1.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY);
|
||||
dot2.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY);
|
||||
dot3.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY);
|
||||
}
|
||||
}
|
||||
|
||||
public void setDotTint(@ColorInt int tint) {
|
||||
dot1.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY);
|
||||
dot2.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY);
|
||||
dot3.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
if (!isActive) {
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.fragments.findListener
|
||||
|
||||
/**
|
||||
* Convenience class for wrapping Fragments in full-screen dialogs. Due to how fragments work, they
|
||||
* must be public static classes. Therefore, this class should be subclassed as its own entity, rather
|
||||
* than via `object : WrapperDialogFragment`.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```
|
||||
* class Dialog : WrapperDialogFragment() {
|
||||
* override fun getWrappedFragment(): Fragment {
|
||||
* return NavHostFragment.create(R.navigation.private_story_settings, requireArguments())
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* companion object {
|
||||
* fun createAsDialog(distributionListId: DistributionListId): DialogFragment {
|
||||
* return Dialog().apply {
|
||||
* arguments = PrivateStorySettingsFragmentArgs.Builder(distributionListId).build().toBundle()
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
abstract class WrapperDialogFragment : DialogFragment(R.layout.fragment_container) {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setStyle(STYLE_NO_FRAME, R.style.Signal_DayNight_Dialog_FullScreen)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
if (savedInstanceState == null) {
|
||||
childFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, getWrappedFragment())
|
||||
.commitAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
findListener<WrapperDialogFragmentCallback>()?.onWrapperDialogFragmentDismissed()
|
||||
}
|
||||
|
||||
abstract fun getWrappedFragment(): Fragment
|
||||
|
||||
interface WrapperDialogFragmentCallback {
|
||||
fun onWrapperDialogFragmentDismissed()
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.ViewKt;
|
||||
import androidx.core.widget.TextViewCompat;
|
||||
|
||||
import org.signal.core.util.StringUtil;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
@@ -152,10 +151,10 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
// Android fails to ellipsize spannable strings. (https://issuetracker.google.com/issues/36991688)
|
||||
// We ellipsize them ourselves by manually truncating the appropriate section.
|
||||
if (getText() != null && getText().length() > 0 && isEllipsizedAtEnd()) {
|
||||
if (getMaxLines() > 0) {
|
||||
ellipsizeEmojiTextForMaxLines();
|
||||
} else if (maxLength > 0) {
|
||||
if (maxLength > 0) {
|
||||
ellipsizeAnyTextForMaxLength();
|
||||
} else if (getMaxLines() > 0) {
|
||||
ellipsizeEmojiTextForMaxLines();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,17 +308,11 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
|
||||
int lineCount = getLineCount();
|
||||
if (lineCount > maxLines) {
|
||||
int overflowStart = getLayout().getLineStart(maxLines - 1);
|
||||
|
||||
if (maxLength > 0 && overflowStart > maxLength) {
|
||||
ellipsizeAnyTextForMaxLength();
|
||||
return;
|
||||
}
|
||||
|
||||
int overflowStart = getLayout().getLineStart(maxLines - 1);
|
||||
int overflowEnd = getLayout().getLineEnd(maxLines - 1);
|
||||
CharSequence overflow = getText().subSequence(overflowStart, overflowEnd);
|
||||
float adjust = overflowText != null ? getPaint().measureText(overflowText, 0, overflowText.length()) : 0f;
|
||||
CharSequence ellipsized = StringUtil.trim(TextUtils.ellipsize(overflow, getPaint(), getWidth() - adjust, TextUtils.TruncateAt.END));
|
||||
CharSequence ellipsized = TextUtils.ellipsize(overflow, getPaint(), getWidth() - adjust, TextUtils.TruncateAt.END);
|
||||
|
||||
SpannableStringBuilder newContent = new SpannableStringBuilder();
|
||||
newContent.append(getText().subSequence(0, overflowStart))
|
||||
@@ -330,8 +323,6 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
CharSequence emojified = EmojiProvider.emojify(newCandidates, newContent, this, isJumbomoji || forceJumboEmoji);
|
||||
|
||||
super.setText(emojified, BufferType.SPANNABLE);
|
||||
} else if (maxLength > 0) {
|
||||
ellipsizeAnyTextForMaxLength();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -26,17 +26,17 @@ public class EmojiToggle extends AppCompatImageButton implements MediaKeyboard.M
|
||||
|
||||
public EmojiToggle(Context context) {
|
||||
super(context);
|
||||
initialize();
|
||||
initialize(null);
|
||||
}
|
||||
|
||||
public EmojiToggle(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize();
|
||||
initialize(attrs);
|
||||
}
|
||||
|
||||
public EmojiToggle(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
initialize();
|
||||
initialize(attrs);
|
||||
}
|
||||
|
||||
public void setToMedia() {
|
||||
@@ -47,11 +47,18 @@ public class EmojiToggle extends AppCompatImageButton implements MediaKeyboard.M
|
||||
setImageDrawable(imeToggle);
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
this.emojiToggle = ContextUtil.requireDrawable(getContext(), R.drawable.ic_emoji);
|
||||
private void initialize(@Nullable AttributeSet attrs) {
|
||||
boolean forceOutline = false;
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiToggle, 0, 0);
|
||||
forceOutline = typedArray.getBoolean(R.styleable.EmojiToggle_force_outline, false);
|
||||
typedArray.recycle();
|
||||
}
|
||||
|
||||
this.emojiToggle = ContextUtil.requireDrawable(getContext(), forceOutline ? R.drawable.ic_emoji_outline : R.drawable.ic_emoji);
|
||||
this.stickerToggle = ContextUtil.requireDrawable(getContext(), R.drawable.ic_sticker_24);
|
||||
this.gifToggle = ContextUtil.requireDrawable(getContext(), R.drawable.ic_gif_24);
|
||||
this.imeToggle = ContextUtil.requireDrawable(getContext(), R.drawable.ic_keyboard_24);
|
||||
this.imeToggle = ContextUtil.requireDrawable(getContext(), forceOutline ? R.drawable.ic_keyboard_outline_24 : R.drawable.ic_keyboard_24);
|
||||
this.mediaToggle = emojiToggle;
|
||||
|
||||
setToMedia();
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
package org.thoughtcrime.securesms.components.menu
|
||||
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
* Represents an action to be rendered via [SignalContextMenu] or [SignalBottomActionBar]
|
||||
*/
|
||||
data class ActionItem @JvmOverloads constructor(
|
||||
data class ActionItem(
|
||||
@DrawableRes val iconRes: Int,
|
||||
val title: CharSequence,
|
||||
@ColorRes val tintRes: Int = R.color.signal_colorOnSurface,
|
||||
val action: Runnable,
|
||||
val action: Runnable
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.os.Build
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -79,10 +78,6 @@ class ContextMenuList(recyclerView: RecyclerView, onItemClick: () -> Unit) {
|
||||
onItemClick()
|
||||
}
|
||||
|
||||
val tintColor = ContextCompat.getColor(context, model.item.tintRes)
|
||||
icon.setColorFilter(tintColor)
|
||||
title.setTextColor(tintColor)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
when (model.displayType) {
|
||||
DisplayType.TOP -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_top)
|
||||
|
||||
@@ -25,7 +25,6 @@ class SignalContextMenu private constructor(
|
||||
val baseOffsetX: Int = 0,
|
||||
val baseOffsetY: Int = 0,
|
||||
val horizontalPosition: HorizontalPosition = HorizontalPosition.START,
|
||||
val verticalPosition: VerticalPosition = VerticalPosition.BELOW,
|
||||
val onDismiss: Runnable? = null
|
||||
) : PopupWindow(
|
||||
LayoutInflater.from(anchor.context).inflate(R.layout.signal_context_menu, null),
|
||||
@@ -42,7 +41,6 @@ class SignalContextMenu private constructor(
|
||||
|
||||
init {
|
||||
setBackgroundDrawable(ContextCompat.getDrawable(context, R.drawable.signal_context_menu_background))
|
||||
inputMethodMode = INPUT_METHOD_NOT_NEEDED
|
||||
|
||||
isFocusable = true
|
||||
|
||||
@@ -82,10 +80,7 @@ class SignalContextMenu private constructor(
|
||||
|
||||
val offsetY: Int
|
||||
|
||||
if (verticalPosition == VerticalPosition.ABOVE && menuTopBound > screenTopBound) {
|
||||
offsetY = -(anchorRect.height() + contentView.measuredHeight + baseOffsetY)
|
||||
contextMenuList.setItems(items.reversed())
|
||||
} else if (menuBottomBound < screenBottomBound) {
|
||||
if (menuBottomBound < screenBottomBound) {
|
||||
offsetY = baseOffsetY
|
||||
} else if (menuTopBound > screenTopBound) {
|
||||
offsetY = -(anchorRect.height() + contentView.measuredHeight + baseOffsetY)
|
||||
@@ -120,10 +115,6 @@ class SignalContextMenu private constructor(
|
||||
START, END
|
||||
}
|
||||
|
||||
enum class VerticalPosition {
|
||||
ABOVE, BELOW
|
||||
}
|
||||
|
||||
/**
|
||||
* @param anchor The view to put the pop-up on
|
||||
* @param container A parent of [anchor] that represents the acceptable boundaries of the popup
|
||||
@@ -137,7 +128,6 @@ class SignalContextMenu private constructor(
|
||||
var offsetX = 0
|
||||
var offsetY = 0
|
||||
var horizontalPosition = HorizontalPosition.START
|
||||
var verticalPosition = VerticalPosition.BELOW
|
||||
|
||||
fun onDismiss(onDismiss: Runnable): Builder {
|
||||
this.onDismiss = onDismiss
|
||||
@@ -159,11 +149,6 @@ class SignalContextMenu private constructor(
|
||||
return this
|
||||
}
|
||||
|
||||
fun preferredVerticalPosition(verticalPosition: VerticalPosition): Builder {
|
||||
this.verticalPosition = verticalPosition
|
||||
return this
|
||||
}
|
||||
|
||||
fun show(items: List<ActionItem>): SignalContextMenu {
|
||||
return SignalContextMenu(
|
||||
anchor = anchor,
|
||||
@@ -172,7 +157,6 @@ class SignalContextMenu private constructor(
|
||||
baseOffsetX = offsetX,
|
||||
baseOffsetY = offsetY,
|
||||
horizontalPosition = horizontalPosition,
|
||||
verticalPosition = verticalPosition,
|
||||
onDismiss = onDismiss
|
||||
).show()
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.quotes
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
enum class QuoteViewColorTheme(
|
||||
private val backgroundColorRes: Int,
|
||||
private val barColorRes: Int,
|
||||
private val foregroundColorRes: Int
|
||||
) {
|
||||
|
||||
INCOMING_WALLPAPER(
|
||||
R.color.quote_view_background_incoming_wallpaper,
|
||||
R.color.quote_view_bar_incoming_wallpaper,
|
||||
R.color.quote_view_foreground_incoming_wallpaper
|
||||
),
|
||||
INCOMING_NORMAL(
|
||||
R.color.quote_view_background_incoming_normal,
|
||||
R.color.quote_view_bar_incoming_normal,
|
||||
R.color.quote_view_foreground_incoming_normal
|
||||
),
|
||||
OUTGOING_WALLPAPER(
|
||||
R.color.quote_view_background_outgoing_wallpaper,
|
||||
R.color.quote_view_bar_outgoing_wallpaper,
|
||||
R.color.quote_view_foreground_outgoing_wallpaper
|
||||
),
|
||||
OUTGOING_NORMAL(
|
||||
R.color.quote_view_background_outgoing_normal,
|
||||
R.color.quote_view_bar_outgoing_normal,
|
||||
R.color.quote_view_foreground_outgoing_normal
|
||||
);
|
||||
|
||||
fun getBackgroundColor(context: Context) = ContextCompat.getColor(context, backgroundColorRes)
|
||||
fun getBarColor(context: Context) = ContextCompat.getColor(context, barColorRes)
|
||||
fun getForegroundColor(context: Context) = ContextCompat.getColor(context, foregroundColorRes)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun resolveTheme(isOutgoing: Boolean, isPreview: Boolean, hasWallpaper: Boolean): QuoteViewColorTheme {
|
||||
return when {
|
||||
isPreview && hasWallpaper -> INCOMING_WALLPAPER
|
||||
isPreview && !hasWallpaper -> INCOMING_NORMAL
|
||||
isOutgoing && hasWallpaper -> OUTGOING_WALLPAPER
|
||||
!isOutgoing && hasWallpaper -> INCOMING_WALLPAPER
|
||||
isOutgoing && !hasWallpaper -> OUTGOING_NORMAL
|
||||
else -> INCOMING_NORMAL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,11 @@ import android.util.AttributeSet;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class CallMeCountDownView extends MaterialButton {
|
||||
public class CallMeCountDownView extends androidx.appcompat.widget.AppCompatButton {
|
||||
|
||||
private long countDownToTime;
|
||||
@Nullable
|
||||
|
||||
@@ -22,7 +22,6 @@ import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.views.LearnMoreTextView
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
class DSLSettingsAdapter : MappingAdapter() {
|
||||
@@ -30,7 +29,6 @@ class DSLSettingsAdapter : MappingAdapter() {
|
||||
registerFactory(ClickPreference::class.java, LayoutFactory(::ClickPreferenceViewHolder, R.layout.dsl_preference_item))
|
||||
registerFactory(LongClickPreference::class.java, LayoutFactory(::LongClickPreferenceViewHolder, R.layout.dsl_preference_item))
|
||||
registerFactory(TextPreference::class.java, LayoutFactory(::TextPreferenceViewHolder, R.layout.dsl_preference_item))
|
||||
registerFactory(LearnMoreTextPreference::class.java, LayoutFactory(::LearnMoreTextPreferenceViewHolder, R.layout.dsl_learn_more_preference_item))
|
||||
registerFactory(RadioListPreference::class.java, LayoutFactory(::RadioListPreferenceViewHolder, R.layout.dsl_preference_item))
|
||||
registerFactory(MultiSelectListPreference::class.java, LayoutFactory(::MultiSelectListPreferenceViewHolder, R.layout.dsl_preference_item))
|
||||
registerFactory(ExternalLinkPreference::class.java, LayoutFactory(::ExternalLinkPreferenceViewHolder, R.layout.dsl_preference_item))
|
||||
@@ -93,14 +91,6 @@ abstract class PreferenceViewHolder<T : PreferenceModel<T>>(itemView: View) : Ma
|
||||
|
||||
class TextPreferenceViewHolder(itemView: View) : PreferenceViewHolder<TextPreference>(itemView)
|
||||
|
||||
class LearnMoreTextPreferenceViewHolder(itemView: View) : PreferenceViewHolder<LearnMoreTextPreference>(itemView) {
|
||||
override fun bind(model: LearnMoreTextPreference) {
|
||||
super.bind(model)
|
||||
(titleView as LearnMoreTextView).setOnLinkClickListener { model.onClick() }
|
||||
(summaryView as LearnMoreTextView).setOnLinkClickListener { model.onClick() }
|
||||
}
|
||||
}
|
||||
|
||||
class ClickPreferenceViewHolder(itemView: View) : PreferenceViewHolder<ClickPreference>(itemView) {
|
||||
override fun bind(model: ClickPreference) {
|
||||
super.bind(model)
|
||||
|
||||
@@ -15,7 +15,8 @@ import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
|
||||
import org.thoughtcrime.securesms.components.recyclerview.OnScrollAnimationHelper
|
||||
import org.thoughtcrime.securesms.components.recyclerview.ToolbarShadowAnimationHelper
|
||||
|
||||
abstract class DSLSettingsFragment(
|
||||
@StringRes private val titleId: Int = -1,
|
||||
@@ -27,16 +28,19 @@ abstract class DSLSettingsFragment(
|
||||
protected var recyclerView: RecyclerView? = null
|
||||
private set
|
||||
|
||||
private var scrollAnimationHelper: OnScrollAnimationHelper? = null
|
||||
|
||||
@CallSuper
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val toolbar: Toolbar? = view.findViewById(R.id.toolbar)
|
||||
val toolbarShadow: View? = view.findViewById(R.id.toolbar_shadow)
|
||||
|
||||
if (titleId != -1) {
|
||||
toolbar?.setTitle(titleId)
|
||||
}
|
||||
|
||||
toolbar?.setNavigationOnClickListener {
|
||||
onToolbarNavigationClicked()
|
||||
requireActivity().onBackPressed()
|
||||
}
|
||||
|
||||
if (menuId != -1) {
|
||||
@@ -44,6 +48,10 @@ abstract class DSLSettingsFragment(
|
||||
toolbar?.setOnMenuItemClickListener { onOptionsItemSelected(it) }
|
||||
}
|
||||
|
||||
if (toolbarShadow != null) {
|
||||
scrollAnimationHelper = getOnScrollAnimationHelper(toolbarShadow)
|
||||
}
|
||||
|
||||
val settingsAdapter = DSLSettingsAdapter()
|
||||
|
||||
recyclerView = view.findViewById<RecyclerView>(R.id.recycler).apply {
|
||||
@@ -51,29 +59,23 @@ abstract class DSLSettingsFragment(
|
||||
layoutManager = layoutManagerProducer(requireContext())
|
||||
adapter = settingsAdapter
|
||||
|
||||
getMaterial3OnScrollHelper(toolbar)?.let {
|
||||
it.attach(this)
|
||||
val helper = scrollAnimationHelper
|
||||
if (helper != null) {
|
||||
addOnScrollListener(helper)
|
||||
}
|
||||
}
|
||||
|
||||
bindAdapter(settingsAdapter)
|
||||
}
|
||||
|
||||
open fun getMaterial3OnScrollHelper(toolbar: Toolbar?): Material3OnScrollHelper? {
|
||||
if (toolbar == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return Material3OnScrollHelper(requireActivity(), toolbar)
|
||||
}
|
||||
|
||||
open fun onToolbarNavigationClicked() {
|
||||
requireActivity().onBackPressed()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
recyclerView = null
|
||||
scrollAnimationHelper = null
|
||||
}
|
||||
|
||||
protected open fun getOnScrollAnimationHelper(toolbarShadow: View): OnScrollAnimationHelper {
|
||||
return ToolbarShadowAnimationHelper(toolbarShadow)
|
||||
}
|
||||
|
||||
abstract fun bindAdapter(adapter: DSLSettingsAdapter)
|
||||
|
||||
@@ -65,7 +65,7 @@ sealed class DSLSettingsText {
|
||||
}
|
||||
}
|
||||
|
||||
object TitleLargeModifier : TextAppearanceModifier(R.style.Signal_Text_TitleLarge)
|
||||
object Title2BoldModifier : TextAppearanceModifier(R.style.TextAppearance_Signal_Title2_Bold)
|
||||
object Body1BoldModifier : TextAppearanceModifier(R.style.TextAppearance_Signal_Body1_Bold)
|
||||
|
||||
open class TextAppearanceModifier(@StyleRes private val textAppearance: Int) : Modifier {
|
||||
|
||||
@@ -101,7 +101,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences_chats__chats),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_chat_message_24),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_message_tinted_bitmap_24),
|
||||
onClick = {
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_chatsSettingsFragment)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.settings.app.account
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.text.InputType
|
||||
import android.util.DisplayMetrics
|
||||
@@ -41,7 +42,7 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == CreateKbsPinActivity.REQUEST_NEW_PIN && resultCode == CreateKbsPinActivity.RESULT_OK) {
|
||||
Snackbar.make(requireView(), R.string.ConfirmKbsPinFragment__pin_created, Snackbar.LENGTH_LONG).show()
|
||||
Snackbar.make(requireView(), R.string.ConfirmKbsPinFragment__pin_created, Snackbar.LENGTH_LONG).setTextColor(Color.WHITE).show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class AppearanceSettingsFragment : DSLSettingsFragment(R.string.preferences__app
|
||||
private val themeValues by lazy { resources.getStringArray(R.array.pref_theme_values) }
|
||||
|
||||
private val messageFontSizeLabels by lazy { resources.getStringArray(R.array.pref_message_font_size_entries) }
|
||||
private val messageFontSizeValues by lazy { resources.getIntArray(R.array.pref_message_font_size_values) }
|
||||
private val messageFontSizeValues by lazy { resources.getStringArray(R.array.pref_message_font_size_values) }
|
||||
|
||||
private val languageLabels by lazy { resources.getStringArray(R.array.language_entries) }
|
||||
private val languageValues by lazy { resources.getStringArray(R.array.language_values) }
|
||||
@@ -53,7 +53,7 @@ class AppearanceSettingsFragment : DSLSettingsFragment(R.string.preferences__app
|
||||
radioListPref(
|
||||
title = DSLSettingsText.from(R.string.preferences_chats__message_text_size),
|
||||
listItems = messageFontSizeLabels,
|
||||
selected = messageFontSizeValues.indexOf(state.messageFontSize),
|
||||
selected = messageFontSizeValues.indexOf(state.messageFontSize.toString()),
|
||||
onSelected = {
|
||||
viewModel.setMessageFontSize(messageFontSizeValues[it].toInt())
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.lock.PinHashing
|
||||
import org.thoughtcrime.securesms.registration.fragments.BaseRegistrationLockFragment
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel
|
||||
import org.thoughtcrime.securesms.util.CircularProgressButtonUtil.cancelSpinning
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.SupportEmailUtil
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
@@ -44,7 +45,7 @@ class ChangeNumberRegistrationLockFragment : BaseRegistrationLockFragment(R.layo
|
||||
override fun handleSuccessfulPinEntry(pin: String) {
|
||||
val pinsDiffer: Boolean = SignalStore.kbsValues().localPinHash?.let { !PinHashing.verifyLocalPinHash(it, pin) } ?: false
|
||||
|
||||
pinButton.cancelSpinning()
|
||||
cancelSpinning(pinButton)
|
||||
|
||||
if (pinsDiffer) {
|
||||
findNavController().safeNavigate(ChangeNumberRegistrationLockFragmentDirections.actionChangeNumberRegistrationLockToChangeNumberPinDiffers())
|
||||
|
||||
@@ -43,9 +43,9 @@ class InternalSettingsRepository(context: Context) {
|
||||
body = body,
|
||||
threadId = threadId,
|
||||
messageRanges = bodyRangeList.build(),
|
||||
image = "/static/release-notes/signal.png",
|
||||
imageWidth = 1800,
|
||||
imageHeight = 720
|
||||
image = "https://via.placeholder.com/720x480",
|
||||
imageWidth = 720,
|
||||
imageHeight = 480
|
||||
)
|
||||
|
||||
SignalDatabase.sms.insertBoostRequestMessage(recipientId, threadId)
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.notifications.profiles
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.dd.CircularProgressButton
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -19,7 +22,6 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton
|
||||
|
||||
/**
|
||||
* Show and allow addition of recipients to a profile during the create flow.
|
||||
@@ -35,7 +37,7 @@ class AddAllowedMembersFragment : DSLSettingsFragment(layoutId = R.layout.fragme
|
||||
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner.lifecycle)
|
||||
|
||||
view.findViewById<CircularProgressMaterialButton>(R.id.add_allowed_members_profile_next).apply {
|
||||
view.findViewById<CircularProgressButton>(R.id.add_allowed_members_profile_next).apply {
|
||||
setOnClickListener {
|
||||
findNavController().safeNavigate(AddAllowedMembersFragmentDirections.actionAddAllowedMembersFragmentToEditNotificationProfileScheduleFragment(profileId, true))
|
||||
}
|
||||
@@ -85,6 +87,8 @@ class AddAllowedMembersFragment : DSLSettingsFragment(layoutId = R.layout.fragme
|
||||
view?.let { view ->
|
||||
Snackbar.make(view, getString(R.string.NotificationProfileDetails__s_removed, removed.getDisplayName(requireContext())), Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.NotificationProfileDetails__undo) { undoRemove(id) }
|
||||
.setActionTextColor(ContextCompat.getColor(requireContext(), R.color.core_ultramarine_light))
|
||||
.setTextColor(Color.WHITE)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.dd.CircularProgressButton
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.BreakIteratorCompat
|
||||
@@ -23,11 +24,11 @@ import org.thoughtcrime.securesms.components.settings.app.notifications.profiles
|
||||
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.models.NotificationProfileNamePreset
|
||||
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
import org.thoughtcrime.securesms.util.CircularProgressButtonUtil
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged
|
||||
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton
|
||||
|
||||
/**
|
||||
* Dual use Edit/Create notification profile fragment. Use to create in the create profile flow,
|
||||
@@ -57,7 +58,7 @@ class EditNotificationProfileFragment : DSLSettingsFragment(layoutId = R.layout.
|
||||
|
||||
val title: TextView = view.findViewById(R.id.edit_notification_profile_title)
|
||||
val countView: TextView = view.findViewById(R.id.edit_notification_profile_count)
|
||||
val saveButton: CircularProgressMaterialButton = view.findViewById(R.id.edit_notification_profile_save)
|
||||
val saveButton: CircularProgressButton = view.findViewById(R.id.edit_notification_profile_save)
|
||||
val emojiView: ImageView = view.findViewById(R.id.edit_notification_profile_emoji)
|
||||
val nameView: EditText = view.findViewById(R.id.edit_notification_profile_name)
|
||||
val nameTextWrapper: TextInputLayout = view.findViewById(R.id.edit_notification_profile_name_wrapper)
|
||||
@@ -89,8 +90,8 @@ class EditNotificationProfileFragment : DSLSettingsFragment(layoutId = R.layout.
|
||||
}
|
||||
|
||||
lifecycleDisposable += viewModel.save(nameView.text.toString())
|
||||
.doOnSubscribe { saveButton.setSpinning() }
|
||||
.doAfterTerminate { saveButton.cancelSpinning() }
|
||||
.doOnSubscribe { CircularProgressButtonUtil.setSpinning(saveButton) }
|
||||
.doAfterTerminate { CircularProgressButtonUtil.cancelSpinning(saveButton) }
|
||||
.subscribeBy(
|
||||
onSuccess = { saveResult ->
|
||||
when (saveResult) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.dd.CircularProgressButton
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||
import com.google.android.material.timepicker.MaterialTimePicker
|
||||
import com.google.android.material.timepicker.TimeFormat
|
||||
@@ -27,7 +28,6 @@ import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.formatHours
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.orderOfDaysInWeek
|
||||
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import java.time.DayOfWeek
|
||||
import java.time.LocalTime
|
||||
@@ -75,7 +75,7 @@ class EditNotificationProfileScheduleFragment : LoggingFragment(R.layout.fragmen
|
||||
val startTime: TextView = view.findViewById(R.id.edit_notification_profile_schedule_start_time)
|
||||
val endTime: TextView = view.findViewById(R.id.edit_notification_profile_schedule_end_time)
|
||||
|
||||
val next: CircularProgressMaterialButton = view.findViewById(R.id.edit_notification_profile_schedule__next)
|
||||
val next: CircularProgressButton = view.findViewById(R.id.edit_notification_profile_schedule__next)
|
||||
next.setOnClickListener {
|
||||
lifecycleDisposable += viewModel.save(createMode)
|
||||
.subscribeBy(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.notifications.profiles
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
@@ -146,6 +147,8 @@ class NotificationProfileDetailsFragment : DSLSettingsFragment() {
|
||||
view?.let { view ->
|
||||
Snackbar.make(view, getString(R.string.NotificationProfileDetails__s_removed, removed.getDisplayName(requireContext())), Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.NotificationProfileDetails__undo) { undoRemove(id) }
|
||||
.setActionTextColor(ContextCompat.getColor(requireContext(), R.color.core_ultramarine_light))
|
||||
.setTextColor(Color.WHITE)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.dd.CircularProgressButton
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.thoughtcrime.securesms.ContactSelectionListFragment
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
@@ -16,10 +17,10 @@ import org.thoughtcrime.securesms.components.ContactFilterView
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.CircularProgressButtonUtil
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton
|
||||
import java.util.Optional
|
||||
import java.util.function.Consumer
|
||||
|
||||
@@ -31,7 +32,7 @@ class SelectRecipientsFragment : LoggingFragment(), ContactSelectionListFragment
|
||||
private val viewModel: SelectRecipientsViewModel by viewModels(factoryProducer = this::createFactory)
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
private var addToProfile: CircularProgressMaterialButton? = null
|
||||
private var addToProfile: CircularProgressButton? = null
|
||||
|
||||
private fun createFactory(): ViewModelProvider.Factory {
|
||||
val args = SelectRecipientsFragmentArgs.fromBundle(requireArguments())
|
||||
@@ -85,8 +86,8 @@ class SelectRecipientsFragment : LoggingFragment(), ContactSelectionListFragment
|
||||
addToProfile = view.findViewById(R.id.select_recipients_add)
|
||||
addToProfile?.setOnClickListener {
|
||||
lifecycleDisposable += viewModel.updateAllowedMembers()
|
||||
.doOnSubscribe { addToProfile?.setSpinning() }
|
||||
.doOnTerminate { addToProfile?.cancelSpinning() }
|
||||
.doOnSubscribe { CircularProgressButtonUtil.setSpinning(addToProfile) }
|
||||
.doOnTerminate { CircularProgressButtonUtil.cancelSpinning(addToProfile) }
|
||||
.subscribeBy(onSuccess = { findNavController().navigateUp() })
|
||||
}
|
||||
|
||||
|
||||
@@ -23,8 +23,9 @@ class CustomExpireTimerSelectDialog : DialogFragment() {
|
||||
val dialogView: View = LayoutInflater.from(context).inflate(R.layout.custom_expire_timer_select_dialog, null, false)
|
||||
selector = dialogView.findViewById(R.id.custom_expire_timer_select_dialog_selector)
|
||||
|
||||
return MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.ExpireTimerSettingsFragment__custom_time)
|
||||
val builder = MaterialAlertDialogBuilder(requireContext(), R.style.Signal_ThemeOverlay_Dialog_Rounded)
|
||||
|
||||
return builder.setTitle(R.string.ExpireTimerSettingsFragment__custom_time)
|
||||
.setView(dialogView)
|
||||
.setPositiveButton(R.string.ExpireTimerSettingsFragment__set) { _, _ ->
|
||||
viewModel.select(selector.getTimer())
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.widget.Toast
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.dd.CircularProgressButton
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
@@ -21,7 +22,6 @@ import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.livedata.ProcessState
|
||||
import org.thoughtcrime.securesms.util.livedata.distinctUntilChanged
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton
|
||||
|
||||
/**
|
||||
* Depending on the arguments, can be used to set the universal expire timer, set expire timer
|
||||
@@ -32,7 +32,7 @@ class ExpireTimerSettingsFragment : DSLSettingsFragment(
|
||||
layoutId = R.layout.expire_timer_settings_fragment
|
||||
) {
|
||||
|
||||
private lateinit var save: CircularProgressMaterialButton
|
||||
private lateinit var save: CircularProgressButton
|
||||
private lateinit var viewModel: ExpireTimerSettingsViewModel
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
@@ -62,7 +62,9 @@ class ExpireTimerSettingsFragment : DSLSettingsFragment(
|
||||
viewModel.state.distinctUntilChanged(ExpireTimerSettingsState::saveState).observe(viewLifecycleOwner) { state ->
|
||||
when (val saveState: ProcessState<Int> = state.saveState) {
|
||||
is ProcessState.Working -> {
|
||||
save.setSpinning()
|
||||
save.isClickable = false
|
||||
save.isIndeterminateProgressMode = true
|
||||
save.progress = 50
|
||||
}
|
||||
is ProcessState.Success -> {
|
||||
if (state.isGroupCreate) {
|
||||
@@ -77,7 +79,9 @@ class ExpireTimerSettingsFragment : DSLSettingsFragment(
|
||||
viewModel.resetError()
|
||||
}
|
||||
else -> {
|
||||
save.cancelSpinning()
|
||||
save.isClickable = true
|
||||
save.isIndeterminateProgressMode = false
|
||||
save.progress = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ class BoostFragment : DSLSettingsBottomSheetFragment(
|
||||
sectionHeaderPref(
|
||||
title = DSLSettingsText.from(
|
||||
R.string.BoostFragment__give_signal_a_boost,
|
||||
DSLSettingsText.CenterModifier, DSLSettingsText.TitleLargeModifier
|
||||
DSLSettingsText.CenterModifier, DSLSettingsText.Title2BoldModifier
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ class ManageDonationsFragment : DSLSettingsFragment(), ExpiredGiftSheet.Callback
|
||||
sectionHeaderPref(
|
||||
title = DSLSettingsText.from(
|
||||
R.string.SubscribeFragment__signal_is_powered_by_people_like_you,
|
||||
DSLSettingsText.CenterModifier, DSLSettingsText.TitleLargeModifier
|
||||
DSLSettingsText.CenterModifier, DSLSettingsText.Title2BoldModifier
|
||||
)
|
||||
)
|
||||
|
||||
@@ -215,7 +215,7 @@ class ManageDonationsFragment : DSLSettingsFragment(), ExpiredGiftSheet.Callback
|
||||
|
||||
space(DimensionUnit.DP.toPixels(16f).toInt())
|
||||
|
||||
tonalButton(
|
||||
primaryButton(
|
||||
text = DSLSettingsText.from(R.string.ManageDonationsFragment__make_a_monthly_donation),
|
||||
onClick = {
|
||||
findNavController().safeNavigate(R.id.action_manageDonationsFragment_to_subscribeFragment)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.subscribe
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.graphics.Color
|
||||
import android.text.SpannableStringBuilder
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -137,7 +138,7 @@ class SubscribeFragment : DSLSettingsFragment(
|
||||
sectionHeaderPref(
|
||||
title = DSLSettingsText.from(
|
||||
R.string.SubscribeFragment__signal_is_powered_by_people_like_you,
|
||||
DSLSettingsText.CenterModifier, DSLSettingsText.TitleLargeModifier
|
||||
DSLSettingsText.CenterModifier, DSLSettingsText.Title2BoldModifier
|
||||
)
|
||||
)
|
||||
|
||||
@@ -299,7 +300,9 @@ class SubscribeFragment : DSLSettingsFragment(
|
||||
}
|
||||
|
||||
private fun onSubscriptionCancelled() {
|
||||
Snackbar.make(requireView(), R.string.SubscribeFragment__your_subscription_has_been_cancelled, Snackbar.LENGTH_LONG).show()
|
||||
Snackbar.make(requireView(), R.string.SubscribeFragment__your_subscription_has_been_cancelled, Snackbar.LENGTH_LONG)
|
||||
.setTextColor(Color.WHITE)
|
||||
.show()
|
||||
|
||||
requireActivity().finish()
|
||||
requireActivity().startActivity(AppSettingsActivity.home(requireContext()))
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.settings.conversation
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.graphics.Rect
|
||||
@@ -23,7 +24,6 @@ import app.cash.exhaustive.Exhaustive
|
||||
import com.google.android.flexbox.FlexboxLayoutManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.thoughtcrime.securesms.AvatarPreviewActivity
|
||||
import org.thoughtcrime.securesms.BlockUnblockDialog
|
||||
import org.thoughtcrime.securesms.InviteActivity
|
||||
@@ -38,6 +38,7 @@ import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.view.ViewBadgeBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.components.recyclerview.OnScrollAnimationHelper
|
||||
import org.thoughtcrime.securesms.components.recyclerview.ToolbarShadowAnimationHelper
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
@@ -82,7 +83,7 @@ import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.ContextUtil
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog
|
||||
@@ -160,8 +161,6 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
}
|
||||
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
recyclerView?.addOnScrollListener(ConversationSettingsOnUserScrolledAnimationHelper(toolbarAvatarContainer, toolbarTitle, toolbarBackground))
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
@@ -180,6 +179,10 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getOnScrollAnimationHelper(toolbarShadow: View): OnScrollAnimationHelper {
|
||||
return ConversationSettingsOnUserScrolledAnimationHelper(toolbarAvatarContainer, toolbarTitle, toolbarBackground, toolbarShadow)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return if (item.itemId == R.id.action_edit) {
|
||||
val args = ConversationSettingsFragmentArgs.fromBundle(requireArguments())
|
||||
@@ -192,15 +195,6 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMaterial3OnScrollHelper(toolbar: Toolbar?): Material3OnScrollHelper {
|
||||
return object : Material3OnScrollHelper(requireActivity(), toolbar!!) {
|
||||
override val inactiveColorSet = ColorSet(
|
||||
toolbarColorRes = R.color.signal_colorBackground_0,
|
||||
statusBarColorRes = R.color.signal_colorBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
val args = ConversationSettingsFragmentArgs.fromBundle(requireArguments())
|
||||
|
||||
@@ -235,7 +229,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
}
|
||||
|
||||
state.withRecipientSettingsState {
|
||||
toolbarTitle.text = if (state.recipient.isSelf) getString(R.string.note_to_self) else state.recipient.getDisplayName(requireContext())
|
||||
toolbarTitle.text = state.recipient.getDisplayName(requireContext())
|
||||
}
|
||||
|
||||
state.withGroupSettingsState {
|
||||
@@ -773,18 +767,19 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
showMembersAdded.membersAddedCount
|
||||
)
|
||||
|
||||
Snackbar.make(requireView(), string, Snackbar.LENGTH_SHORT).show()
|
||||
Snackbar.make(requireView(), string, Snackbar.LENGTH_SHORT).setTextColor(Color.WHITE).show()
|
||||
}
|
||||
|
||||
private class ConversationSettingsOnUserScrolledAnimationHelper(
|
||||
private val toolbarAvatar: View,
|
||||
private val toolbarTitle: View,
|
||||
private val toolbarBackground: View
|
||||
) : OnScrollAnimationHelper() {
|
||||
private val toolbarBackground: View,
|
||||
toolbarShadow: View
|
||||
) : ToolbarShadowAnimationHelper(toolbarShadow) {
|
||||
|
||||
override val duration: Long = 200L
|
||||
|
||||
private val actionBarSize = DimensionUnit.DP.toPixels(64f)
|
||||
private val actionBarSize = ThemeUtil.getThemedDimen(toolbarShadow.context, R.attr.actionBarSize)
|
||||
private val rect = Rect()
|
||||
|
||||
override fun getAnimationState(recyclerView: RecyclerView): AnimationState {
|
||||
@@ -810,6 +805,8 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
}
|
||||
|
||||
override fun show(duration: Long) {
|
||||
super.show(duration)
|
||||
|
||||
toolbarAvatar
|
||||
.animate()
|
||||
.setDuration(duration)
|
||||
@@ -829,6 +826,8 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
}
|
||||
|
||||
override fun hide(duration: Long) {
|
||||
super.hide(duration)
|
||||
|
||||
toolbarAvatar
|
||||
.animate()
|
||||
.setDuration(duration)
|
||||
|
||||
@@ -158,15 +158,6 @@ class DSLConfiguration {
|
||||
children.add(preference)
|
||||
}
|
||||
|
||||
fun tonalButton(
|
||||
text: DSLSettingsText,
|
||||
isEnabled: Boolean = true,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val preference = Button.Model.Tonal(text, null, isEnabled, onClick)
|
||||
children.add(preference)
|
||||
}
|
||||
|
||||
fun secondaryButtonNoOutline(
|
||||
text: DSLSettingsText,
|
||||
icon: DSLSettingsIcon? = null,
|
||||
@@ -185,15 +176,6 @@ class DSLConfiguration {
|
||||
children.add(preference)
|
||||
}
|
||||
|
||||
fun learnMoreTextPref(
|
||||
title: DSLSettingsText? = null,
|
||||
summary: DSLSettingsText? = null,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val preference = LearnMoreTextPreference(title, summary, onClick)
|
||||
children.add(preference)
|
||||
}
|
||||
|
||||
fun toMappingModelList(): MappingModelList = MappingModelList().apply { addAll(children) }
|
||||
}
|
||||
|
||||
@@ -227,12 +209,6 @@ class TextPreference(
|
||||
summary: DSLSettingsText?
|
||||
) : PreferenceModel<TextPreference>(title = title, summary = summary)
|
||||
|
||||
class LearnMoreTextPreference(
|
||||
override val title: DSLSettingsText?,
|
||||
override val summary: DSLSettingsText?,
|
||||
val onClick: () -> Unit
|
||||
) : PreferenceModel<LearnMoreTextPreference>()
|
||||
|
||||
class DividerPreference : PreferenceModel<DividerPreference>() {
|
||||
override fun areItemsTheSame(newItem: DividerPreference) = true
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ object Button {
|
||||
|
||||
fun register(mappingAdapter: MappingAdapter) {
|
||||
mappingAdapter.registerFactory(Model.Primary::class.java, LayoutFactory({ ViewHolder(it) }, R.layout.dsl_button_primary))
|
||||
mappingAdapter.registerFactory(Model.Tonal::class.java, LayoutFactory({ ViewHolder(it) }, R.layout.dsl_button_tonal))
|
||||
mappingAdapter.registerFactory(Model.SecondaryNoOutline::class.java, LayoutFactory({ ViewHolder(it) }, R.layout.dsl_button_secondary))
|
||||
}
|
||||
|
||||
@@ -35,13 +34,6 @@ object Button {
|
||||
onClick: () -> Unit
|
||||
) : Model<Primary>(title, icon, isEnabled, onClick)
|
||||
|
||||
class Tonal(
|
||||
title: DSLSettingsText?,
|
||||
icon: DSLSettingsIcon?,
|
||||
isEnabled: Boolean,
|
||||
onClick: () -> Unit
|
||||
) : Model<Tonal>(title, icon, isEnabled, onClick)
|
||||
|
||||
class SecondaryNoOutline(
|
||||
title: DSLSettingsText?,
|
||||
icon: DSLSettingsIcon?,
|
||||
@@ -52,7 +44,7 @@ object Button {
|
||||
|
||||
class ViewHolder<T : Model<T>>(itemView: View) : MappingViewHolder<T>(itemView) {
|
||||
|
||||
private val button: MaterialButton = itemView.findViewById(R.id.button)
|
||||
private val button: MaterialButton = itemView as MaterialButton
|
||||
|
||||
override fun bind(model: T) {
|
||||
button.text = model.title?.resolve(context)
|
||||
|
||||
@@ -175,8 +175,7 @@ class VoiceNoteMediaItemFactory {
|
||||
sender.getDisplayName(context),
|
||||
threadRecipient.getDisplayName(context));
|
||||
} else if (preference.isDisplayContact()) {
|
||||
return sender.isSelf() ? context.getString(R.string.note_to_self)
|
||||
: sender.getDisplayName(context);
|
||||
return sender.getDisplayName(context);
|
||||
} else {
|
||||
return context.getString(R.string.MessageNotifier_signal_message);
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ class VoiceNotePlayerView @JvmOverloads constructor(
|
||||
playPauseToggleView.addValueCallback(
|
||||
KeyPath("**"),
|
||||
LottieProperty.COLOR_FILTER,
|
||||
LottieValueCallback(SimpleColorFilter(ContextCompat.getColor(context, R.color.signal_colorOnSurface)))
|
||||
LottieValueCallback(SimpleColorFilter(ContextCompat.getColor(context, R.color.signal_icon_tint_primary)))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,11 @@ import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.AppCompatImageView;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -114,7 +113,7 @@ public class WebRtcAudioOutputToggleButton extends AppCompatImageView {
|
||||
rv.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false));
|
||||
rv.setAdapter(adapter);
|
||||
|
||||
picker = new MaterialAlertDialogBuilder(getContext())
|
||||
picker = new AlertDialog.Builder(getContext(), R.style.Theme_Signal_AlertDialog_Dark_Cornered)
|
||||
.setTitle(R.string.WebRtcAudioOutputToggle__audio_output)
|
||||
.setView(rv)
|
||||
.setCancelable(true)
|
||||
|
||||
@@ -61,8 +61,8 @@ public final class ContactChip extends Chip {
|
||||
.placeholder(fallbackContactPhotoDrawable)
|
||||
.fallback(fallbackContactPhotoDrawable)
|
||||
.error(fallbackContactPhotoDrawable)
|
||||
.circleCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.circleCrop()
|
||||
.into(new CustomTarget<Drawable>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
package org.thoughtcrime.securesms.contacts
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.BackpressureStrategy
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
|
||||
/**
|
||||
* ViewModel expressly for displaying the current state of the contact chips
|
||||
* in the contact selection fragment.
|
||||
*/
|
||||
class ContactChipViewModel : ViewModel() {
|
||||
|
||||
private val store = RxStore(emptyList<SelectedContacts.Model>())
|
||||
|
||||
val state: Flowable<List<SelectedContacts.Model>> = store.stateFlowable
|
||||
.distinctUntilChanged()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
val count = store.state.size
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
private val disposableMap: MutableMap<RecipientId, Disposable> = mutableMapOf()
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
disposableMap.values.forEach { it.dispose() }
|
||||
disposableMap.clear()
|
||||
}
|
||||
|
||||
fun add(selectedContact: SelectedContact) {
|
||||
disposables += getOrCreateRecipientId(selectedContact).map { Recipient.resolved(it) }.observeOn(Schedulers.io()).subscribe { recipient ->
|
||||
store.update { it + SelectedContacts.Model(selectedContact, recipient) }
|
||||
disposableMap[recipient.id]?.dispose()
|
||||
disposableMap[recipient.id] = store.update(recipient.live().asObservable().toFlowable(BackpressureStrategy.LATEST)) { changedRecipient, state ->
|
||||
val index = state.indexOfFirst { it.selectedContact.matches(selectedContact) }
|
||||
when {
|
||||
index == 0 -> {
|
||||
listOf(SelectedContacts.Model(selectedContact, changedRecipient)) + state.drop(index + 1)
|
||||
}
|
||||
index > 0 -> {
|
||||
state.take(index) + SelectedContacts.Model(selectedContact, changedRecipient) + state.drop(index + 1)
|
||||
}
|
||||
else -> {
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(selectedContact: SelectedContact) {
|
||||
store.update { list ->
|
||||
list.filterNot { it.selectedContact.matches(selectedContact) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getOrCreateRecipientId(selectedContact: SelectedContact): Single<RecipientId> {
|
||||
return Single.fromCallable {
|
||||
selectedContact.getOrCreateRecipientId(ApplicationDependencies.getApplication())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,6 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
||||
private final ItemClickListener clickListener;
|
||||
private final GlideRequests glideRequests;
|
||||
private final Set<RecipientId> currentContacts;
|
||||
private final int checkboxResource;
|
||||
|
||||
private final SelectedContactSet selectedContacts = new SelectedContactSet();
|
||||
|
||||
@@ -206,16 +205,14 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
||||
@Nullable Cursor cursor,
|
||||
@Nullable ItemClickListener clickListener,
|
||||
boolean multiSelect,
|
||||
@NonNull Set<RecipientId> currentContacts,
|
||||
int checkboxResource)
|
||||
@NonNull Set<RecipientId> currentContacts)
|
||||
{
|
||||
super(context, cursor);
|
||||
this.layoutInflater = LayoutInflater.from(context);
|
||||
this.glideRequests = glideRequests;
|
||||
this.multiSelect = multiSelect;
|
||||
this.clickListener = clickListener;
|
||||
this.currentContacts = currentContacts;
|
||||
this.checkboxResource = checkboxResource;
|
||||
this.layoutInflater = LayoutInflater.from(context);
|
||||
this.glideRequests = glideRequests;
|
||||
this.multiSelect = multiSelect;
|
||||
this.clickListener = clickListener;
|
||||
this.currentContacts = currentContacts;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -232,9 +229,7 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
||||
@Override
|
||||
public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
|
||||
if (viewType == VIEW_TYPE_CONTACT) {
|
||||
View view = layoutInflater.inflate(R.layout.contact_selection_list_item, parent, false);
|
||||
view.findViewById(R.id.check_box).setBackgroundResource(checkboxResource);
|
||||
return new ContactViewHolder(view, clickListener);
|
||||
return new ContactViewHolder(layoutInflater.inflate(R.layout.contact_selection_list_item, parent, false), clickListener);
|
||||
} else {
|
||||
return new DividerViewHolder(layoutInflater.inflate(R.layout.contact_selection_list_divider, parent, false));
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.signal.core.util.SetUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
@@ -70,19 +69,13 @@ public class ContactsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
} else if (unknownSystemE164s.size() > 0) {
|
||||
Log.i(TAG, "There are " + unknownSystemE164s.size() + " unknown contacts. Doing an individual sync.");
|
||||
List<Recipient> recipients = Stream.of(unknownSystemE164s)
|
||||
.filter(s -> s.startsWith("+"))
|
||||
.map(s -> Recipient.external(getContext(), s))
|
||||
.toList();
|
||||
|
||||
Log.i(TAG, "There are " + unknownSystemE164s.size() + " unknown E164s, which are now " + recipients.size() + " recipients. Only syncing these specific contacts.");
|
||||
|
||||
try {
|
||||
ContactDiscovery.refresh(context, recipients, true);
|
||||
|
||||
if (Util.isDefaultSmsProvider(context)) {
|
||||
ContactDiscovery.syncRecipientInfoWithSystemContacts(context);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to refresh! Scheduling for later.", e);
|
||||
ApplicationDependencies.getJobManager().add(new DirectoryRefreshJob(true));
|
||||
|
||||
@@ -28,7 +28,7 @@ class LetterHeaderDecoration(private val context: Context, private val hideDecor
|
||||
color = ContextCompat.getColor(context, R.color.signal_text_primary)
|
||||
isAntiAlias = true
|
||||
style = Paint.Style.FILL
|
||||
typeface = Typeface.create("sans-serif-medium", Typeface.NORMAL)
|
||||
typeface = Typeface.create("sans-serif-medium", Typeface.BOLD)
|
||||
textAlign = Paint.Align.LEFT
|
||||
textSize = ViewUtil.spToPx(16f).toFloat()
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
package org.thoughtcrime.securesms.contacts
|
||||
|
||||
import android.view.View
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
|
||||
object SelectedContacts {
|
||||
@JvmStatic
|
||||
fun register(adapter: MappingAdapter, onCloseIconClicked: (Model) -> Unit) {
|
||||
adapter.registerFactory(Model::class.java, LayoutFactory({ ViewHolder(it, onCloseIconClicked) }, R.layout.contact_selection_list_chip))
|
||||
}
|
||||
|
||||
class Model(val selectedContact: SelectedContact, val recipient: Recipient) : MappingModel<Model> {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return newItem.selectedContact.matches(selectedContact) && recipient == newItem.recipient
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||
return areItemsTheSame(newItem) && recipient.hasSameContent(newItem.recipient)
|
||||
}
|
||||
}
|
||||
|
||||
private class ViewHolder(itemView: View, private val onCloseIconClicked: (Model) -> Unit) : MappingViewHolder<Model>(itemView) {
|
||||
|
||||
private val chip: ContactChip = itemView.findViewById(R.id.contact_chip)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
chip.text = model.recipient.getShortDisplayName(context)
|
||||
chip.setContact(model.selectedContact)
|
||||
chip.isCloseIconVisible = true
|
||||
chip.setOnCloseIconClickListener {
|
||||
onCloseIconClicked(model)
|
||||
}
|
||||
chip.setAvatar(GlideApp.with(itemView), model.recipient, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package org.thoughtcrime.securesms.contacts.paged
|
||||
|
||||
/**
|
||||
* Number of active contacts for a given section, handed to the expand config.
|
||||
*/
|
||||
typealias ActiveContactCount = Int
|
||||
@@ -83,7 +83,7 @@ class ContactSearchConfiguration private constructor(
|
||||
*/
|
||||
data class ExpandConfig(
|
||||
val isExpanded: Boolean,
|
||||
val maxCountWhenNotExpanded: (ActiveContactCount) -> Int = { 2 }
|
||||
val maxCountWhenNotExpanded: Int = 2
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
package org.thoughtcrime.securesms.contacts.paged
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckBox
|
||||
import android.widget.TextView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.components.FromTextView
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem
|
||||
import org.thoughtcrime.securesms.components.menu.SignalContextMenu
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
@@ -18,9 +15,6 @@ import org.thoughtcrime.securesms.util.adapter.mapping.MappingModelList
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
private typealias StoryClickListener = (View, ContactSearchData.Story, Boolean) -> Unit
|
||||
private typealias RecipientClickListener = (View, ContactSearchData.KnownRecipient, Boolean) -> Unit
|
||||
|
||||
/**
|
||||
* Mapping Models and View Holders for ContactSearchData
|
||||
*/
|
||||
@@ -28,14 +22,13 @@ object ContactSearchItems {
|
||||
fun register(
|
||||
mappingAdapter: MappingAdapter,
|
||||
displayCheckBox: Boolean,
|
||||
recipientListener: RecipientClickListener,
|
||||
storyListener: StoryClickListener,
|
||||
storyContextMenuCallbacks: StoryContextMenuCallbacks,
|
||||
recipientListener: (ContactSearchData.KnownRecipient, Boolean) -> Unit,
|
||||
storyListener: (ContactSearchData.Story, Boolean) -> Unit,
|
||||
expandListener: (ContactSearchData.Expand) -> Unit
|
||||
) {
|
||||
mappingAdapter.registerFactory(
|
||||
StoryModel::class.java,
|
||||
LayoutFactory({ StoryViewHolder(it, displayCheckBox, storyListener, storyContextMenuCallbacks) }, R.layout.contact_search_item)
|
||||
LayoutFactory({ StoryViewHolder(it, displayCheckBox, storyListener) }, R.layout.contact_search_item)
|
||||
)
|
||||
mappingAdapter.registerFactory(
|
||||
RecipientModel::class.java,
|
||||
@@ -86,7 +79,7 @@ object ContactSearchItems {
|
||||
}
|
||||
}
|
||||
|
||||
private class StoryViewHolder(itemView: View, displayCheckBox: Boolean, onClick: StoryClickListener, private val storyContextMenuCallbacks: StoryContextMenuCallbacks) : BaseRecipientViewHolder<StoryModel, ContactSearchData.Story>(itemView, displayCheckBox, onClick) {
|
||||
private class StoryViewHolder(itemView: View, displayCheckBox: Boolean, onClick: (ContactSearchData.Story, Boolean) -> Unit) : BaseRecipientViewHolder<StoryModel, ContactSearchData.Story>(itemView, displayCheckBox, onClick) {
|
||||
override fun isSelected(model: StoryModel): Boolean = model.isSelected
|
||||
override fun getData(model: StoryModel): ContactSearchData.Story = model.story
|
||||
override fun getRecipient(model: StoryModel): Recipient = model.story.recipient
|
||||
@@ -108,50 +101,6 @@ object ContactSearchItems {
|
||||
|
||||
number.text = context.resources.getQuantityString(pluralId, count, count)
|
||||
}
|
||||
|
||||
override fun bindLongPress(model: StoryModel) {
|
||||
itemView.setOnLongClickListener {
|
||||
val actions: List<ActionItem> = when {
|
||||
model.story.recipient.isMyStory -> getMyStoryContextMenuActions(model)
|
||||
model.story.recipient.isGroup -> getGroupStoryContextMenuActions(model)
|
||||
model.story.recipient.isDistributionList -> getPrivateStoryContextMenuActions(model)
|
||||
else -> error("Unsupported story target. Not a group or distribution list.")
|
||||
}
|
||||
|
||||
SignalContextMenu.Builder(itemView, itemView.rootView as ViewGroup)
|
||||
.offsetX(context.resources.getDimensionPixelSize(R.dimen.dsl_settings_gutter))
|
||||
.show(actions)
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMyStoryContextMenuActions(model: StoryModel): List<ActionItem> {
|
||||
return listOf(
|
||||
ActionItem(R.drawable.ic_settings_24, context.getString(R.string.ContactSearchItems__story_settings)) {
|
||||
storyContextMenuCallbacks.onOpenStorySettings(model.story)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun getGroupStoryContextMenuActions(model: StoryModel): List<ActionItem> {
|
||||
return listOf(
|
||||
ActionItem(R.drawable.ic_minus_circle_20, context.getString(R.string.ContactSearchItems__remove_story)) {
|
||||
storyContextMenuCallbacks.onRemoveGroupStory(model.story, model.isSelected)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun getPrivateStoryContextMenuActions(model: StoryModel): List<ActionItem> {
|
||||
return listOf(
|
||||
ActionItem(R.drawable.ic_settings_24, context.getString(R.string.ContactSearchItems__story_settings)) {
|
||||
storyContextMenuCallbacks.onOpenStorySettings(model.story)
|
||||
},
|
||||
ActionItem(R.drawable.ic_delete_24, context.getString(R.string.ContactSearchItems__delete_story), R.color.signal_colorError) {
|
||||
storyContextMenuCallbacks.onDeletePrivateStory(model.story, model.isSelected)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,7 +125,7 @@ object ContactSearchItems {
|
||||
}
|
||||
}
|
||||
|
||||
private class KnownRecipientViewHolder(itemView: View, displayCheckBox: Boolean, onClick: RecipientClickListener) : BaseRecipientViewHolder<RecipientModel, ContactSearchData.KnownRecipient>(itemView, displayCheckBox, onClick) {
|
||||
private class KnownRecipientViewHolder(itemView: View, displayCheckBox: Boolean, onClick: (ContactSearchData.KnownRecipient, Boolean) -> Unit) : BaseRecipientViewHolder<RecipientModel, ContactSearchData.KnownRecipient>(itemView, displayCheckBox, onClick) {
|
||||
override fun isSelected(model: RecipientModel): Boolean = model.isSelected
|
||||
override fun getData(model: RecipientModel): ContactSearchData.KnownRecipient = model.knownRecipient
|
||||
override fun getRecipient(model: RecipientModel): Recipient = model.knownRecipient.recipient
|
||||
@@ -185,7 +134,7 @@ object ContactSearchItems {
|
||||
/**
|
||||
* Base Recipient View Holder
|
||||
*/
|
||||
private abstract class BaseRecipientViewHolder<T : MappingModel<T>, D : ContactSearchData>(itemView: View, private val displayCheckBox: Boolean, val onClick: (View, D, Boolean) -> Unit) : MappingViewHolder<T>(itemView) {
|
||||
private abstract class BaseRecipientViewHolder<T : MappingModel<T>, D : ContactSearchData>(itemView: View, private val displayCheckBox: Boolean, val onClick: (D, Boolean) -> Unit) : MappingViewHolder<T>(itemView) {
|
||||
|
||||
protected val avatar: AvatarImageView = itemView.findViewById(R.id.contact_photo_image)
|
||||
protected val badge: BadgeImageView = itemView.findViewById(R.id.contact_badge)
|
||||
@@ -198,8 +147,7 @@ object ContactSearchItems {
|
||||
override fun bind(model: T) {
|
||||
checkbox.visible = displayCheckBox
|
||||
checkbox.isChecked = isSelected(model)
|
||||
itemView.setOnClickListener { onClick(itemView, getData(model), isSelected(model)) }
|
||||
bindLongPress(model)
|
||||
itemView.setOnClickListener { onClick(getData(model), isSelected(model)) }
|
||||
|
||||
if (payload.isNotEmpty()) {
|
||||
return
|
||||
@@ -237,8 +185,6 @@ object ContactSearchItems {
|
||||
smsTag.visible = isSmsContact(model)
|
||||
}
|
||||
|
||||
protected open fun bindLongPress(model: T) = Unit
|
||||
|
||||
private fun isSmsContact(model: T): Boolean {
|
||||
return (getRecipient(model).isForceSmsSelection || getRecipient(model).isUnregistered) && !getRecipient(model).isDistributionList
|
||||
}
|
||||
@@ -322,10 +268,4 @@ object ContactSearchItems {
|
||||
return if (isLeftSelf == isRightSelf) 0 else if (isLeftSelf) 1 else -1
|
||||
}
|
||||
}
|
||||
|
||||
interface StoryContextMenuCallbacks {
|
||||
fun onOpenStorySettings(story: ContactSearchData.Story)
|
||||
fun onRemoveGroupStory(story: ContactSearchData.Story, isSelected: Boolean)
|
||||
fun onDeletePrivateStory(story: ContactSearchData.Story, isSelected: Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,24 @@
|
||||
package org.thoughtcrime.securesms.contacts.paged
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits
|
||||
import org.thoughtcrime.securesms.stories.settings.custom.PrivateStorySettingsFragment
|
||||
import org.thoughtcrime.securesms.stories.settings.my.MyStorySettingsFragment
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.PagingMappingAdapter
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
|
||||
|
||||
class ContactSearchMediator(
|
||||
private val fragment: Fragment,
|
||||
fragment: Fragment,
|
||||
recyclerView: RecyclerView,
|
||||
selectionLimits: SelectionLimits,
|
||||
displayCheckBox: Boolean,
|
||||
mapStateToConfiguration: (ContactSearchState) -> ContactSearchConfiguration,
|
||||
private val contactSelectionPreFilter: (View?, Set<ContactSearchKey>) -> Set<ContactSearchKey> = { _, s -> s }
|
||||
mapStateToConfiguration: (ContactSearchState) -> ContactSearchConfiguration
|
||||
) {
|
||||
|
||||
private val viewModel: ContactSearchViewModel = ViewModelProvider(fragment, ContactSearchViewModel.Factory(selectionLimits, ContactSearchRepository())).get(ContactSearchViewModel::class.java)
|
||||
|
||||
init {
|
||||
|
||||
val adapter = PagingMappingAdapter<ContactSearchKey>()
|
||||
recyclerView.adapter = adapter
|
||||
|
||||
@@ -36,7 +27,6 @@ class ContactSearchMediator(
|
||||
displayCheckBox = displayCheckBox,
|
||||
recipientListener = this::toggleSelection,
|
||||
storyListener = this::toggleSelection,
|
||||
storyContextMenuCallbacks = StoryContextMenuCallbacks(),
|
||||
expandListener = { viewModel.expandSection(it.sectionKey) }
|
||||
)
|
||||
|
||||
@@ -64,7 +54,7 @@ class ContactSearchMediator(
|
||||
}
|
||||
|
||||
fun setKeysSelected(keys: Set<ContactSearchKey>) {
|
||||
viewModel.setKeysSelected(contactSelectionPreFilter(null, keys))
|
||||
viewModel.setKeysSelected(keys)
|
||||
}
|
||||
|
||||
fun setKeysNotSelected(keys: Set<ContactSearchKey>) {
|
||||
@@ -83,45 +73,11 @@ class ContactSearchMediator(
|
||||
viewModel.addToVisibleGroupStories(groupStories)
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
viewModel.refresh()
|
||||
}
|
||||
|
||||
private fun toggleSelection(view: View, contactSearchData: ContactSearchData, isSelected: Boolean) {
|
||||
return if (isSelected) {
|
||||
private fun toggleSelection(contactSearchData: ContactSearchData, isSelected: Boolean) {
|
||||
if (isSelected) {
|
||||
viewModel.setKeysNotSelected(setOf(contactSearchData.contactSearchKey))
|
||||
} else {
|
||||
viewModel.setKeysSelected(contactSelectionPreFilter(view, setOf(contactSearchData.contactSearchKey)))
|
||||
}
|
||||
}
|
||||
|
||||
private inner class StoryContextMenuCallbacks : ContactSearchItems.StoryContextMenuCallbacks {
|
||||
override fun onOpenStorySettings(story: ContactSearchData.Story) {
|
||||
if (story.recipient.isMyStory) {
|
||||
MyStorySettingsFragment.createAsDialog()
|
||||
.show(fragment.childFragmentManager, null)
|
||||
} else {
|
||||
PrivateStorySettingsFragment.createAsDialog(story.recipient.requireDistributionListId())
|
||||
.show(fragment.childFragmentManager, null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRemoveGroupStory(story: ContactSearchData.Story, isSelected: Boolean) {
|
||||
MaterialAlertDialogBuilder(fragment.requireContext())
|
||||
.setTitle(R.string.ContactSearchMediator__remove_group_story)
|
||||
.setMessage(R.string.ContactSearchMediator__this_will_remove)
|
||||
.setPositiveButton(R.string.ContactSearchMediator__remove) { _, _ -> viewModel.removeGroupStory(story) }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onDeletePrivateStory(story: ContactSearchData.Story, isSelected: Boolean) {
|
||||
MaterialAlertDialogBuilder(fragment.requireContext())
|
||||
.setTitle(R.string.ContactSearchMediator__delete_story)
|
||||
.setMessage(fragment.getString(R.string.ContactSearchMediator__delete_the_private, story.recipient.getDisplayName(fragment.requireContext())))
|
||||
.setPositiveButton(SpanUtil.color(ContextCompat.getColor(fragment.requireContext(), R.color.signal_colorError), fragment.getString(R.string.ContactSearchMediator__delete))) { _, _ -> viewModel.deletePrivateStory(story) }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
viewModel.setKeysSelected(setOf(contactSearchData.contactSearchKey))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ package org.thoughtcrime.securesms.contacts.paged
|
||||
import android.database.Cursor
|
||||
import org.signal.paging.PagedDataSource
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.StorySend
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
@@ -15,14 +13,6 @@ class ContactSearchPagedDataSource(
|
||||
private val contactSearchPagedDataSourceRepository: ContactSearchPagedDataSourceRepository = ContactSearchPagedDataSourceRepository(ApplicationDependencies.getApplication())
|
||||
) : PagedDataSource<ContactSearchKey, ContactSearchData> {
|
||||
|
||||
companion object {
|
||||
private val ACTIVE_STORY_CUTOFF_DURATION = TimeUnit.DAYS.toMillis(1)
|
||||
}
|
||||
|
||||
private val latestStorySends: List<StorySend> = contactSearchPagedDataSourceRepository.getLatestStorySends(ACTIVE_STORY_CUTOFF_DURATION)
|
||||
|
||||
private val activeStoryCount = latestStorySends.size
|
||||
|
||||
override fun size(): Int {
|
||||
return contactConfiguration.sections.sumOf {
|
||||
getSectionSize(it, contactConfiguration.query)
|
||||
@@ -77,25 +67,26 @@ class ContactSearchPagedDataSource(
|
||||
}
|
||||
|
||||
private fun getSectionSize(section: ContactSearchConfiguration.Section, query: String?): Int {
|
||||
when (section) {
|
||||
val cursor: Cursor = when (section) {
|
||||
is ContactSearchConfiguration.Section.Individuals -> getNonGroupContactsCursor(section, query)
|
||||
is ContactSearchConfiguration.Section.Groups -> contactSearchPagedDataSourceRepository.getGroupContacts(section, query)
|
||||
is ContactSearchConfiguration.Section.Recents -> getRecentsCursor(section, query)
|
||||
is ContactSearchConfiguration.Section.Stories -> getStoriesCursor(query)
|
||||
}!!.use { cursor ->
|
||||
val extras: List<ContactSearchData> = when (section) {
|
||||
is ContactSearchConfiguration.Section.Stories -> getFilteredGroupStories(section, query)
|
||||
else -> emptyList()
|
||||
}
|
||||
}!!
|
||||
|
||||
val collection = createResultsCollection(
|
||||
section = section,
|
||||
cursor = cursor,
|
||||
extraData = extras,
|
||||
cursorMapper = { error("Unsupported") }
|
||||
)
|
||||
return collection.getSize()
|
||||
val extras: List<ContactSearchData> = when (section) {
|
||||
is ContactSearchConfiguration.Section.Stories -> getFilteredGroupStories(section, query)
|
||||
else -> emptyList()
|
||||
}
|
||||
|
||||
val collection = ResultsCollection(
|
||||
section = section,
|
||||
cursor = cursor,
|
||||
extraData = extras,
|
||||
cursorMapper = { error("Unsupported") }
|
||||
)
|
||||
|
||||
return collection.getSize()
|
||||
}
|
||||
|
||||
private fun getFilteredGroupStories(section: ContactSearchConfiguration.Section.Stories, query: String?): List<ContactSearchData> {
|
||||
@@ -142,7 +133,7 @@ class ContactSearchPagedDataSource(
|
||||
): List<ContactSearchData> {
|
||||
val results = mutableListOf<ContactSearchData>()
|
||||
|
||||
val collection = createResultsCollection(section, cursor, extraData, cursorRowToData)
|
||||
val collection = ResultsCollection(section, cursor, extraData, cursorRowToData)
|
||||
results.addAll(collection.getSublist(startIndex, endIndex))
|
||||
|
||||
return results
|
||||
@@ -210,27 +201,14 @@ class ContactSearchPagedDataSource(
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
private fun createResultsCollection(
|
||||
section: ContactSearchConfiguration.Section,
|
||||
cursor: Cursor,
|
||||
extraData: List<ContactSearchData>,
|
||||
cursorMapper: (Cursor) -> ContactSearchData
|
||||
): ResultsCollection {
|
||||
return when (section) {
|
||||
is ContactSearchConfiguration.Section.Stories -> StoriesCollection(section, cursor, extraData, cursorMapper, activeStoryCount, StoryComparator(latestStorySends))
|
||||
else -> ResultsCollection(section, cursor, extraData, cursorMapper, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We assume that the collection is [cursor contents] + [extraData contents]
|
||||
*/
|
||||
private open class ResultsCollection(
|
||||
private data class ResultsCollection(
|
||||
val section: ContactSearchConfiguration.Section,
|
||||
val cursor: Cursor,
|
||||
val extraData: List<ContactSearchData>,
|
||||
val cursorMapper: (Cursor) -> ContactSearchData,
|
||||
val activeContactCount: Int
|
||||
val cursorMapper: (Cursor) -> ContactSearchData
|
||||
) {
|
||||
|
||||
private val contentSize = cursor.count + extraData.count()
|
||||
@@ -238,7 +216,7 @@ class ContactSearchPagedDataSource(
|
||||
fun getSize(): Int {
|
||||
val contentsAndExpand = min(
|
||||
section.expandConfig?.let {
|
||||
if (it.isExpanded) Int.MAX_VALUE else (it.maxCountWhenNotExpanded(activeContactCount) + 1)
|
||||
if (it.isExpanded) Int.MAX_VALUE else (it.maxCountWhenNotExpanded + 1)
|
||||
} ?: Int.MAX_VALUE,
|
||||
contentSize
|
||||
)
|
||||
@@ -261,74 +239,22 @@ class ContactSearchPagedDataSource(
|
||||
index == getSize() - 1 && shouldDisplayExpandRow() -> ContactSearchData.Expand(section.sectionKey)
|
||||
else -> {
|
||||
val correctedIndex = if (section.includeHeader) index - 1 else index
|
||||
return getItemAtCorrectedIndex(correctedIndex)
|
||||
if (correctedIndex < cursor.count) {
|
||||
cursor.moveToPosition(correctedIndex)
|
||||
cursorMapper.invoke(cursor)
|
||||
} else {
|
||||
val extraIndex = correctedIndex - cursor.count
|
||||
extraData[extraIndex]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun getItemAtCorrectedIndex(correctedIndex: Int): ContactSearchData {
|
||||
return if (correctedIndex < cursor.count) {
|
||||
cursor.moveToPosition(correctedIndex)
|
||||
cursorMapper.invoke(cursor)
|
||||
} else {
|
||||
val extraIndex = correctedIndex - cursor.count
|
||||
extraData[extraIndex]
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldDisplayExpandRow(): Boolean {
|
||||
val expandConfig = section.expandConfig
|
||||
return when {
|
||||
expandConfig == null || expandConfig.isExpanded -> false
|
||||
else -> contentSize > expandConfig.maxCountWhenNotExpanded(activeContactCount) + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class StoriesCollection(
|
||||
section: ContactSearchConfiguration.Section,
|
||||
cursor: Cursor,
|
||||
extraData: List<ContactSearchData>,
|
||||
cursorMapper: (Cursor) -> ContactSearchData,
|
||||
activeContactCount: Int,
|
||||
val storyComparator: StoryComparator
|
||||
) : ResultsCollection(section, cursor, extraData, cursorMapper, activeContactCount) {
|
||||
private val aggregateStoryData: List<ContactSearchData.Story> by lazy {
|
||||
if (section !is ContactSearchConfiguration.Section.Stories) {
|
||||
error("Aggregate data creation is only necessary for stories.")
|
||||
}
|
||||
|
||||
val cursorContacts: List<ContactSearchData> = (0 until cursor.count).map {
|
||||
cursor.moveToPosition(it)
|
||||
cursorMapper(cursor)
|
||||
}
|
||||
|
||||
(cursorContacts + extraData)
|
||||
.filterIsInstance(ContactSearchData.Story::class.java)
|
||||
.sortedWith(storyComparator)
|
||||
}
|
||||
|
||||
override fun getItemAtCorrectedIndex(correctedIndex: Int): ContactSearchData {
|
||||
return aggregateStoryData[correctedIndex]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* StoryComparator
|
||||
*/
|
||||
private class StoryComparator(private val latestStorySends: List<StorySend>) : Comparator<ContactSearchData.Story> {
|
||||
override fun compare(lhs: ContactSearchData.Story, rhs: ContactSearchData.Story): Int {
|
||||
val lhsActiveRank = latestStorySends.indexOfFirst { it.identifier.matches(lhs.recipient) }.let { if (it == -1) Int.MAX_VALUE else it }
|
||||
val rhsActiveRank = latestStorySends.indexOfFirst { it.identifier.matches(rhs.recipient) }.let { if (it == -1) Int.MAX_VALUE else it }
|
||||
|
||||
return when {
|
||||
lhs.recipient.isMyStory && rhs.recipient.isMyStory -> 0
|
||||
lhs.recipient.isMyStory -> -1
|
||||
rhs.recipient.isMyStory -> 1
|
||||
lhsActiveRank < rhsActiveRank -> -1
|
||||
lhsActiveRank > rhsActiveRank -> 1
|
||||
lhsActiveRank == rhsActiveRank -> -1
|
||||
else -> 0
|
||||
else -> contentSize > expandConfig.maxCountWhenNotExpanded + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ import org.thoughtcrime.securesms.database.DistributionListDatabase
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.keyvalue.StorySend
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
@@ -24,11 +22,6 @@ open class ContactSearchPagedDataSourceRepository(
|
||||
|
||||
private val contactRepository = ContactRepository(context, context.getString(R.string.note_to_self))
|
||||
|
||||
open fun getLatestStorySends(activeStoryCutoffDuration: Long): List<StorySend> {
|
||||
return SignalStore.storyValues()
|
||||
.getLatestActiveStorySendTimestamps(System.currentTimeMillis() - activeStoryCutoffDuration)
|
||||
}
|
||||
|
||||
open fun querySignalContacts(query: String?, includeSelf: Boolean): Cursor? {
|
||||
return contactRepository.querySignalContacts(query ?: "", includeSelf)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
package org.thoughtcrime.securesms.contacts.paged
|
||||
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
|
||||
class ContactSearchRepository {
|
||||
fun filterOutUnselectableContactSearchKeys(contactSearchKeys: Set<ContactSearchKey>): Single<Set<ContactSearchSelectionResult>> {
|
||||
@@ -34,17 +29,4 @@ class ContactSearchRepository {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun unmarkDisplayAsStory(groupId: GroupId): Completable {
|
||||
return Completable.fromAction {
|
||||
SignalDatabase.groups.markDisplayAsStory(groupId, false)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun deletePrivateStory(distributionListId: DistributionListId): Completable {
|
||||
return Completable.fromAction {
|
||||
SignalDatabase.distributionLists.deleteList(distributionListId)
|
||||
Stories.onStorySettingsChanged(distributionListId)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import org.signal.paging.PagingController
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import org.whispersystems.signalservice.api.util.Preconditions
|
||||
|
||||
/**
|
||||
* Simple, reusable view model that manages a ContactSearchPagedDataSource as well as filter and expansion state.
|
||||
@@ -98,31 +97,6 @@ class ContactSearchViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun removeGroupStory(story: ContactSearchData.Story) {
|
||||
Preconditions.checkArgument(story.recipient.isGroup)
|
||||
setKeysNotSelected(setOf(story.contactSearchKey))
|
||||
disposables += contactSearchRepository.unmarkDisplayAsStory(story.recipient.requireGroupId()).subscribe {
|
||||
configurationStore.update { state ->
|
||||
state.copy(
|
||||
groupStories = state.groupStories.filter { it.recipient.id == story.recipient.id }.toSet()
|
||||
)
|
||||
}
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
fun deletePrivateStory(story: ContactSearchData.Story) {
|
||||
Preconditions.checkArgument(story.recipient.isDistributionList && !story.recipient.isMyStory)
|
||||
setKeysNotSelected(setOf(story.contactSearchKey))
|
||||
disposables += contactSearchRepository.deletePrivateStory(story.recipient.requireDistributionListId()).subscribe {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
controller.value?.onDataInvalidated()
|
||||
}
|
||||
|
||||
class Factory(private val selectionLimits: SelectionLimits, private val repository: ContactSearchRepository) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(ContactSearchViewModel(selectionLimits, repository)) as T
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms.contacts.selection
|
||||
|
||||
import android.os.Bundle
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
@@ -16,8 +15,7 @@ data class ContactSelectionArguments(
|
||||
val canSelectSelf: Boolean = selectionLimits == null,
|
||||
val displayChips: Boolean = true,
|
||||
val recyclerPadBottom: Int = -1,
|
||||
val recyclerChildClipping: Boolean = true,
|
||||
val checkboxResource: Int = R.drawable.contact_selection_checkbox
|
||||
val recyclerChildClipping: Boolean = true
|
||||
) {
|
||||
|
||||
fun toArgumentBundle(): Bundle {
|
||||
@@ -31,7 +29,6 @@ data class ContactSelectionArguments(
|
||||
putBoolean(DISPLAY_CHIPS, displayChips)
|
||||
putInt(RV_PADDING_BOTTOM, recyclerPadBottom)
|
||||
putBoolean(RV_CLIP, recyclerChildClipping)
|
||||
putInt(CHECKBOX_RESOURCE, checkboxResource)
|
||||
putParcelableArrayList(CURRENT_SELECTION, ArrayList(currentSelection))
|
||||
}
|
||||
}
|
||||
@@ -47,6 +44,5 @@ data class ContactSelectionArguments(
|
||||
const val DISPLAY_CHIPS = "display_chips"
|
||||
const val RV_PADDING_BOTTOM = "recycler_view_padding_bottom"
|
||||
const val RV_CLIP = "recycler_view_clipping"
|
||||
const val CHECKBOX_RESOURCE = "checkbox_resource"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,11 +134,6 @@ object ContactDiscovery {
|
||||
@JvmStatic
|
||||
@WorkerThread
|
||||
fun syncRecipientInfoWithSystemContacts(context: Context) {
|
||||
if (!hasContactsPermissions(context)) {
|
||||
Log.w(TAG, "[syncRecipientInfoWithSystemContacts] No contacts permission, skipping.")
|
||||
return
|
||||
}
|
||||
|
||||
syncRecipientsWithSystemContacts(
|
||||
context = context,
|
||||
rewrites = emptyMap(),
|
||||
|
||||
@@ -96,10 +96,6 @@ class ContactDiscoveryRefreshV1 {
|
||||
.map(Recipient::requireE164)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
if (numbers.size() < recipients.size()) {
|
||||
Log.w(TAG, "We were asked to refresh " + recipients.size() + " numbers, but filtered that down to " + numbers.size());
|
||||
}
|
||||
|
||||
return refreshNumbers(context, numbers, numbers);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,24 +3,21 @@ package org.thoughtcrime.securesms.contactshare;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
@@ -34,9 +31,8 @@ import static org.thoughtcrime.securesms.contactshare.ContactShareEditViewModel.
|
||||
|
||||
public class ContactShareEditActivity extends PassphraseRequiredActivity implements ContactShareEditAdapter.EventListener {
|
||||
|
||||
public static final String KEY_CONTACTS = "contacts";
|
||||
private static final String KEY_CONTACT_URIS = "contact_uris";
|
||||
private static final String KEY_SEND_BUTTON_COLOR = "send_button_color";
|
||||
public static final String KEY_CONTACTS = "contacts";
|
||||
private static final String KEY_CONTACT_URIS = "contact_uris";
|
||||
private static final int CODE_NAME_EDIT = 55;
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
@@ -44,12 +40,11 @@ public class ContactShareEditActivity extends PassphraseRequiredActivity impleme
|
||||
|
||||
private ContactShareEditViewModel viewModel;
|
||||
|
||||
public static Intent getIntent(@NonNull Context context, @NonNull List<Uri> contactUris, @ColorInt int sendButtonColor) {
|
||||
public static Intent getIntent(@NonNull Context context, @NonNull List<Uri> contactUris) {
|
||||
ArrayList<Uri> contactUriList = new ArrayList<>(contactUris);
|
||||
|
||||
Intent intent = new Intent(context, ContactShareEditActivity.class);
|
||||
intent.putParcelableArrayListExtra(KEY_CONTACT_URIS, contactUriList);
|
||||
intent.putExtra(KEY_SEND_BUTTON_COLOR, sendButtonColor);
|
||||
return intent;
|
||||
}
|
||||
|
||||
@@ -73,7 +68,6 @@ public class ContactShareEditActivity extends PassphraseRequiredActivity impleme
|
||||
}
|
||||
|
||||
View sendButton = findViewById(R.id.contact_share_edit_send);
|
||||
ViewCompat.setBackgroundTintList(sendButton, ColorStateList.valueOf(getIntent().getIntExtra(KEY_SEND_BUTTON_COLOR, Color.RED)));
|
||||
sendButton.setOnClickListener(v -> onSendClicked(viewModel.getFinalizedContacts()));
|
||||
|
||||
RecyclerView contactList = findViewById(R.id.contact_share_edit_list);
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package org.thoughtcrime.securesms.conversation
|
||||
|
||||
import androidx.core.view.doOnNextLayout
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.signal.core.util.DimensionUnit
|
||||
|
||||
/**
|
||||
* Adds necessary padding to each side of the given RecyclerView in order to ensure that
|
||||
* if all buttons can fit in the visible real-estate on screen, they are centered.
|
||||
*/
|
||||
class AttachmentButtonCenterHelper(private val recyclerView: RecyclerView) : RecyclerView.AdapterDataObserver() {
|
||||
|
||||
private val itemWidth: Float = DimensionUnit.DP.toPixels(88f)
|
||||
private val defaultPadding: Float = DimensionUnit.DP.toPixels(16f)
|
||||
|
||||
override fun onChanged() {
|
||||
val itemCount = recyclerView.adapter?.itemCount ?: return
|
||||
val requiredSpace = itemWidth * itemCount
|
||||
|
||||
recyclerView.doOnNextLayout {
|
||||
if (it.measuredWidth >= requiredSpace) {
|
||||
val extraSpace = it.measuredWidth - requiredSpace
|
||||
val availablePadding = extraSpace / 2f
|
||||
it.post {
|
||||
it.setPadding(availablePadding.toInt(), it.paddingTop, availablePadding.toInt(), it.paddingBottom)
|
||||
}
|
||||
} else {
|
||||
it.setPadding(defaultPadding.toInt(), it.paddingTop, defaultPadding.toInt(), it.paddingBottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,8 +78,6 @@ public class AttachmentKeyboard extends FrameLayout implements InputAwareLayout.
|
||||
mediaList.setAdapter(mediaAdapter);
|
||||
buttonList.setAdapter(buttonAdapter);
|
||||
|
||||
buttonAdapter.registerAdapterDataObserver(new AttachmentButtonCenterHelper(buttonList));
|
||||
|
||||
mediaList.setLayoutManager(new GridLayoutManager(context, 1, GridLayoutManager.HORIZONTAL, false));
|
||||
buttonList.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false));
|
||||
|
||||
@@ -124,6 +122,7 @@ public class AttachmentKeyboard extends FrameLayout implements InputAwareLayout.
|
||||
buttonAdapter.setWallpaperEnabled(wallpaperEnabled);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void show(int height, boolean immediate) {
|
||||
ViewGroup.LayoutParams params = getLayoutParams();
|
||||
|
||||
@@ -7,11 +7,11 @@ import org.thoughtcrime.securesms.R;
|
||||
|
||||
public enum AttachmentKeyboardButton {
|
||||
|
||||
GALLERY(R.string.AttachmentKeyboard_gallery, R.drawable.ic_gallery_outline_24),
|
||||
FILE(R.string.AttachmentKeyboard_file, R.drawable.ic_file_outline_24),
|
||||
PAYMENT(R.string.AttachmentKeyboard_payment, R.drawable.ic_payments_24),
|
||||
CONTACT(R.string.AttachmentKeyboard_contact, R.drawable.ic_contact_outline_24),
|
||||
LOCATION(R.string.AttachmentKeyboard_location, R.drawable.ic_location_outline_24);
|
||||
GALLERY(R.string.AttachmentKeyboard_gallery, R.drawable.ic_photo_album_outline_32),
|
||||
FILE(R.string.AttachmentKeyboard_file, R.drawable.ic_file_outline_32),
|
||||
PAYMENT(R.string.AttachmentKeyboard_payment, R.drawable.ic_payments_32),
|
||||
CONTACT(R.string.AttachmentKeyboard_contact, R.drawable.ic_contact_circle_outline_32),
|
||||
LOCATION(R.string.AttachmentKeyboard_location, R.drawable.ic_location_outline_32);
|
||||
|
||||
private final int titleRes;
|
||||
private final int iconRes;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -8,7 +7,6 @@ import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
@@ -81,8 +79,8 @@ class AttachmentKeyboardButtonAdapter extends RecyclerView.Adapter<AttachmentKey
|
||||
public ButtonViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
this.image = itemView.findViewById(R.id.icon);
|
||||
this.title = itemView.findViewById(R.id.label);
|
||||
this.image = itemView.findViewById(R.id.attachment_button_image);
|
||||
this.title = itemView.findViewById(R.id.attachment_button_title);
|
||||
}
|
||||
|
||||
void bind(@NonNull AttachmentKeyboardButton button, boolean wallpaperEnabled, @NonNull Listener listener) {
|
||||
@@ -90,6 +88,12 @@ class AttachmentKeyboardButtonAdapter extends RecyclerView.Adapter<AttachmentKey
|
||||
title.setText(button.getTitleRes());
|
||||
|
||||
itemView.setOnClickListener(v -> listener.onClick(button));
|
||||
|
||||
if (wallpaperEnabled) {
|
||||
itemView.setBackgroundResource(R.drawable.attachment_keyboard_button_wallpaper_background);
|
||||
} else {
|
||||
itemView.setBackgroundResource(R.drawable.attachment_keyboard_button_background);
|
||||
}
|
||||
}
|
||||
|
||||
void recycle() {
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.LayoutRes;
|
||||
@@ -39,6 +40,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.paging.PagingController;
|
||||
import org.thoughtcrime.securesms.BindableConversationItem;
|
||||
@@ -63,8 +65,10 @@ import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
@@ -123,9 +127,8 @@ public class ConversationAdapter
|
||||
private ConversationMessage inlineContent;
|
||||
private Colorizer colorizer;
|
||||
private boolean isTypingViewEnabled;
|
||||
private boolean condensedMode;
|
||||
|
||||
public ConversationAdapter(@NonNull Context context,
|
||||
ConversationAdapter(@NonNull Context context,
|
||||
@NonNull LifecycleOwner lifecycleOwner,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@@ -178,9 +181,9 @@ public class ConversationAdapter
|
||||
} else if (messageRecord.isUpdate()) {
|
||||
return MESSAGE_TYPE_UPDATE;
|
||||
} else if (messageRecord.isOutgoing()) {
|
||||
return MessageRecordUtil.isTextOnly(messageRecord, context) && !conversationMessage.hasBeenQuoted() ? MESSAGE_TYPE_OUTGOING_TEXT : MESSAGE_TYPE_OUTGOING_MULTIMEDIA;
|
||||
return MessageRecordUtil.isTextOnly(messageRecord, context) ? MESSAGE_TYPE_OUTGOING_TEXT : MESSAGE_TYPE_OUTGOING_MULTIMEDIA;
|
||||
} else {
|
||||
return MessageRecordUtil.isTextOnly(messageRecord, context) && !conversationMessage.hasBeenQuoted() ? MESSAGE_TYPE_INCOMING_TEXT : MESSAGE_TYPE_INCOMING_MULTIMEDIA;
|
||||
return MessageRecordUtil.isTextOnly(messageRecord, context) ? MESSAGE_TYPE_INCOMING_TEXT : MESSAGE_TYPE_INCOMING_MULTIMEDIA;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,11 +263,6 @@ public class ConversationAdapter
|
||||
}
|
||||
}
|
||||
|
||||
public void setCondensedMode(boolean condensedMode) {
|
||||
this.condensedMode = condensedMode;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
switch (getItemViewType(position)) {
|
||||
@@ -290,11 +288,10 @@ public class ConversationAdapter
|
||||
recipient,
|
||||
searchQuery,
|
||||
conversationMessage == recordToPulse,
|
||||
hasWallpaper && !condensedMode,
|
||||
hasWallpaper,
|
||||
isMessageRequestAccepted,
|
||||
conversationMessage == inlineContent,
|
||||
colorizer,
|
||||
condensedMode);
|
||||
colorizer);
|
||||
|
||||
if (conversationMessage == recordToPulse) {
|
||||
recordToPulse = null;
|
||||
@@ -351,22 +348,22 @@ public class ConversationAdapter
|
||||
|
||||
if (type == HEADER_TYPE_POPOVER_DATE) {
|
||||
if (hasWallpaper) {
|
||||
viewHolder.setBackgroundRes(R.drawable.wallpaper_bubble_background_18);
|
||||
viewHolder.setBackgroundRes(R.drawable.wallpaper_bubble_background_8);
|
||||
} else {
|
||||
viewHolder.setBackgroundRes(R.drawable.sticky_date_header_background);
|
||||
}
|
||||
} else if (type == HEADER_TYPE_INLINE_DATE) {
|
||||
if (hasWallpaper) {
|
||||
viewHolder.setBackgroundRes(R.drawable.wallpaper_bubble_background_18);
|
||||
viewHolder.setBackgroundRes(R.drawable.wallpaper_bubble_background_8);
|
||||
} else {
|
||||
viewHolder.clearBackground();
|
||||
}
|
||||
}
|
||||
|
||||
if (hasWallpaper && ThemeUtil.isDarkTheme(context)) {
|
||||
viewHolder.setTextColor(ContextCompat.getColor(context, R.color.signal_colorNeutralInverse));
|
||||
viewHolder.setTextColor(ContextCompat.getColor(context, R.color.core_grey_15));
|
||||
} else {
|
||||
viewHolder.setTextColor(ContextCompat.getColor(context, R.color.signal_colorOnSurfaceVariant));
|
||||
viewHolder.setTextColor(ContextCompat.getColor(context, R.color.signal_text_secondary));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,7 +400,7 @@ public class ConversationAdapter
|
||||
viewHolder.setText(viewHolder.itemView.getContext().getResources().getQuantityString(R.plurals.ConversationAdapter_n_unread_messages, count, count));
|
||||
|
||||
if (hasWallpaper) {
|
||||
viewHolder.setBackgroundRes(R.drawable.wallpaper_bubble_background_18);
|
||||
viewHolder.setBackgroundRes(R.drawable.wallpaper_bubble_background_8);
|
||||
viewHolder.setDividerColor(viewHolder.itemView.getResources().getColor(R.color.transparent_black_80));
|
||||
} else {
|
||||
viewHolder.clearBackground();
|
||||
@@ -783,7 +780,7 @@ public class ConversationAdapter
|
||||
}
|
||||
}
|
||||
|
||||
public interface ItemClickListener extends BindableConversationItem.EventListener {
|
||||
interface ItemClickListener extends BindableConversationItem.EventListener {
|
||||
void onItemClick(MultiselectPart item);
|
||||
void onItemLongClick(View itemView, MultiselectPart item);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user