Merge tag 'v6.28.6' into molly-6.28

This commit is contained in:
Oscar Mira 2023-08-12 12:31:56 +02:00
commit f78f0e5b9e
366 changed files with 4797 additions and 13797 deletions

View file

@ -5,4 +5,5 @@ indent_size = 2
ktlint_standard_trailing-comma-on-call-site = disable
ktlint_standard_trailing-comma-on-declaration-site = disable
ktlink_standard_spacing-between-declarations-with-annotations = disable
ktlint_code_style = intellij_idea
ktlint_code_style = intellij_idea
ktlint_standard_class-naming = disabled

View file

@ -56,8 +56,8 @@ ext {
MAPS_API_KEY = getEnv('CI_MAPS_API_KEY') ?: mapsApiKey
}
def canonicalVersionCode = 1304
def canonicalVersionName = "6.27.10"
def canonicalVersionCode = 1310
def canonicalVersionName = "6.28.6"
def mollyRevision = 0
def postFixSize = 100
@ -354,7 +354,7 @@ android {
buildConfigField "String", "SIGNAL_SVR2_URL", "\"https://svr2.staging.signal.org\""
buildConfigField "String", "SVR2_MRENCLAVE", "\"a8a261420a6bb9b61aa25bf8a79e8bd20d7652531feb3381cbffd446d270be95\""
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"39963b736823d5780be96ab174869a9499d56d66497aa8f9b2244f777ebc366b\", " +
"\"9dbc6855c198e04f21b5cc35df839fdcd51b53658454dfa3f817afefaffc95ef\", " +
"\"ee1d0d972b7ea903615670de43ab1b6e7a825e811c70a29bb5fe0f819e0975fa\", " +
"\"45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c\")"
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[] { new org.thoughtcrime.securesms.KbsEnclave(\"dd6f66d397d9e8cf6ec6db238e59a7be078dd50e9715427b9c89b409ffe53f99\", " +
"\"4200003414528c151e2dccafbc87aa6d3d66a5eb8f8c05979a6e97cb33cd493a\", " +

View file

@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.testing.success
import org.thoughtcrime.securesms.testing.timeout
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import org.whispersystems.signalservice.internal.push.MismatchedDevices
import org.whispersystems.signalservice.internal.push.PreKeyState
import java.util.UUID
@ -73,7 +74,7 @@ class ChangeNumberViewModelTest {
fun testChangeNumber_givenOnlyPrimaryAndNoRegLock() {
// GIVEN
val aci = Recipient.self().requireServiceId()
val newPni = ServiceId.from(UUID.randomUUID())
val newPni = PNI.from(UUID.randomUUID())
lateinit var changeNumberRequest: ChangePhoneNumberRequest
lateinit var setPreKeysRequest: PreKeyState
@ -180,7 +181,7 @@ class ChangeNumberViewModelTest {
val aci = Recipient.self().requireServiceId()
val oldPni = Recipient.self().requirePni()
val oldE164 = Recipient.self().requireE164()
val newPni = ServiceId.from(UUID.randomUUID())
val newPni = PNI.from(UUID.randomUUID())
lateinit var changeNumberRequest: ChangePhoneNumberRequest
lateinit var setPreKeysRequest: PreKeyState
@ -225,7 +226,7 @@ class ChangeNumberViewModelTest {
fun testChangeNumber_givenOnlyPrimaryAndRegistrationLock() {
// GIVEN
val aci = Recipient.self().requireServiceId()
val newPni = ServiceId.from(UUID.randomUUID())
val newPni = PNI.from(UUID.randomUUID())
lateinit var changeNumberRequest: ChangePhoneNumberRequest
lateinit var setPreKeysRequest: PreKeyState
@ -269,7 +270,7 @@ class ChangeNumberViewModelTest {
fun testChangeNumber_givenMismatchedDevicesOnFirstCall() {
// GIVEN
val aci = Recipient.self().requireServiceId()
val newPni = ServiceId.from(UUID.randomUUID())
val newPni = PNI.from(UUID.randomUUID())
lateinit var changeNumberRequest: ChangePhoneNumberRequest
lateinit var setPreKeysRequest: PreKeyState
@ -313,7 +314,7 @@ class ChangeNumberViewModelTest {
fun testChangeNumber_givenRegLockAndMismatchedDevicesOnFirstTwoCalls() {
// GIVEN
val aci = Recipient.self().requireServiceId()
val newPni = ServiceId.from(UUID.randomUUID())
val newPni = PNI.from(UUID.randomUUID())
lateinit var changeNumberRequest: ChangePhoneNumberRequest
lateinit var setPreKeysRequest: PreKeyState

View file

@ -8,6 +8,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.ThreadUtil
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.conversation.v2.ConversationActivity
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
import org.thoughtcrime.securesms.mms.OutgoingMessage

View file

@ -7,6 +7,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.conversation.v2.ConversationActivity
import org.thoughtcrime.securesms.database.IdentityTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DistributionListId

View file

@ -7,7 +7,7 @@ import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.DistributionListRecord
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import java.util.UUID
class DistributionListTablesTest {

View file

@ -10,9 +10,8 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import java.util.UUID
@Suppress("ClassName")
@ -34,7 +33,7 @@ class MessageTableTest_gifts {
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())) }
recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())) }
}
@Test

View file

@ -17,7 +17,6 @@ object MmsHelper {
recipient: Recipient = Recipient.UNKNOWN,
body: String = "body",
sentTimeMillis: Long = System.currentTimeMillis(),
subscriptionId: Int = -1,
expiresIn: Long = 0,
viewOnce: Boolean = false,
distributionType: Int = ThreadTable.DistributionTypes.DEFAULT,
@ -32,7 +31,6 @@ object MmsHelper {
recipient = recipient,
body = body,
timestamp = sentTimeMillis,
subscriptionId = subscriptionId,
expiresIn = expiresIn,
viewOnce = viewOnce,
distributionType = distributionType,

View file

@ -16,9 +16,8 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import java.util.UUID
import java.util.concurrent.TimeUnit
@ -45,7 +44,7 @@ class MmsTableTest_stories {
SignalStore.account().setPni(localPni)
myStory = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(DistributionListId.MY_STORY))
recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())) }
recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())) }
releaseChannelRecipient = Recipient.resolved(SignalDatabase.recipients.insertReleaseChannelRecipient())
SignalStore.releaseChannelValues().setReleaseChannelRecipientId(releaseChannelRecipient.id)

View file

@ -13,8 +13,8 @@ import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.FeatureFlagsAccessor
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import java.util.UUID
@RunWith(AndroidJUnit4::class)
@ -173,10 +173,10 @@ class RecipientTableTest {
SignalDatabase.recipients.markUnregistered(mainId)
val byAci: RecipientId = SignalDatabase.recipients.getByServiceId(ACI_A).get()
val byAci: RecipientId = SignalDatabase.recipients.getByAci(ACI_A).get()
val byE164: RecipientId = SignalDatabase.recipients.getByE164(E164_A).get()
val byPni: RecipientId = SignalDatabase.recipients.getByServiceId(PNI_A).get()
val byPni: RecipientId = SignalDatabase.recipients.getByPni(PNI_A).get()
assertEquals(mainId, byAci)
assertEquals(byE164, byPni)
@ -192,10 +192,10 @@ class RecipientTableTest {
SignalDatabase.recipients.splitForStorageSync(mainRecord.storageId!!)
val byAci: RecipientId = SignalDatabase.recipients.getByServiceId(ACI_A).get()
val byAci: RecipientId = SignalDatabase.recipients.getByAci(ACI_A).get()
val byE164: RecipientId = SignalDatabase.recipients.getByE164(E164_A).get()
val byPni: RecipientId = SignalDatabase.recipients.getByServiceId(PNI_A).get()
val byPni: RecipientId = SignalDatabase.recipients.getByPni(PNI_A).get()
assertEquals(mainId, byAci)
assertEquals(byE164, byPni)

View file

@ -7,6 +7,7 @@ import org.hamcrest.MatcherAssert
import org.hamcrest.Matchers
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
@ -14,6 +15,7 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.SqlUtil
import org.signal.core.util.exists
import org.signal.core.util.requireLong
import org.signal.core.util.requireNonNullString
import org.signal.core.util.select
@ -36,14 +38,13 @@ import org.thoughtcrime.securesms.mms.IncomingMediaMessage
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage
import org.thoughtcrime.securesms.sms.IncomingTextMessage
import org.thoughtcrime.securesms.util.Base64
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.FeatureFlagsAccessor
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import java.util.Optional
import java.util.UUID
@ -59,6 +60,21 @@ class RecipientTableTest_getAndPossiblyMerge {
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
}
@Test
fun single() {
test("merge, e164 + pni reassigned, aci abandoned") {
given(E164_A, PNI_A, ACI_A)
given(E164_B, PNI_B, ACI_B)
process(E164_A, PNI_A, ACI_B)
expect(null, null, ACI_A)
expect(E164_A, PNI_A, ACI_B)
expectChangeNumberEvent()
}
}
@Test
fun allNonMergeTests() {
test("e164-only insert") {
@ -69,7 +85,7 @@ class RecipientTableTest_getAndPossiblyMerge {
assertEquals(RecipientTable.RegisteredState.UNKNOWN, record.registered)
}
test("pni-only insert") {
test("pni-only insert", exception = IllegalArgumentException::class.java) {
val id = process(null, PNI_A, null)
expect(null, PNI_A, null)
@ -84,6 +100,21 @@ class RecipientTableTest_getAndPossiblyMerge {
val record = SignalDatabase.recipients.getRecord(id)
assertEquals(RecipientTable.RegisteredState.REGISTERED, record.registered)
}
test("e164+pni insert") {
process(E164_A, PNI_A, null)
expect(E164_A, PNI_A, null)
}
test("e164+aci insert") {
process(E164_A, null, ACI_A)
expect(E164_A, null, ACI_A)
}
test("e164+pni+aci insert") {
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
@ -167,6 +198,12 @@ class RecipientTableTest_getAndPossiblyMerge {
expectSessionSwitchoverEvent(E164_A)
}
test("e164 and pni matches, all provided, new aci, existing pni session, pni-verified") {
given(E164_A, PNI_A, null, pniSession = true)
process(E164_A, PNI_A, ACI_A, pniVerified = true)
expect(E164_A, PNI_A, ACI_A)
}
test("e164 and aci matches, all provided, new pni") {
given(E164_A, null, ACI_A)
process(E164_A, PNI_A, ACI_A)
@ -309,6 +346,26 @@ class RecipientTableTest_getAndPossiblyMerge {
expectChangeNumberEvent()
}
test("steal, pni is changed") {
given(E164_A, PNI_B, ACI_A)
given(E164_B, PNI_A, null)
process(E164_A, PNI_A, null)
expect(E164_A, PNI_A, ACI_A)
expect(E164_B, null, null)
}
test("steal, pni is changed, aci left behind") {
given(E164_B, PNI_A, ACI_A)
given(E164_A, PNI_B, null)
process(E164_A, PNI_A, null)
expect(E164_B, null, ACI_A)
expect(E164_A, PNI_A, null)
}
test("steal, e164+pni & e164+pni, no aci provided, no pni session") {
given(E164_A, PNI_B, null)
given(E164_B, PNI_A, null)
@ -502,7 +559,7 @@ class RecipientTableTest_getAndPossiblyMerge {
expectThreadMergeEvent(E164_A)
}
test("merge, e164+pni & aci, pni session, thread merge shadows") {
test("merge, e164+pni & aci, pni session, thread merge shadows SSE") {
given(E164_A, PNI_A, null, pniSession = true)
given(null, null, ACI_A)
@ -600,6 +657,18 @@ class RecipientTableTest_getAndPossiblyMerge {
expectThreadMergeEvent(E164_A)
}
test("merge, e164 + pni reassigned, aci abandoned") {
given(E164_A, PNI_A, ACI_A)
given(E164_B, PNI_B, ACI_B)
process(E164_A, PNI_A, ACI_B)
expect(null, null, ACI_A)
expect(E164_A, PNI_A, ACI_B)
expectChangeNumberEvent()
}
test("local user, local e164 and aci provided, changeSelf=false, leave e164 alone") {
given(E164_SELF, null, ACI_SELF)
given(null, null, ACI_A)
@ -768,9 +837,15 @@ class RecipientTableTest_getAndPossiblyMerge {
}
private fun identityKey(value: Byte): IdentityKey {
val byteArray = ByteArray(32)
byteArray[0] = value
return identityKey(byteArray)
}
private fun identityKey(value: ByteArray): IdentityKey {
val bytes = ByteArray(33)
bytes[0] = 0x05
bytes[1] = value
value.copyInto(bytes, 1)
return IdentityKey(bytes)
}
@ -873,8 +948,8 @@ class RecipientTableTest_getAndPossiblyMerge {
generatedIds += id
if (createThread) {
// Create a thread and throw a dummy message in it so it doesn't get automatically deleted
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(id))
SignalDatabase.messages.insertMessageInbox(IncomingEncryptedMessage(IncomingTextMessage(id, 1, (Math.random() * Long.MAX_VALUE).toLong(), 0, 0, "", Optional.empty(), 0, false, ""), ""))
val result = SignalDatabase.messages.insertMessageInbox(smsMessage(sender = id, time = (Math.random() * 10000000).toLong(), body = "1"))
SignalDatabase.threads.markAsActiveEarly(result.get().threadId)
}
if (pniSession) {
@ -885,11 +960,34 @@ class RecipientTableTest_getAndPossiblyMerge {
SignalDatabase.sessions.store(pni, SignalProtocolAddress(pni.toString(), 1), SessionRecord())
}
if (aci != null) {
SignalDatabase.identities.saveIdentity(
addressName = aci.toString(),
recipientId = id,
identityKey = identityKey(Util.getSecretBytes(32)),
verifiedStatus = IdentityTable.VerifiedStatus.DEFAULT,
firstUse = true,
timestamp = 0,
nonBlockingApproval = false
)
}
if (pni != null) {
SignalDatabase.identities.saveIdentity(
addressName = pni.toString(),
recipientId = id,
identityKey = identityKey(Util.getSecretBytes(32)),
verifiedStatus = IdentityTable.VerifiedStatus.DEFAULT,
firstUse = true,
timestamp = 0,
nonBlockingApproval = false
)
}
return id
}
fun process(e164: String?, pni: PNI?, aci: ACI?, changeSelf: Boolean = false, pniVerified: Boolean = false): RecipientId {
outputRecipientId = SignalDatabase.recipients.getAndPossiblyMerge(serviceId = aci ?: pni, pni = pni, e164 = e164, pniVerified = pniVerified, changeSelf = changeSelf)
outputRecipientId = SignalDatabase.recipients.getAndPossiblyMerge(aci = aci, pni = pni, e164 = e164, pniVerified = pniVerified, changeSelf = changeSelf)
generatedIds += outputRecipientId
return outputRecipientId
}
@ -903,15 +1001,15 @@ class RecipientTableTest_getAndPossiblyMerge {
val expected = RecipientTuple(
e164 = e164,
pni = pni,
serviceId = aci ?: pni
aci = aci
)
val actual = RecipientTuple(
e164 = recipient.e164.orElse(null),
pni = recipient.pni.orElse(null),
serviceId = recipient.serviceId.orElse(null)
aci = recipient.aci.orElse(null)
)
assertEquals(expected, actual)
assertEquals("Recipient $id did not match expected result!", expected, actual)
}
fun expectDeleted() {
@ -919,21 +1017,21 @@ class RecipientTableTest_getAndPossiblyMerge {
}
fun expectDeleted(id: RecipientId) {
SignalDatabase.rawDatabase
.select("1")
.from(RecipientTable.TABLE_NAME)
val found = SignalDatabase.rawDatabase
.exists(RecipientTable.TABLE_NAME)
.where("${RecipientTable.ID} = ?", id)
.run()
.use { !it.moveToFirst() }
assertFalse("Expected $id to be deleted, but it's still present!", found)
}
fun expectChangeNumberEvent() {
assertEquals(1, SignalDatabase.messages.getChangeNumberMessageCount(outputRecipientId))
assertEquals("Missing change number event!", 1, SignalDatabase.messages.getChangeNumberMessageCount(outputRecipientId))
changeNumberExpected = true
}
fun expectNoChangeNumberEvent() {
assertEquals(0, SignalDatabase.messages.getChangeNumberMessageCount(outputRecipientId))
assertEquals("Unexpected change number event!", 0, SignalDatabase.messages.getChangeNumberMessageCount(outputRecipientId))
changeNumberExpected = false
}
@ -943,42 +1041,39 @@ class RecipientTableTest_getAndPossiblyMerge {
fun expectSessionSwitchoverEvent(recipientId: RecipientId, e164: String) {
val event: SessionSwitchoverEvent? = getLatestSessionSwitchoverEvent(recipientId)
assertNotNull(event)
assertNotNull("Missing session switchover event! Expected one with e164 = $e164", event)
assertEquals(e164, event!!.e164)
sessionSwitchoverExpected = true
}
fun expectNoSessionSwitchoverEvent() {
assertNull(getLatestSessionSwitchoverEvent(outputRecipientId))
assertNull("Unexpected session switchover event!", getLatestSessionSwitchoverEvent(outputRecipientId))
}
fun expectThreadMergeEvent(previousE164: String) {
val event: ThreadMergeEvent? = getLatestThreadMergeEvent(outputRecipientId)
assertNotNull(event)
assertEquals(previousE164, event!!.previousE164)
assertNotNull("Missing thread merge event! Expected one with e164 = $previousE164", event)
assertEquals("E164 on thread merge event doesn't match!", previousE164, event!!.previousE164)
threadMergeExpected = true
}
fun expectNoThreadMergeEvent() {
assertNull(getLatestThreadMergeEvent(outputRecipientId))
assertNull("Unexpected thread merge event!", getLatestThreadMergeEvent(outputRecipientId))
}
private fun insert(e164: String?, pni: PNI?, aci: ACI?): RecipientId {
val serviceIdString: String? = (aci ?: pni)?.toString()
val pniString: String? = pni?.toString()
val id: Long = SignalDatabase.rawDatabase.insert(
RecipientTable.TABLE_NAME,
null,
contentValuesOf(
RecipientTable.PHONE to e164,
RecipientTable.SERVICE_ID to serviceIdString,
RecipientTable.PNI_COLUMN to pniString,
RecipientTable.E164 to e164,
RecipientTable.ACI_COLUMN to aci?.toString(),
RecipientTable.PNI_COLUMN to pni?.toString(),
RecipientTable.REGISTERED to RecipientTable.RegisteredState.REGISTERED.id
)
)
assertTrue("Failed to insert! E164: $e164, ServiceId: $serviceIdString, PNI: $pniString", id > 0)
assertTrue("Failed to insert! E164: $e164, ACI: $aci, PNI: $pni", id > 0)
return RecipientId.from(id)
}
@ -987,14 +1082,14 @@ class RecipientTableTest_getAndPossiblyMerge {
data class RecipientTuple(
val e164: String?,
val pni: PNI?,
val serviceId: ServiceId?
val aci: ACI?
) {
/**
* The intent here is to give nice diffs with the name of the constants rather than the values.
*/
override fun toString(): String {
return "(${e164.e164String()}, ${pni.pniString()}, ${serviceId.serviceIdString()})"
return "(${e164.e164String()}, ${pni.pniString()}, ${aci.aciString()})"
}
private fun String?.e164String(): String {
@ -1018,12 +1113,9 @@ class RecipientTableTest_getAndPossiblyMerge {
} ?: "null"
}
private fun ServiceId?.serviceIdString(): String {
private fun ACI?.aciString(): String {
return this?.let {
when (it) {
PNI_A -> "PNI_A"
PNI_B -> "PNI_B"
PNI_SELF -> "PNI_SELF"
ACI_A -> "ACI_A"
ACI_B -> "ACI_B"
ACI_SELF -> "ACI_SELF"

View file

@ -21,9 +21,8 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sms.IncomingGroupUpdateMessage
import org.thoughtcrime.securesms.sms.IncomingTextMessage
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import java.util.Optional
import java.util.UUID
@ -283,8 +282,8 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
}
companion object {
private val aliceServiceId: ServiceId = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
private val bobServiceId: ServiceId = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
private val aliceServiceId: ACI = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
private val bobServiceId: ACI = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
private val masterKey = GroupMasterKey(Hex.fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
private val groupId = GroupId.v2(masterKey)

View file

@ -21,7 +21,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.whispersystems.signalservice.api.push.DistributionId
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import java.util.UUID
@RunWith(AndroidJUnit4::class)
@ -465,7 +465,7 @@ class StorySendTableTest {
private fun makeRecipients(count: Int): List<RecipientId> {
return (1..count).map {
SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID()))
SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID()))
}
}
}

View file

@ -14,7 +14,7 @@ import org.junit.Test
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import java.util.UUID
@Suppress("ClassName")
@ -28,7 +28,7 @@ class ThreadTableTest_active {
@Before
fun setUp() {
recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())))
recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())))
}
@Test

View file

@ -9,7 +9,7 @@ import org.signal.core.util.CursorUtil
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import java.util.UUID
@Suppress("ClassName")
@ -23,7 +23,7 @@ class ThreadTableTest_pinned {
@Before
fun setUp() {
recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())))
recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())))
}
@Test

View file

@ -10,7 +10,7 @@ import org.signal.core.util.CursorUtil
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import java.util.UUID
@Suppress("ClassName")
@ -25,7 +25,7 @@ class ThreadTableTest_recents {
@Before
fun setUp() {
recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())))
recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())))
}
@Test

View file

@ -46,9 +46,9 @@ abstract class MessageContentProcessorTest {
): SignalServiceContentProto {
return TestProtos.build {
serviceContent(
localAddress = address(uuid = harness.self.requireServiceId().uuid()).build(),
localAddress = address(uuid = harness.self.requireServiceId().rawUuid).build(),
metadata = metadata(
address = address(uuid = messageSender.requireServiceId().uuid()).build()
address = address(uuid = messageSender.requireServiceId().rawUuid).build()
).build()
).apply {
content = content().apply {

View file

@ -14,8 +14,8 @@ import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.Base64
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.FeatureFlagsAccessor
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import org.whispersystems.signalservice.api.storage.SignalContactRecord
import org.whispersystems.signalservice.api.storage.StorageId
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord
@ -39,14 +39,14 @@ class ContactRecordProcessorTest {
setStorageId(originalId, STORAGE_ID_A)
val remote1 = buildRecord(STORAGE_ID_B) {
setServiceId(ACI_A.toString())
setAci(ACI_A.toString())
setUnregisteredAtTimestamp(100)
}
val remote2 = buildRecord(STORAGE_ID_C) {
setServiceId(PNI_A.toString())
setServicePni(PNI_A.toString())
setServiceE164(E164_A)
setAci(PNI_A.toString())
setPni(PNI_A.toString())
setE164(E164_A)
}
// WHEN
@ -54,10 +54,10 @@ class ContactRecordProcessorTest {
subject.process(listOf(remote1, remote2), StorageSyncHelper.KEY_GENERATOR)
// THEN
val byAci: RecipientId = SignalDatabase.recipients.getByServiceId(ACI_A).get()
val byAci: RecipientId = SignalDatabase.recipients.getByAci(ACI_A).get()
val byE164: RecipientId = SignalDatabase.recipients.getByE164(E164_A).get()
val byPni: RecipientId = SignalDatabase.recipients.getByServiceId(PNI_A).get()
val byPni: RecipientId = SignalDatabase.recipients.getByPni(PNI_A).get()
assertEquals(originalId, byAci)
assertEquals(byE164, byPni)
@ -71,14 +71,14 @@ class ContactRecordProcessorTest {
setStorageId(originalId, STORAGE_ID_A)
val remote1 = buildRecord(STORAGE_ID_B) {
setServiceId(ACI_A.toString())
setAci(ACI_A.toString())
setUnregisteredAtTimestamp(0)
}
val remote2 = buildRecord(STORAGE_ID_C) {
setServiceId(PNI_A.toString())
setServicePni(PNI_A.toString())
setServiceE164(E164_A)
setAci(PNI_A.toString())
setPni(PNI_A.toString())
setE164(E164_A)
}
// WHEN
@ -86,7 +86,7 @@ class ContactRecordProcessorTest {
subject.process(listOf(remote1, remote2), StorageSyncHelper.KEY_GENERATOR)
// THEN
val byAci: RecipientId = SignalDatabase.recipients.getByServiceId(ACI_A).get()
val byAci: RecipientId = SignalDatabase.recipients.getByAci(ACI_A).get()
val byE164: RecipientId = SignalDatabase.recipients.getByE164(E164_A).get()
val byPni: RecipientId = SignalDatabase.recipients.getByPni(PNI_A).get()

View file

@ -27,7 +27,7 @@ class AliceClient(val serviceId: ServiceId, val e164: String, val trustRoot: ECK
private val aliceSenderCertificate = FakeClientHelpers.createCertificateFor(
trustRoot = trustRoot,
uuid = serviceId.uuid(),
uuid = serviceId.rawUuid,
e164 = e164,
deviceId = 1,
identityKey = SignalStore.account().aciIdentityKey.publicKey.publicKey,

View file

@ -50,7 +50,7 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
private val serviceAddress = SignalServiceAddress(serviceId, e164)
private val registrationId = KeyHelper.generateRegistrationId(false)
private val aciStore = BobSignalServiceAccountDataStore(registrationId, identityKeyPair)
private val senderCertificate = FakeClientHelpers.createCertificateFor(trustRoot, serviceId.uuid(), e164, 1, identityKeyPair.publicKey.publicKey, 31337)
private val senderCertificate = FakeClientHelpers.createCertificateFor(trustRoot, serviceId.rawUuid, e164, 1, identityKeyPair.publicKey.publicKey, 31337)
private val sessionLock = object : SignalSessionLock {
private val lock = ReentrantLock()

View file

@ -30,7 +30,7 @@ import org.thoughtcrime.securesms.testing.GroupTestingUtils.asMember
import org.thoughtcrime.securesms.util.SecurePreferenceManager
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.internal.ServiceResponse
import org.whispersystems.signalservice.internal.ServiceResponseProcessor

View file

@ -4,8 +4,8 @@ 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 org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import java.util.UUID
/**

View file

@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.testing
import com.google.protobuf.ByteString
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2
@ -17,7 +17,7 @@ class TestProtos private constructor() {
uuid: UUID = UUID.randomUUID()
): AddressProto.Builder {
return AddressProto.newBuilder()
.setUuid(ServiceId.from(uuid).toByteString())
.setUuid(ACI.from(uuid).toByteString())
}
fun metadata(

View file

@ -589,22 +589,11 @@
android:value="org.thoughtcrime.securesms.MainActivity" />
</activity>
<activity android:name=".conversation.ConversationActivity"
android:windowSoftInputMode="stateUnchanged"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:parentActivityName=".MainActivity"
android:exported="false">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.thoughtcrime.securesms.MainActivity" />
</activity>
<activity android:name=".conversation.BubbleConversationActivity"
android:theme="@style/Signal.DayNight"
android:allowEmbedded="true"
android:resizeableActivity="true"
android:exported="false"/>
android:theme="@style/Signal.DayNight"
android:allowEmbedded="true"
android:resizeableActivity="true"
android:exported="false"/>
<activity android:name=".conversation.ConversationPopupActivity"
android:windowSoftInputMode="stateVisible"

View file

@ -247,13 +247,8 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
for (SelectedContact contact : contacts) {
RecipientId recipientId = contact.getOrCreateRecipientId(context);
Recipient recipient = Recipient.resolved(recipientId);
int subscriptionId = recipient.getDefaultSubscriptionId().orElse(-1);
MessageSender.send(context, OutgoingMessage.sms(recipient, message, subscriptionId), -1L, MessageSender.SendType.SMS, null, null);
if (recipient.getContactUri() != null) {
SignalDatabase.recipients().setHasSentInvite(recipient.getId());
}
MessageSender.send(context, OutgoingMessage.sms(recipient, message), -1L, MessageSender.SendType.SMS, null, null);
}
return null;

View file

@ -367,6 +367,7 @@ public class NewConversationActivity extends ContactSelectionActivity
.setPositiveButton(R.string.NewConversationActivity__remove,
(dialog, which) -> {
disposables.add(viewModel.hideContact(recipient).subscribe(() -> {
onRefresh();
displaySnackbar(R.string.NewConversationActivity__s_has_been_removed, recipient.getDisplayName(this));
}));
}

View file

@ -62,6 +62,7 @@ import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
import org.thoughtcrime.securesms.components.webrtc.CallStateUpdatePopupWindow;
import org.thoughtcrime.securesms.components.webrtc.CallToastPopupWindow;
import org.thoughtcrime.securesms.components.webrtc.GroupCallSafetyNumberChangeNotificationUtil;
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioDevice;
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
@ -857,6 +858,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@Override
public void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput) {
maybeDisplaySpeakerphonePopup(audioOutput);
switch (audioOutput) {
case HANDSET:
handleSetAudioHandset();
@ -877,8 +879,9 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@RequiresApi(31)
@Override
public void onAudioOutputChanged31(@NonNull Integer audioDeviceInfo) {
ApplicationDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(audioDeviceInfo));
public void onAudioOutputChanged31(@NonNull WebRtcAudioDevice audioOutput) {
maybeDisplaySpeakerphonePopup(audioOutput.getWebRtcAudioOutput());
ApplicationDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(audioOutput.getDeviceId()));
}
@Override
@ -961,6 +964,15 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
}
}
private void maybeDisplaySpeakerphonePopup(WebRtcAudioOutput nextOutput) {
final WebRtcAudioOutput currentOutput = viewModel.getCurrentAudioOutput();
if (currentOutput == WebRtcAudioOutput.SPEAKER && nextOutput != WebRtcAudioOutput.SPEAKER) {
callStateUpdatePopupWindow.onCallStateUpdate(CallStateUpdatePopupWindow.CallStateUpdate.SPEAKER_OFF);
} else if (currentOutput != WebRtcAudioOutput.SPEAKER && nextOutput == WebRtcAudioOutput.SPEAKER) {
callStateUpdatePopupWindow.onCallStateUpdate(CallStateUpdatePopupWindow.CallStateUpdate.SPEAKER_ON);
}
}
private class WindowLayoutInfoConsumer implements Consumer<WindowLayoutInfo> {
@Override

View file

@ -64,11 +64,21 @@ public class AudioRecorder {
}
public @NonNull Single<VoiceNoteDraft> startRecording() {
Log.i(TAG, "startRecording()");
return startRecording(Build.VERSION.SDK_INT >= 26);
}
public @NonNull Single<VoiceNoteDraft> startRecording(final boolean useMediaCodecWrapper) {
Log.i(TAG, "startRecording(" + useMediaCodecWrapper + ")");
final SingleSubject<VoiceNoteDraft> recordingSingle = SingleSubject.create();
startRecordingInternal(useMediaCodecWrapper, recordingSingle);
return recordingSingle;
}
private void startRecordingInternal(boolean useMediaRecorderWrapper, SingleSubject<VoiceNoteDraft> recordingSingle) {
executor.execute(() -> {
Log.i(TAG, "Running startRecording() + " + Thread.currentThread().getId());
Log.i(TAG, "Running startRecording(" + useMediaRecorderWrapper + ") + " + Thread.currentThread().getId());
try {
if (recorder != null) {
recordingSingle.onError(new IllegalStateException("We can only do one recording at a time!"));
@ -82,7 +92,7 @@ public class AudioRecorder {
.withMimeType(MediaUtil.AUDIO_AAC)
.createForDraftAttachmentAsync(context);
recorder = Build.VERSION.SDK_INT >= 26 ? new MediaRecorderWrapper() : new AudioCodec();
recorder = useMediaRecorderWrapper ? new MediaRecorderWrapper() : new AudioCodec();
int focusResult = audioFocusManager.requestAudioFocus();
if (focusResult != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Log.w(TAG, "Could not gain audio focus. Received result code " + focusResult);
@ -90,13 +100,17 @@ public class AudioRecorder {
recorder.start(fds[1]);
this.recordingSubject = recordingSingle;
} catch (IOException | RuntimeException e) {
recordingSingle.onError(e);
recorder = null;
Log.w(TAG, e);
recordingUriFuture = null;
recorder = null;
audioFocusManager.abandonAudioFocus();
if (useMediaRecorderWrapper) {
startRecordingInternal(false, recordingSingle);
} else {
recordingSingle.onError(e);
}
}
});
return recordingSingle;
}
public void stopRecording() {

View file

@ -41,7 +41,7 @@ public class MediaRecorderWrapper implements Recorder {
recorder.setAudioChannels(CHANNELS);
recorder.prepare();
recorder.start();
} catch (IllegalStateException e) {
} catch (RuntimeException e) {
Log.w(TAG, "Unable to start recording", e);
recorder.release();
recorder = null;

View file

@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.database.DatabaseObserver
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
import org.thoughtcrime.securesms.util.FeatureFlags
import java.net.URLDecoder
/**
@ -53,6 +54,10 @@ object CallLinks {
@JvmStatic
fun isCallLink(url: String): Boolean {
if (FeatureFlags.adHocCalling()) {
return false
}
if (!url.startsWith(HTTPS_LINK_PREFIX) && !url.startsWith(SNGL_LINK_PREFIX)) {
Log.w(TAG, "Invalid url prefix.")
return false
@ -63,6 +68,10 @@ object CallLinks {
@JvmStatic
fun parseUrl(url: String): CallLinkRootKey? {
if (FeatureFlags.adHocCalling()) {
return null
}
if (!url.startsWith(HTTPS_LINK_PREFIX) && !url.startsWith(SNGL_LINK_PREFIX)) {
Log.w(TAG, "Invalid url prefix.")
return null

View file

@ -10,6 +10,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.ringrtc.CallLinkState
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.CallLinkUpdateSendJob
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkManager
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
@ -58,6 +59,7 @@ class UpdateCallLinkRepository(
return { result ->
if (result is UpdateCallLinkResult.Success) {
SignalDatabase.callLinks.updateCallLinkState(credentials.roomId, result.state)
ApplicationDependencies.getJobManager().add(CallLinkUpdateSendJob(credentials.roomId))
}
}
}

View file

@ -215,6 +215,13 @@ class CallLogAdapter(
binding.callInfo.setRelativeDrawables(start = R.drawable.symbol_link_compact_16)
binding.callInfo.setText(R.string.CallLogAdapter__call_link)
TextViewCompat.setCompoundDrawableTintList(
binding.callInfo,
ColorStateList.valueOf(
ContextCompat.getColor(context, R.color.signal_colorOnSurfaceVariant)
)
)
binding.callType.setImageResource(R.drawable.symbol_video_24)
binding.callType.setOnClickListener {
onStartVideoCallClicked(model.callLink.recipient)

View file

@ -5,11 +5,14 @@ import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.withinTransaction
import org.thoughtcrime.securesms.calls.links.UpdateCallLinkRepository
import org.thoughtcrime.securesms.database.CallLinkTable
import org.thoughtcrime.securesms.database.DatabaseObserver
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.CallLinkPeekJob
import org.thoughtcrime.securesms.jobs.CallLogEventSendJob
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
@ -80,6 +83,23 @@ class CallLogRepository(
}.subscribeOn(Schedulers.io())
}
/**
* Delete all call events / unowned links and enqueue clear history job, and then
* emit a clear history message.
*/
fun deleteAllCallLogsOnOrBeforeNow(): Single<Int> {
return Single.fromCallable {
SignalDatabase.rawDatabase.withinTransaction {
val now = System.currentTimeMillis()
SignalDatabase.calls.deleteNonAdHocCallEventsOnOrBefore(now)
SignalDatabase.callLinks.deleteNonAdminCallLinksOnOrBefore(now)
ApplicationDependencies.getJobManager().add(CallLogEventSendJob.forClearHistory(now))
}
SignalDatabase.callLinks.getAllAdminCallLinksExcept(emptySet())
}.flatMap(this::revokeAndCollectResults).map { -1 }.subscribeOn(Schedulers.io())
}
/**
* Deletes the selected call links. We DELETE those links we don't have admin keys for,
* and revoke the ones we *do* have admin keys for. We then perform a cleanup step on
@ -93,19 +113,7 @@ class CallLogRepository(
val allCallLinkIds = SignalDatabase.calls.getCallLinkRoomIdsFromCallRowIds(selectedCallRowIds) + selectedRoomIds
SignalDatabase.callLinks.deleteNonAdminCallLinks(allCallLinkIds)
SignalDatabase.callLinks.getAdminCallLinks(allCallLinkIds)
}.flatMap { callLinksToRevoke ->
Single.merge(
callLinksToRevoke.map {
updateCallLinkRepository.revokeCallLink(it.credentials!!)
}
).reduce(0) { acc, current ->
acc + (if (current is UpdateCallLinkResult.Success) 0 else 1)
}
}.doOnTerminate {
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
}.doOnDispose {
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
}.subscribeOn(Schedulers.io())
}.flatMap(this::revokeAndCollectResults).subscribeOn(Schedulers.io())
}
/**
@ -121,19 +129,21 @@ class CallLogRepository(
val allCallLinkIds = SignalDatabase.calls.getCallLinkRoomIdsFromCallRowIds(selectedCallRowIds) + selectedRoomIds
SignalDatabase.callLinks.deleteAllNonAdminCallLinksExcept(allCallLinkIds)
SignalDatabase.callLinks.getAllAdminCallLinksExcept(allCallLinkIds)
}.flatMap { callLinksToRevoke ->
Single.merge(
callLinksToRevoke.map {
updateCallLinkRepository.revokeCallLink(it.credentials!!)
}
).reduce(0) { acc, current ->
acc + (if (current is UpdateCallLinkResult.Success) 0 else 1)
}.flatMap(this::revokeAndCollectResults).subscribeOn(Schedulers.io())
}
private fun revokeAndCollectResults(callLinksToRevoke: Set<CallLinkTable.CallLink>): Single<Int> {
return Single.merge(
callLinksToRevoke.map {
updateCallLinkRepository.revokeCallLink(it.credentials!!)
}
).reduce(0) { acc, current ->
acc + (if (current is UpdateCallLinkResult.Success) 0 else 1)
}.doOnTerminate {
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
}.doOnDispose {
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
}.subscribeOn(Schedulers.io())
}
}
fun peekCallLinks(): Completable {

View file

@ -93,7 +93,7 @@ sealed class CallLogRow {
return FULL
}
if (groupCallUpdateDetails.inCallUuidsList.contains(Recipient.self().requireServiceId().uuid().toString())) {
if (groupCallUpdateDetails.inCallUuidsList.contains(Recipient.self().requireAci().rawUuid.toString())) {
return LOCAL_USER_JOINED
}

View file

@ -3,17 +3,17 @@ package org.thoughtcrime.securesms.calls.log
/**
* Selection state object for call logs.
*/
sealed class CallLogSelectionState {
abstract fun contains(callId: CallLogRow.Id): Boolean
abstract fun isNotEmpty(totalCount: Int): Boolean
sealed interface CallLogSelectionState {
fun contains(callId: CallLogRow.Id): Boolean
fun isNotEmpty(totalCount: Int): Boolean
abstract fun count(totalCount: Int): Int
fun count(totalCount: Int): Int
abstract fun selected(): Set<CallLogRow.Id>
fun selected(): Set<CallLogRow.Id>
fun isExclusionary(): Boolean = this is Excludes
protected abstract fun select(callId: CallLogRow.Id): CallLogSelectionState
protected abstract fun deselect(callId: CallLogRow.Id): CallLogSelectionState
fun select(callId: CallLogRow.Id): CallLogSelectionState
fun deselect(callId: CallLogRow.Id): CallLogSelectionState
fun toggle(callId: CallLogRow.Id): CallLogSelectionState {
return if (contains(callId)) {
@ -26,7 +26,7 @@ sealed class CallLogSelectionState {
/**
* Includes contains an opt-in list of call logs.
*/
data class Includes(private val includes: Set<CallLogRow.Id>) : CallLogSelectionState() {
data class Includes(private val includes: Set<CallLogRow.Id>) : CallLogSelectionState {
override fun contains(callId: CallLogRow.Id): Boolean {
return includes.contains(callId)
}
@ -55,7 +55,7 @@ sealed class CallLogSelectionState {
/**
* Excludes contains an opt-out list of call logs.
*/
data class Excludes(private val excluded: Set<CallLogRow.Id>) : CallLogSelectionState() {
data class Excludes(private val excluded: Set<CallLogRow.Id>) : CallLogSelectionState {
override fun contains(callId: CallLogRow.Id): Boolean = !excluded.contains(callId)
override fun isNotEmpty(totalCount: Int): Boolean = excluded.size < totalCount
@ -74,8 +74,10 @@ sealed class CallLogSelectionState {
override fun selected(): Set<CallLogRow.Id> = excluded
}
object All : CallLogSelectionState by Excludes(emptySet())
companion object {
fun empty(): CallLogSelectionState = Includes(emptySet())
fun selectAll(): CallLogSelectionState = Excludes(emptySet())
fun selectAll(): CallLogSelectionState = All
}
}

View file

@ -35,14 +35,21 @@ class CallLogStagedDeletion(
.map { it.roomId }
.toSet()
return if (stateSnapshot.isExclusionary()) {
repository.deleteAllCallLogsExcept(callRowIds, filter == CallLogFilter.MISSED).andThen(
repository.deleteAllCallLinksExcept(callRowIds, callLinkIds)
)
} else {
repository.deleteSelectedCallLogs(callRowIds).andThen(
repository.deleteSelectedCallLinks(callRowIds, callLinkIds)
)
return when {
stateSnapshot is CallLogSelectionState.All && filter == CallLogFilter.ALL -> {
repository.deleteAllCallLogsOnOrBeforeNow()
}
stateSnapshot is CallLogSelectionState.Excludes || stateSnapshot is CallLogSelectionState.All -> {
repository.deleteAllCallLogsExcept(callRowIds, filter == CallLogFilter.MISSED).andThen(
repository.deleteAllCallLinksExcept(callRowIds, callLinkIds)
)
}
stateSnapshot is CallLogSelectionState.Includes -> {
repository.deleteSelectedCallLogs(callRowIds).andThen(
repository.deleteSelectedCallLinks(callRowIds, callLinkIds)
)
}
else -> error("Unhandled state $stateSnapshot $filter")
}
}
}

View file

@ -325,7 +325,7 @@ public class ConversationItemFooter extends ConstraintLayout {
}
}
String date = DateUtils.getSimpleRelativeTimeSpanString(getContext(), locale, timestamp);
if (displayMode != ConversationItemDisplayMode.DETAILED && messageRecord.isEditMessage()) {
if (displayMode != ConversationItemDisplayMode.DETAILED && messageRecord.isEditMessage() && messageRecord.isLatestRevision()) {
date = getContext().getString(R.string.ConversationItem_edited_timestamp_footer, date);
}
dateView.setText(date);

View file

@ -48,9 +48,13 @@ public class FromTextView extends SimpleEmojiTextView {
}
public void setText(Recipient recipient, @Nullable CharSequence fromString, boolean read, @Nullable String suffix) {
setText(recipient, fromString, read, suffix, true);
}
public void setText(Recipient recipient, @Nullable CharSequence fromString, boolean read, @Nullable String suffix, boolean asThread) {
SpannableStringBuilder builder = new SpannableStringBuilder();
if (recipient.isSelf()) {
if (asThread && recipient.isSelf()) {
builder.append(getContext().getString(R.string.note_to_self));
} else {
builder.append(fromString);
@ -60,7 +64,7 @@ public class FromTextView extends SimpleEmojiTextView {
builder.append(suffix);
}
if (recipient.showVerified()) {
if (asThread && recipient.showVerified()) {
Drawable official = ContextUtil.requireDrawable(getContext(), R.drawable.ic_official_20);
official.setBounds(0, 0, ViewUtil.dpToPx(20), ViewUtil.dpToPx(20));

View file

@ -12,8 +12,6 @@ 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.
@ -25,7 +23,6 @@ class SendButton(context: Context, attributeSet: AttributeSet?) : AppCompatImage
private val TAG = Log.tag(SendButton::class.java)
}
private val listeners: MutableList<SendTypeChangedListener> = CopyOnWriteArrayList()
private var scheduledSendListener: ScheduledSendListener? = null
private var availableSendTypes: List<MessageSendType> = MessageSendType.getAllAvailable(context)
@ -40,16 +37,10 @@ class SendButton(context: Context, attributeSet: AttributeSet?) : AppCompatImage
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
private val selectedSendType: MessageSendType
get() {
activeMessageSendType?.let {
return it
@ -67,61 +58,33 @@ class SendButton(context: Context, attributeSet: AttributeSet?) : AppCompatImage
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)
onSelectionChanged(signalType)
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)
onSelectionChanged(MessageSendType.SignalMessageSendType)
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)
onSelectionChanged(newType = selectedSendType)
}
fun setScheduledSendListener(listener: ScheduledSendListener?) {
this.scheduledSendListener = listener
}
fun resetAvailableTransports() {
setSendType(null)
}
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?) {
private 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)
onSelectionChanged(newType = selectedSendType)
}
/**
@ -131,22 +94,9 @@ class SendButton(context: Context, attributeSet: AttributeSet?) : AppCompatImage
popupContainer = container
}
private fun onSelectionChanged(newType: MessageSendType, isManualSelection: Boolean) {
private fun onSelectionChanged(newType: MessageSendType) {
setImageResource(newType.buttonDrawableRes)
contentDescription = context.getString(newType.titleRes)
for (listener in listeners) {
listener.onSendTypeChanged(newType, isManualSelection)
}
}
fun showSendTypeMenu(): Boolean {
return if (availableSendTypes.size == 1) {
false
} else {
showSendTypeContextMenu(false)
true
}
}
override fun onLongClick(v: View): Boolean {
@ -195,10 +145,6 @@ class SendButton(context: Context, attributeSet: AttributeSet?) : AppCompatImage
.show(items)
}
fun interface SendTypeChangedListener {
fun onSendTypeChanged(newType: MessageSendType, manuallySelected: Boolean)
}
interface ScheduledSendListener {
fun onSendScheduled()
fun canSchedule(): Boolean

View file

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.components.emoji;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
@ -18,6 +19,8 @@ import android.text.method.TransformationMethod;
import android.text.style.CharacterStyle;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ViewGroup;
import androidx.annotation.ColorInt;
@ -25,6 +28,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.core.content.ContextCompat;
import androidx.core.view.GestureDetectorCompat;
import androidx.core.view.ViewKt;
import androidx.core.widget.TextViewCompat;
@ -315,6 +319,12 @@ public class EmojiTextView extends AppCompatTextView {
}
}
@SuppressLint("ClickableViewAccessibility")
public void bindGestureListener() {
GestureDetectorCompat gestureDetectorCompat = new GestureDetectorCompat(getContext(), new OnGestureListener());
setOnTouchListener((v, event) -> gestureDetectorCompat.onTouchEvent(event));
}
private void ellipsizeAnyTextForMaxLength() {
if (maxLength > 0 && getText().length() > maxLength + 1) {
SpannableStringBuilder newContent = new SpannableStringBuilder();
@ -465,4 +475,32 @@ public class EmojiTextView extends AppCompatTextView {
mentionRendererDelegate.setTint(mentionBackgroundTint);
}
}
/**
* Due to some peculiarities in how TextView deals with touch events, it's really easy to accidentally trigger
* a click (say, when you try to scroll but you're at the bottom of a view.) Because of this, we handle these
* events manually.
*/
private class OnGestureListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onDown(@NonNull MotionEvent e) {
return true;
}
@Override
public boolean onScroll(@NonNull MotionEvent e1, @NonNull MotionEvent e2, float distanceX, float distanceY) {
if (!canScrollVertically((int) distanceY)) {
return true;
}
int maxScrollDistance = computeVerticalScrollRange() - computeHorizontalScrollExtent();
scrollTo(0, Util.clamp(getScrollY() + (int) distanceY, 0, maxScrollDistance));
return true;
}
@Override
public boolean onSingleTapConfirmed(@NonNull MotionEvent e) {
return performClick();
}
}
}

View file

@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
import org.thoughtcrime.securesms.util.DynamicTheme
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import java.util.Objects
private val TAG: String = Log.tag(ChangeNumberLockActivity::class.java)
@ -60,7 +60,7 @@ class ChangeNumberLockActivity : PassphraseRequiredActivity() {
Log.i(TAG, "Local (${SignalStore.account().e164}) and remote (${whoAmI.number}) numbers do not match, updating local.")
Single
.just(true)
.flatMap { changeNumberRepository.changeLocalNumber(whoAmI.number, PNI.parseOrThrow(whoAmI.pni)) }
.flatMap { changeNumberRepository.changeLocalNumber(whoAmI.number, PNI.parseUnPrefixedOrThrow(whoAmI.pni)) }
.compose(ChangeNumberRepository::acquireReleaseChangeNumberLock)
.map { true }
}

View file

@ -34,8 +34,7 @@ import org.whispersystems.signalservice.api.SvrNoDataException
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest
import org.whispersystems.signalservice.api.account.PreKeyUpload
import org.whispersystems.signalservice.api.kbs.MasterKey
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import org.whispersystems.signalservice.api.push.ServiceIdType
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity
@ -243,7 +242,7 @@ class ChangeNumberRepository(
throw AssertionError("No change number metadata")
}
val originalPni = ServiceId.fromByteString(metadata.previousPni)
val originalPni = PNI.parseOrThrow(metadata.previousPni)
if (originalPni == pni) {
Log.i(TAG, "No change has occurred, PNI is unchanged: $pni")

View file

@ -27,7 +27,7 @@ import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewMod
import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
import org.thoughtcrime.securesms.util.DefaultValueLiveData
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import org.whispersystems.signalservice.api.push.exceptions.IncorrectCodeException
import org.whispersystems.signalservice.internal.ServiceResponse
import java.util.Objects
@ -184,7 +184,7 @@ class ChangeNumberViewModel(
@WorkerThread
override fun onVerifySuccess(processor: VerifyResponseProcessor): Single<VerifyResponseProcessor> {
return changeNumberRepository.changeLocalNumber(number.e164Number, PNI.parseOrThrow(processor.result.verifyAccountResponse.pni))
return changeNumberRepository.changeLocalNumber(number.e164Number, PNI.parseUnPrefixedOrThrow(processor.result.verifyAccountResponse.pni))
.map { processor }
.onErrorReturn { t ->
Log.w(TAG, "Error attempting to change local number", t)
@ -193,7 +193,7 @@ class ChangeNumberViewModel(
}
override fun onVerifySuccessWithRegistrationLock(processor: VerifyResponseWithRegistrationLockProcessor, pin: String): Single<VerifyResponseWithRegistrationLockProcessor> {
return changeNumberRepository.changeLocalNumber(number.e164Number, PNI.parseOrThrow(processor.result.verifyAccountResponse.pni))
return changeNumberRepository.changeLocalNumber(number.e164Number, PNI.parseUnPrefixedOrThrow(processor.result.verifyAccountResponse.pni))
.map { processor }
.onErrorReturn { t ->
Log.w(TAG, "Error attempting to change local number", t)

View file

@ -8,7 +8,6 @@ package org.thoughtcrime.securesms.components.settings.app.internal.search
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.CompositeDisposable
@ -46,7 +45,7 @@ class InternalSearchViewModel : ViewModel() {
InternalSearchResult(
id = record.id,
name = record.displayName(),
aci = record.serviceId?.toString(),
aci = record.aci?.toString(),
pni = record.pni.toString(),
groupId = record.groupId
)
@ -70,10 +69,10 @@ class InternalSearchViewModel : ViewModel() {
private fun RecipientRecord.displayName(): String {
return when {
this.groupType == RecipientTable.GroupType.SIGNAL_V1 -> "GV1::${this.groupId}"
this.groupType == RecipientTable.GroupType.SIGNAL_V2 -> "GV2::${this.groupId}"
this.groupType == RecipientTable.GroupType.MMS -> "MMS_GROUP::${this.groupId}"
this.groupType == RecipientTable.GroupType.DISTRIBUTION_LIST -> "DLIST::${this.distributionListId}"
this.recipientType == RecipientTable.RecipientType.GV1 -> "GV1::${this.groupId}"
this.recipientType == RecipientTable.RecipientType.GV2 -> "GV2::${this.groupId}"
this.recipientType == RecipientTable.RecipientType.MMS -> "MMS_GROUP::${this.groupId}"
this.recipientType == RecipientTable.RecipientType.DISTRIBUTION_LIST -> "DLIST::${this.distributionListId}"
this.systemDisplayName?.isNotBlank() == true -> this.systemDisplayName
this.signalProfileName.toString().isNotBlank() -> this.signalProfileName.serialize()
this.e164 != null -> this.e164

View file

@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.UsernameUtil
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException
import java.io.IOException

View file

@ -153,8 +153,8 @@ class ConversationSettingsRepository(
if (groupRecord.isV2Group) {
val decryptedGroup: DecryptedGroup = groupRecord.requireV2GroupProperties().decryptedGroup
val pendingMembers: List<RecipientId> = decryptedGroup.pendingMembersList
.map(DecryptedPendingMember::getUuid)
.map(GroupProtoUtil::uuidByteStringToRecipientId)
.map(DecryptedPendingMember::getServiceIdBinary)
.map(GroupProtoUtil::serviceIdBinaryToRecipientId)
val members = mutableListOf<RecipientId>()

View file

@ -165,8 +165,11 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
.setTitle("Are you sure?")
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
.setPositiveButton(android.R.string.ok) { _, _ ->
if (recipient.hasServiceId()) {
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account().requireAci(), addressName = recipient.requireServiceId().toString())
if (recipient.hasAci()) {
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account().requireAci(), addressName = recipient.requireAci().toString())
}
if (recipient.hasPni()) {
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account().requireAci(), addressName = recipient.requirePni().toString())
}
}
.show()
@ -182,14 +185,25 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
.setTitle("Are you sure?")
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
.setPositiveButton(android.R.string.ok) { _, _ ->
SignalDatabase.threads.deleteConversation(SignalDatabase.threads.getThreadIdIfExistsFor(recipient.id))
if (recipient.hasServiceId()) {
SignalDatabase.recipients.debugClearServiceIds(recipient.id)
SignalDatabase.recipients.debugClearProfileData(recipient.id)
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account().requireAci(), addressName = recipient.requireServiceId().toString())
ApplicationDependencies.getProtocolStore().aci().identities().delete(recipient.requireServiceId().toString())
ApplicationDependencies.getProtocolStore().pni().identities().delete(recipient.requireServiceId().toString())
SignalDatabase.threads.deleteConversation(SignalDatabase.threads.getThreadIdIfExistsFor(recipient.id))
}
if (recipient.hasAci()) {
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account().requireAci(), addressName = recipient.requireAci().toString())
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account().requirePni(), addressName = recipient.requireAci().toString())
ApplicationDependencies.getProtocolStore().aci().identities().delete(recipient.requireAci().toString())
}
if (recipient.hasPni()) {
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account().requireAci(), addressName = recipient.requirePni().toString())
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account().requirePni(), addressName = recipient.requirePni().toString())
ApplicationDependencies.getProtocolStore().aci().identities().delete(recipient.requirePni().toString())
}
startActivity(MainActivity.clearTop(requireContext()))
}
.show()
@ -237,7 +251,7 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
SignalDatabase.recipients.debugClearE164AndPni(recipient.id)
val splitRecipientId: RecipientId = if (FeatureFlags.phoneNumberPrivacy()) {
SignalDatabase.recipients.getAndPossiblyMergePnpVerified(recipient.pni.orElse(null), recipient.pni.orElse(null), recipient.requireE164())
SignalDatabase.recipients.getAndPossiblyMergePnpVerified(null, recipient.pni.orElse(null), recipient.requireE164())
} else {
SignalDatabase.recipients.getAndPossiblyMerge(recipient.pni.orElse(null), recipient.requireE164())
}
@ -281,7 +295,7 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
SignalDatabase.recipients.debugRemoveAci(recipient.id)
val aciRecipientId: RecipientId = SignalDatabase.recipients.getAndPossiblyMergePnpVerified(recipient.requireServiceId(), null, null)
val aciRecipientId: RecipientId = SignalDatabase.recipients.getAndPossiblyMergePnpVerified(recipient.requireAci(), null, null)
recipient.profileKey?.let { profileKey ->
SignalDatabase.recipients.setProfileKey(aciRecipientId, ProfileKey(profileKey))

View file

@ -114,6 +114,8 @@ class CallStateUpdatePopupWindow(private val parent: ViewGroup) : PopupWindow(
RINGING_OFF(R.drawable.symbol_bell_slash_compact_16, R.string.CallStateUpdatePopupWindow__ringing_off),
RINGING_DISABLED(null, R.string.CallStateUpdatePopupWindow__group_is_too_large),
MIC_ON(R.drawable.symbol_mic_compact_16, R.string.CallStateUpdatePopupWindow__mic_on),
MIC_OFF(R.drawable.symbol_mic_slash_compact_16, R.string.CallStateUpdatePopupWindow__mic_off)
MIC_OFF(R.drawable.symbol_mic_slash_compact_16, R.string.CallStateUpdatePopupWindow__mic_off),
SPEAKER_ON(R.drawable.symbol_speaker_24, R.string.CallStateUpdatePopupWindow__speaker_on),
SPEAKER_OFF(R.drawable.symbol_speaker_slash_24, R.string.CallStateUpdatePopupWindow__speaker_off)
}
}

View file

@ -248,9 +248,8 @@ public class WebRtcCallView extends ConstraintLayout {
runIfNonNull(controlsListener, listener ->
{
if (Build.VERSION.SDK_INT >= 31) {
final Integer deviceId = webRtcAudioDevice.getDeviceId();
if (deviceId != null) {
listener.onAudioOutputChanged31(deviceId);
if (webRtcAudioDevice.getDeviceId() != null) {
listener.onAudioOutputChanged31(webRtcAudioDevice);
} else {
Log.e(TAG, "Attempted to change audio output to null device ID.");
}
@ -1102,7 +1101,7 @@ public class WebRtcCallView extends ConstraintLayout {
void hideSystemUI();
void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput);
@RequiresApi(31)
void onAudioOutputChanged31(@NonNull Integer audioOutputAddress);
void onAudioOutputChanged31(@NonNull WebRtcAudioDevice audioOutput);
void onVideoChanged(boolean isVideoEnabled);
void onMicChanged(boolean isMicEnabled);
void onCameraDirectionChanged();

View file

@ -163,6 +163,10 @@ public class WebRtcCallViewModel extends ViewModel {
return shouldShowSpeakerHint;
}
public WebRtcAudioOutput getCurrentAudioOutput() {
return getWebRtcControls().getValue().getAudioOutput();
}
public LiveData<WebRtcEphemeralState> getEphemeralState() {
return ephemeralState;
}

View file

@ -63,7 +63,7 @@ public class ContactRepository {
}));
add(new Pair<>(NUMBER_COLUMN, cursor -> {
String phone = CursorUtil.requireString(cursor, RecipientTable.PHONE);
String phone = CursorUtil.requireString(cursor, RecipientTable.E164);
String email = CursorUtil.requireString(cursor, RecipientTable.EMAIL);
if (phone != null) {

View file

@ -573,7 +573,7 @@ open class ContactSearchAdapter(
}
private fun isSmsContact(model: T): Boolean {
return (getRecipient(model).isForceSmsSelection || getRecipient(model).isUnregistered) && !getRecipient(model).isDistributionList
return getRecipient(model).isUnregistered && !getRecipient(model).isDistributionList
}
private fun isNotRegistered(model: T): Boolean {

View file

@ -50,12 +50,12 @@ class SafetyNumberRepository(
if (recipients.isNotEmpty()) {
Log.i(TAG, "Checking on ${recipients.size} identities...")
val requests: List<Single<List<IdentityCheckResponse.AciIdentityPair>>> = recipients.chunked(batchSize) { it.createBatchRequestSingle() }
val requests: List<Single<List<IdentityCheckResponse.ServiceIdentityPair>>> = recipients.chunked(batchSize) { it.createBatchRequestSingle() }
stopwatch.split("requests")
val aciKeyPairs: List<IdentityCheckResponse.AciIdentityPair> = Single.zip(requests) { responses ->
val aciKeyPairs: List<IdentityCheckResponse.ServiceIdentityPair> = Single.zip(requests) { responses ->
responses
.map { it as List<IdentityCheckResponse.AciIdentityPair> }
.map { it as List<IdentityCheckResponse.ServiceIdentityPair> }
.flatten()
}.safeBlockingGet()
@ -65,8 +65,8 @@ class SafetyNumberRepository(
Log.d(TAG, "No identity key mismatches")
} else {
aciKeyPairs
.filter { it.aci != null && it.identityKey != null }
.forEach { IdentityUtil.saveIdentity(it.aci.toString(), it.identityKey) }
.filter { it.serviceId != null && it.identityKey != null }
.forEach { IdentityUtil.saveIdentity(it.serviceId.toString(), it.identityKey) }
}
recentlyFetched += recipients.associate { it.id to now }
stopwatch.split("saving-identities")
@ -95,7 +95,7 @@ class SafetyNumberRepository(
.apply { remove(Recipient.self().id) }
}
private fun List<Recipient>.createBatchRequestSingle(): Single<List<IdentityCheckResponse.AciIdentityPair>> {
private fun List<Recipient>.createBatchRequestSingle(): Single<List<IdentityCheckResponse.ServiceIdentityPair>> {
return profileService
.performIdentityCheck(
mapNotNull { r ->
@ -107,7 +107,7 @@ class SafetyNumberRepository(
}
}.associate { it }
)
.map { ServiceResponseProcessor.DefaultProcessor(it).resultOrThrow.aciKeyPairs ?: emptyList() }
.map { ServiceResponseProcessor.DefaultProcessor(it).resultOrThrow.serviceIdKeyPairs ?: emptyList() }
.onErrorReturn { t ->
Log.w(TAG, "Unable to fetch identities", t)
emptyList()

View file

@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.FeatureFlags
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException
import org.whispersystems.signalservice.api.push.exceptions.CdsiResourceExhaustedException
import org.whispersystems.signalservice.api.services.CdsiV2Service

View file

@ -4,7 +4,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.whispersystems.signalservice.api.push.ACI;
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import java.util.Arrays;
import java.util.Collection;

View file

@ -1,30 +0,0 @@
package org.thoughtcrime.securesms.conversation;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import org.thoughtcrime.securesms.util.ViewUtil;
/**
* Activity which encapsulates a conversation for a Bubble window.
*
* This activity exists so that we can override some of its manifest parameters
* without clashing with {@link ConversationActivity} and provide an API-level
* independent "is in bubble?" check.
*/
public class BubbleConversationActivity extends ConversationActivity {
@Override
public boolean isInBubble() {
return true;
}
@Override
protected void onPause() {
super.onPause();
ViewUtil.hideKeyboard(this, getComposeText());
}
@Override
public void onInitializeToolbar(@NonNull Toolbar toolbar) {
}
}

View file

@ -0,0 +1,19 @@
package org.thoughtcrime.securesms.conversation
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.conversation.v2.ConversationActivity
import org.thoughtcrime.securesms.util.ViewUtil
/**
* Activity which encapsulates a conversation for a Bubble window.
*
* This activity exists so that we can override some of its manifest parameters
* without clashing with [ConversationActivity] and provide an API-level
* independent "is in bubble?" check.
*/
class BubbleConversationActivity : ConversationActivity() {
override fun onPause() {
super.onPause()
ViewUtil.hideKeyboard(this, findViewById(R.id.fragment_container))
}
}

View file

@ -1,122 +0,0 @@
package org.thoughtcrime.securesms.conversation
import android.content.Intent
import android.os.Bundle
import android.view.MotionEvent
import android.view.View
import android.view.Window
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.Toolbar
import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.HidingLinearLayout
import org.thoughtcrime.securesms.components.reminder.ReminderView
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.Debouncer
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
import org.thoughtcrime.securesms.util.DynamicTheme
import org.thoughtcrime.securesms.util.views.Stub
import java.util.concurrent.TimeUnit
open class ConversationActivity : PassphraseRequiredActivity(), ConversationParentFragment.Callback {
companion object {
private const val STATE_WATERMARK = "share_data_watermark"
}
private val transitionDebouncer: Debouncer = Debouncer(150, TimeUnit.MILLISECONDS)
private lateinit var fragment: ConversationParentFragment
private var shareDataTimestamp: Long = -1L
private val dynamicTheme: DynamicTheme = DynamicNoActionBarTheme()
override fun onPreCreate() {
dynamicTheme.onCreate(this)
}
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
supportPostponeEnterTransition()
transitionDebouncer.publish { supportStartPostponedEnterTransition() }
window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
if (savedInstanceState != null) {
shareDataTimestamp = savedInstanceState.getLong(STATE_WATERMARK, -1L)
} else if (intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0) {
shareDataTimestamp = System.currentTimeMillis()
}
setContentView(R.layout.conversation_parent_fragment_container)
if (savedInstanceState == null) {
replaceFragment(intent!!)
} else {
fragment = supportFragmentManager.findFragmentById(R.id.fragment_container) as ConversationParentFragment
}
}
override fun onDestroy() {
super.onDestroy()
transitionDebouncer.clear()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putLong(STATE_WATERMARK, shareDataTimestamp)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
setIntent(intent)
replaceFragment(intent!!)
}
private fun replaceFragment(intent: Intent) {
fragment = ConversationParentFragment.create(intent)
supportFragmentManager
.beginTransaction()
.replace(R.id.fragment_container, fragment)
.disallowAddToBackStack()
.commitNowAllowingStateLoss()
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
return fragment.dispatchTouchEvent(ev) || super.dispatchTouchEvent(ev)
}
override fun onResume() {
super.onResume()
dynamicTheme.onResume(this)
}
override fun getShareDataTimestamp(): Long {
return shareDataTimestamp
}
override fun setShareDataTimestamp(timestamp: Long) {
shareDataTimestamp = timestamp
}
override fun onInitializeToolbar(toolbar: Toolbar) {
toolbar.navigationIcon = AppCompatResources.getDrawable(this, R.drawable.ic_arrow_left_24)
toolbar.setNavigationOnClickListener { finish() }
}
fun getRecipient(): Recipient {
return fragment.recipient
}
fun getTitleView(): View {
return fragment.titleView
}
fun getComposeText(): View {
return fragment.composeText
}
fun getQuickAttachmentToggle(): HidingLinearLayout {
return fragment.quickAttachmentToggle
}
fun getReminderView(): Stub<ReminderView> {
return fragment.reminderView
}
}

View file

@ -1,256 +0,0 @@
package org.thoughtcrime.securesms.conversation;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import org.signal.core.util.Stopwatch;
import org.signal.core.util.logging.Log;
import org.signal.paging.PagedDataSource;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.conversation.ConversationData.MessageRequestData;
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory;
import org.thoughtcrime.securesms.conversation.v2.data.AttachmentHelper;
import org.thoughtcrime.securesms.conversation.v2.data.CallHelper;
import org.thoughtcrime.securesms.conversation.v2.data.MentionHelper;
import org.thoughtcrime.securesms.conversation.v2.data.PaymentHelper;
import org.thoughtcrime.securesms.conversation.v2.data.QuotedHelper;
import org.thoughtcrime.securesms.conversation.v2.data.ReactionHelper;
import org.thoughtcrime.securesms.database.CallTable;
import org.thoughtcrime.securesms.database.MessageTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.ReactionRecord;
import org.thoughtcrime.securesms.database.model.UpdateDescription;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.whispersystems.signalservice.api.push.ServiceId;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Core data source for loading an individual conversation.
*/
public class ConversationDataSource implements PagedDataSource<MessageId, ConversationMessage> {
private static final String TAG = Log.tag(ConversationDataSource.class);
private final Context context;
private final long threadId;
private final MessageRequestData messageRequestData;
private final boolean showUniversalExpireTimerUpdate;
/** Used once for the initial fetch, then cleared. */
private int baseSize;
private final Recipient threadRecipient;
public ConversationDataSource(
@NonNull Context context,
long threadId,
@NonNull MessageRequestData messageRequestData,
boolean showUniversalExpireTimerUpdate,
int baseSize,
@NonNull Recipient threadRecipient
) {
this.context = context;
this.threadId = threadId;
this.messageRequestData = messageRequestData;
this.showUniversalExpireTimerUpdate = showUniversalExpireTimerUpdate;
this.baseSize = baseSize;
this.threadRecipient = threadRecipient;
}
@Override
public int size() {
long startTime = System.currentTimeMillis();
int size = getSizeInternal() +
(messageRequestData.includeWarningUpdateMessage() ? 1 : 0) +
(messageRequestData.isHidden() ? 1 : 0) +
(showUniversalExpireTimerUpdate ? 1 : 0);
Log.d(TAG, "[size(), thread " + threadId + "] " + (System.currentTimeMillis() - startTime) + " ms");
return size;
}
private int getSizeInternal() {
synchronized (this) {
if (baseSize != -1) {
int size = baseSize;
baseSize = -1;
return size;
}
}
return SignalDatabase.messages().getMessageCountForThread(threadId);
}
@Override
public @NonNull List<ConversationMessage> load(int start, int length, int totalSize, @NonNull CancellationSignal cancellationSignal) {
Stopwatch stopwatch = new Stopwatch("load(" + start + ", " + length + "), thread " + threadId);
List<MessageRecord> records = new ArrayList<>(length);
MentionHelper mentionHelper = new MentionHelper();
QuotedHelper quotedHelper = new QuotedHelper();
AttachmentHelper attachmentHelper = new AttachmentHelper();
ReactionHelper reactionHelper = new ReactionHelper();
PaymentHelper paymentHelper = new PaymentHelper();
CallHelper callHelper = new CallHelper();
Set<ServiceId> referencedIds = new HashSet<>();
try (MessageTable.Reader reader = MessageTable.mmsReaderFor(SignalDatabase.messages().getConversation(threadId, start, length))) {
MessageRecord record;
while ((record = reader.getNext()) != null && !cancellationSignal.isCanceled()) {
records.add(record);
mentionHelper.add(record);
quotedHelper.add(record);
reactionHelper.add(record);
attachmentHelper.add(record);
paymentHelper.add(record);
callHelper.add(record);
UpdateDescription description = record.getUpdateDisplayBody(context, null);
if (description != null) {
referencedIds.addAll(description.getMentioned());
}
}
}
if (messageRequestData.includeWarningUpdateMessage() && (start + length >= totalSize)) {
records.add(new InMemoryMessageRecord.NoGroupsInCommon(threadId, messageRequestData.isGroup()));
}
if (messageRequestData.isHidden() && (start + length >= totalSize)) {
records.add(new InMemoryMessageRecord.RemovedContactHidden(threadId));
}
if (showUniversalExpireTimerUpdate) {
records.add(new InMemoryMessageRecord.UniversalExpireTimerUpdate(threadId));
}
stopwatch.split("messages");
mentionHelper.fetchMentions(context);
stopwatch.split("mentions");
quotedHelper.fetchQuotedState();
stopwatch.split("is-quoted");
reactionHelper.fetchReactions();
stopwatch.split("reactions");
records = reactionHelper.buildUpdatedModels(records);
stopwatch.split("reaction-models");
attachmentHelper.fetchAttachments();
stopwatch.split("attachments");
records = attachmentHelper.buildUpdatedModels(context, records);
stopwatch.split("attachment-models");
paymentHelper.fetchPayments();
stopwatch.split("payments");
records = paymentHelper.buildUpdatedModels(records);
stopwatch.split("payment-models");
callHelper.fetchCalls();
stopwatch.split("calls");
records = callHelper.buildUpdatedModels(records);
stopwatch.split("call-models");
for (ServiceId serviceId : referencedIds) {
Recipient.resolved(RecipientId.from(serviceId));
}
stopwatch.split("recipient-resolves");
List<ConversationMessage> messages = Stream.of(records)
.map(m -> ConversationMessageFactory.createWithUnresolvedData(context, m, m.getDisplayBody(context), mentionHelper.getMentions(m.getId()), quotedHelper.isQuoted(m.getId()), threadRecipient))
.toList();
stopwatch.split("conversion");
stopwatch.stop(TAG);
return messages;
}
@Override
public @Nullable ConversationMessage load(@NonNull MessageId messageId) {
Stopwatch stopwatch = new Stopwatch("load(" + messageId + "), thread " + threadId);
MessageRecord record = SignalDatabase.messages().getMessageRecordOrNull(messageId.getId());
if (record instanceof MediaMmsMessageRecord &&
((MediaMmsMessageRecord) record).getParentStoryId() != null &&
((MediaMmsMessageRecord) record).getParentStoryId().isGroupReply()) {
return null;
}
if (record instanceof MediaMmsMessageRecord && ((MediaMmsMessageRecord) record).getScheduledDate() != -1) {
return null;
}
stopwatch.split("message");
try {
if (record != null) {
List<Mention> mentions = SignalDatabase.mentions().getMentionsForMessage(messageId.getId());
stopwatch.split("mentions");
boolean isQuoted = SignalDatabase.messages().isQuoted(record);
stopwatch.split("is-quoted");
List<ReactionRecord> reactions = SignalDatabase.reactions().getReactions(messageId);
record = ReactionHelper.recordWithReactions(record, reactions);
stopwatch.split("reactions");
List<DatabaseAttachment> attachments = SignalDatabase.attachments().getAttachmentsForMessage(messageId.getId());
if (attachments.size() > 0) {
record = ((MediaMmsMessageRecord) record).withAttachments(context, attachments);
}
stopwatch.split("attachments");
if (record.isPaymentNotification()) {
record = SignalDatabase.payments().updateMessageWithPayment(record);
}
stopwatch.split("payments");
if (record.isCallLog() && !record.isGroupCall()) {
CallTable.Call call = SignalDatabase.calls().getCallByMessageId(record.getId());
if (call != null && record instanceof MediaMmsMessageRecord) {
record = ((MediaMmsMessageRecord) record).withCall(call);
}
}
stopwatch.split("calls");
return ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(ApplicationDependencies.getApplication(),
record,
record.getDisplayBody(ApplicationDependencies.getApplication()),
mentions,
isQuoted,
threadRecipient);
} else {
return null;
}
} finally {
stopwatch.stop(TAG);
}
}
@Override
public @NonNull MessageId getKey(@NonNull ConversationMessage conversationMessage) {
return new MessageId(conversationMessage.getMessageRecord().getId());
}
}

View file

@ -1,293 +0,0 @@
package org.thoughtcrime.securesms.conversation;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import com.annimon.stream.Stream;
import org.signal.core.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.database.GroupTable;
import org.thoughtcrime.securesms.database.model.GroupRecord;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.groups.GroupsV1MigrationUtil;
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.invite.GroupLinkInviteFriendsBottomSheetDialogFragment;
import org.thoughtcrime.securesms.groups.v2.GroupBlockJoinRequestResult;
import org.thoughtcrime.securesms.groups.v2.GroupManagementRepository;
import org.thoughtcrime.securesms.profiles.spoofing.ReviewRecipient;
import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.AsynchronousCallback;
import org.signal.core.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Single;
final class ConversationGroupViewModel extends ViewModel {
private final MutableLiveData<Recipient> liveRecipient;
private final LiveData<GroupActiveState> groupActiveState;
private final LiveData<ConversationMemberLevel> selfMembershipLevel;
private final LiveData<Integer> actionableRequestingMembers;
private final LiveData<ReviewState> reviewState;
private final LiveData<List<RecipientId>> gv1MigrationSuggestions;
private final GroupManagementRepository groupManagementRepository;
private boolean firstTimeInviteFriendsTriggered;
private ConversationGroupViewModel() {
this.liveRecipient = new MutableLiveData<>();
this.groupManagementRepository = new GroupManagementRepository();
LiveData<GroupRecord> groupRecord = LiveDataUtil.mapAsync(liveRecipient, ConversationGroupViewModel::getGroupRecordForRecipient);
LiveData<List<Recipient>> duplicates = LiveDataUtil.mapAsync(groupRecord, record -> {
if (record != null && record.isV2Group()) {
return Stream.of(ReviewUtil.getDuplicatedRecipients(record.getId().requireV2()))
.map(ReviewRecipient::getRecipient)
.toList();
} else {
return Collections.emptyList();
}
});
this.groupActiveState = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToGroupActiveState));
this.selfMembershipLevel = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToSelfMembershipLevel));
this.actionableRequestingMembers = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToActionableRequestingMemberCount));
this.gv1MigrationSuggestions = Transformations.distinctUntilChanged(LiveDataUtil.mapAsync(groupRecord, ConversationGroupViewModel::mapToGroupV1MigrationSuggestions));
this.reviewState = LiveDataUtil.combineLatest(groupRecord,
duplicates,
(record, dups) -> dups.isEmpty()
? ReviewState.EMPTY
: new ReviewState(record.getId().requireV2(), dups.get(0), dups.size()));
}
void onRecipientChange(Recipient recipient) {
liveRecipient.setValue(recipient);
}
void onSuggestedMembersBannerDismissed(@NonNull GroupId groupId) {
SignalExecutors.BOUNDED.execute(() -> {
if (groupId.isV2()) {
SignalDatabase.groups().removeUnmigratedV1Members(groupId.requireV2());
liveRecipient.postValue(liveRecipient.getValue());
}
});
}
/**
* The number of pending group join requests that can be actioned by this client.
*/
LiveData<Integer> getActionableRequestingMembers() {
return actionableRequestingMembers;
}
LiveData<GroupActiveState> getGroupActiveState() {
return groupActiveState;
}
LiveData<ConversationMemberLevel> getSelfMemberLevel() {
return selfMembershipLevel;
}
public LiveData<ReviewState> getReviewState() {
return reviewState;
}
@NonNull LiveData<List<RecipientId>> getGroupV1MigrationSuggestions() {
return gv1MigrationSuggestions;
}
boolean isNonAdminInAnnouncementGroup() {
ConversationMemberLevel level = selfMembershipLevel.getValue();
return level != null && level.getMemberLevel() != GroupTable.MemberLevel.ADMINISTRATOR && level.isAnnouncementGroup();
}
private static @Nullable GroupRecord getGroupRecordForRecipient(@Nullable Recipient recipient) {
if (recipient != null && recipient.isGroup()) {
Application context = ApplicationDependencies.getApplication();
GroupTable groupDatabase = SignalDatabase.groups();
return groupDatabase.getGroup(recipient.getId()).orElse(null);
} else {
return null;
}
}
private static int mapToActionableRequestingMemberCount(@Nullable GroupRecord record) {
if (record != null &&
record.isV2Group() &&
record.memberLevel(Recipient.self()) == GroupTable.MemberLevel.ADMINISTRATOR)
{
return record.requireV2GroupProperties()
.getDecryptedGroup()
.getRequestingMembersCount();
} else {
return 0;
}
}
private static GroupActiveState mapToGroupActiveState(@Nullable GroupRecord record) {
if (record == null) {
return null;
}
return new GroupActiveState(record.isActive(), record.isV2Group());
}
private static ConversationMemberLevel mapToSelfMembershipLevel(@Nullable GroupRecord record) {
if (record == null) {
return null;
}
return new ConversationMemberLevel(record.memberLevel(Recipient.self()), record.isAnnouncementGroup());
}
@WorkerThread
private static List<RecipientId> mapToGroupV1MigrationSuggestions(@Nullable GroupRecord record) {
if (record == null ||
!record.isV2Group() ||
!record.isActive() ||
record.isPendingMember(Recipient.self())) {
return Collections.emptyList();
}
return Stream.of(record.getUnmigratedV1Members())
.filterNot(m -> record.getMembers().contains(m))
.map(Recipient::resolved)
.filter(GroupsV1MigrationUtil::isAutoMigratable)
.map(Recipient::getId)
.toList();
}
public static void onCancelJoinRequest(@NonNull Recipient recipient,
@NonNull AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason> callback)
{
SignalExecutors.UNBOUNDED.execute(() -> {
if (!recipient.isPushV2Group()) {
throw new AssertionError();
}
try {
GroupManager.cancelJoinRequest(ApplicationDependencies.getApplication(), recipient.getGroupId().get().requireV2());
callback.onComplete(null);
} catch (GroupChangeFailedException | GroupChangeBusyException | IOException e) {
callback.onError(GroupChangeFailureReason.fromException(e));
}
});
}
void inviteFriendsOneTimeIfJustSelfInGroup(@NonNull FragmentManager supportFragmentManager, @NonNull GroupId.V2 groupId) {
if (firstTimeInviteFriendsTriggered) {
return;
}
firstTimeInviteFriendsTriggered = true;
SimpleTask.run(() -> SignalDatabase.groups()
.requireGroup(groupId)
.getMembers().equals(Collections.singletonList(Recipient.self().getId())),
justSelf -> {
if (justSelf) {
inviteFriends(supportFragmentManager, groupId);
}
}
);
}
void inviteFriends(@NonNull FragmentManager supportFragmentManager, @NonNull GroupId.V2 groupId) {
GroupLinkInviteFriendsBottomSheetDialogFragment.show(supportFragmentManager, groupId);
}
public Single<GroupBlockJoinRequestResult> blockJoinRequests(@NonNull Recipient groupRecipient, @NonNull Recipient recipient) {
return groupManagementRepository.blockJoinRequests(groupRecipient.requireGroupId().requireV2(), recipient)
.observeOn(AndroidSchedulers.mainThread());
}
static final class ReviewState {
private static final ReviewState EMPTY = new ReviewState(null, Recipient.UNKNOWN, 0);
private final GroupId.V2 groupId;
private final Recipient recipient;
private final int count;
ReviewState(@Nullable GroupId.V2 groupId, @NonNull Recipient recipient, int count) {
this.groupId = groupId;
this.recipient = recipient;
this.count = count;
}
public @Nullable GroupId.V2 getGroupId() {
return groupId;
}
public @NonNull Recipient getRecipient() {
return recipient;
}
public int getCount() {
return count;
}
}
static final class GroupActiveState {
private final boolean isActive;
private final boolean isActiveV2;
public GroupActiveState(boolean isActive, boolean isV2) {
this.isActive = isActive;
this.isActiveV2 = isActive && isV2;
}
public boolean isActiveGroup() {
return isActive;
}
public boolean isActiveV2Group() {
return isActiveV2;
}
}
static final class ConversationMemberLevel {
private final GroupTable.MemberLevel memberLevel;
private final boolean isAnnouncementGroup;
private ConversationMemberLevel(GroupTable.MemberLevel memberLevel, boolean isAnnouncementGroup) {
this.memberLevel = memberLevel;
this.isAnnouncementGroup = isAnnouncementGroup;
}
public @NonNull GroupTable.MemberLevel getMemberLevel() {
return memberLevel;
}
public boolean isAnnouncementGroup() {
return isAnnouncementGroup;
}
}
static class Factory extends ViewModelProvider.NewInstanceFactory {
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new ConversationGroupViewModel());
}
}
}

View file

@ -16,14 +16,13 @@ import org.thoughtcrime.securesms.conversation.colors.ChatColors;
import org.thoughtcrime.securesms.conversation.v2.ConversationActivity;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.ThreadTable;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mms.SlideFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
import org.whispersystems.signalservice.api.util.Preconditions;
import java.util.ArrayList;
import java.util.Collection;
@ -65,7 +64,6 @@ public class ConversationIntents {
* @param context Context for Intent creation
* @param recipientId The RecipientId to query the thread ID for if the passed one is invalid.
* @param threadId The threadId, or -1L
*
* @return A Single that will return a builder to create the conversation intent.
*/
@MainThread
@ -81,11 +79,11 @@ public class ConversationIntents {
}
public static @NonNull Builder createPopUpBuilder(@NonNull Context context, @NonNull RecipientId recipientId, long threadId) {
return new Builder(context, ConversationPopupActivity.class, recipientId, threadId);
return new Builder(context, ConversationPopupActivity.class, recipientId, threadId, ConversationScreenType.POPUP);
}
public static @NonNull Intent createBubbleIntent(@NonNull Context context, @NonNull RecipientId recipientId, long threadId) {
return new Builder(context, BubbleConversationActivity.class, recipientId, threadId).build();
return new Builder(context, BubbleConversationActivity.class, recipientId, threadId, ConversationScreenType.BUBBLE).build();
}
/**
@ -95,20 +93,11 @@ public class ConversationIntents {
* @param context Context for Intent creation
* @param recipientId The recipientId, only used if the threadId is not valid
* @param threadId The threadId, required for CFV2.
*
* @return A builder that can be used to create a conversation intent.
*/
public static @NonNull Builder createBuilderSync(@NonNull Context context, @NonNull RecipientId recipientId, long threadId) {
return new Builder(context, recipientId, threadId);
}
static boolean isInvalid(@NonNull Bundle arguments) {
Uri uri = getIntentData(arguments);
if (isBubbleIntentUri(uri)) {
return uri.getQueryParameter(EXTRA_RECIPIENT) == null;
} else {
return !arguments.containsKey(EXTRA_RECIPIENT);
}
Preconditions.checkArgument(threadId > 0, "threadId is invalid");
return new Builder(context, ConversationActivity.class, recipientId, threadId, ConversationScreenType.NORMAL);
}
static @Nullable Uri getIntentData(@NonNull Bundle bundle) {
@ -119,7 +108,7 @@ public class ConversationIntents {
return bundle.getString(INTENT_TYPE);
}
static @NonNull Bundle createParentFragmentArguments(@NonNull Intent intent) {
public static @NonNull Bundle createParentFragmentArguments(@NonNull Intent intent) {
Bundle bundle = new Bundle();
if (intent.getExtras() != null) {
@ -132,7 +121,7 @@ public class ConversationIntents {
return bundle;
}
static boolean isBubbleIntentUri(@Nullable Uri uri) {
public static boolean isBubbleIntentUri(@Nullable Uri uri) {
return uri != null && Objects.equals(uri.getAuthority(), BUBBLE_AUTHORITY);
}
@ -311,6 +300,7 @@ public class ConversationIntents {
private final Class<? extends Activity> conversationActivityClass;
private final RecipientId recipientId;
private final long threadId;
private final ConversationScreenType conversationScreenType;
private String draftText;
private List<Media> media;
@ -324,30 +314,18 @@ public class ConversationIntents {
private boolean withSearchOpen;
private Badge giftBadge;
private long shareDataTimestamp = -1L;
private ConversationScreenType conversationScreenType;
private Builder(@NonNull Context context,
@NonNull RecipientId recipientId,
long threadId)
{
this(
context,
getBaseConversationActivity(),
recipientId,
threadId
);
}
private Builder(@NonNull Context context,
@NonNull Class<? extends Activity> conversationActivityClass,
@NonNull RecipientId recipientId,
long threadId)
long threadId,
@NonNull ConversationScreenType conversationScreenType)
{
this.context = context;
this.conversationActivityClass = conversationActivityClass;
this.recipientId = recipientId;
this.threadId = checkThreadId(threadId);
this.conversationScreenType = ConversationScreenType.fromActivityClass(conversationActivityClass);
this.conversationScreenType = conversationScreenType;
}
public @NonNull Builder withDraftText(@Nullable String draftText) {
@ -419,7 +397,7 @@ public class ConversationIntents {
intent.setAction(Intent.ACTION_DEFAULT);
if (Objects.equals(conversationActivityClass, BubbleConversationActivity.class)) {
if (conversationScreenType.isInBubble()) {
intent.setData(new Uri.Builder().authority(BUBBLE_AUTHORITY)
.appendQueryParameter(EXTRA_RECIPIENT, recipientId.serialize())
.appendQueryParameter(EXTRA_THREAD_ID, String.valueOf(threadId))
@ -459,13 +437,9 @@ public class ConversationIntents {
intent.setType(dataType);
}
if (FeatureFlags.useConversationFragmentV2()) {
Bundle args = ConversationIntents.createParentFragmentArguments(intent);
Bundle args = ConversationIntents.createParentFragmentArguments(intent);
return intent.putExtras(args);
} else {
return intent;
}
return intent.putExtras(args);
}
}
@ -501,31 +475,13 @@ public class ConversationIntents {
return NORMAL;
}
private static @NonNull ConversationScreenType fromActivityClass(Class<? extends Activity> activityClass) {
if (Objects.equals(activityClass, ConversationPopupActivity.class)) {
return POPUP;
} else if (Objects.equals(activityClass, BubbleConversationActivity.class)) {
return BUBBLE;
} else {
return NORMAL;
}
}
}
private static long checkThreadId(long threadId) {
if (threadId < 0 && FeatureFlags.useConversationFragmentV2()) {
if (threadId < 0) {
throw new IllegalArgumentException("ThreadId is a required field in CFV2");
} else {
return threadId;
}
}
private static Class<? extends Activity> getBaseConversationActivity() {
if (FeatureFlags.useConversationFragmentV2()) {
return ConversationActivity.class;
} else {
return org.thoughtcrime.securesms.conversation.ConversationActivity.class;
}
}
}

View file

@ -1737,7 +1737,11 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
private boolean forceFooter(@NonNull MessageRecord messageRecord) {
return hasAudio(messageRecord) || MessageRecordUtil.isEditMessage(messageRecord);
return hasAudio(messageRecord) || MessageRecordUtil.isEditMessage(messageRecord) || displayMode == ConversationItemDisplayMode.EDIT_HISTORY;
}
private boolean forceGroupHeader(@NonNull MessageRecord messageRecord) {
return displayMode == ConversationItemDisplayMode.EDIT_HISTORY;
}
private ConversationItemFooter getActiveFooter(@NonNull MessageRecord messageRecord) {
@ -1783,7 +1787,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
contactPhotoHolder.setVisibility(VISIBLE);
if (!previous.isPresent() || previous.get().isUpdate() || !current.getFromRecipient().equals(previous.get().getFromRecipient()) ||
!DateUtils.isSameDay(previous.get().getTimestamp(), current.getTimestamp()) || !isWithinClusteringTime(current, previous.get()))
!DateUtils.isSameDay(previous.get().getTimestamp(), current.getTimestamp()) || !isWithinClusteringTime(current, previous.get()) || forceGroupHeader(current))
{
groupSenderHolder.setVisibility(VISIBLE);
@ -1797,7 +1801,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
groupSenderHolder.setVisibility(GONE);
}
if (!next.isPresent() || next.get().isUpdate() || !current.getFromRecipient().equals(next.get().getFromRecipient()) || !isWithinClusteringTime(current, next.get())) {
if (!next.isPresent() || next.get().isUpdate() || !current.getFromRecipient().equals(next.get().getFromRecipient()) || !isWithinClusteringTime(current, next.get()) || forceGroupHeader(current)) {
contactPhoto.setVisibility(VISIBLE);
badgeImageView.setVisibility(VISIBLE);
} else {
@ -1962,7 +1966,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
if (messageRecord.isMms()) {
TextSlide slide = ((MmsMessageRecord) messageRecord).getSlideDeck().getTextSlide();
if (slide != null && slide.asAttachment().getTransferState() == AttachmentTable.TRANSFER_PROGRESS_DONE) {
if (slide != null && (slide.asAttachment().getTransferState() == AttachmentTable.TRANSFER_PROGRESS_DONE || MessageRecordUtil.isScheduled(messageRecord))) {
message = getResources().getString(R.string.ConversationItem_read_more);
action = () -> eventListener.onMoreTextClicked(conversationRecipient.getId(), messageRecord.getId(), messageRecord.isMms());
} else if (slide != null && slide.asAttachment().getTransferState() == AttachmentTable.TRANSFER_PROGRESS_STARTED) {

View file

@ -1,87 +0,0 @@
package org.thoughtcrime.securesms.conversation;
import android.os.Bundle;
import android.view.Display;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.WindowManager;
import androidx.appcompat.widget.Toolbar;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
public class ConversationPopupActivity extends ConversationActivity {
private static final String TAG = Log.tag(ConversationPopupActivity.class);
@Override
protected void onPreCreate() {
super.onPreCreate();
overridePendingTransition(R.anim.slide_from_top, R.anim.slide_to_top);
}
@Override
protected void onCreate(Bundle bundle, boolean ready) {
getWindow().setFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND,
WindowManager.LayoutParams.FLAG_DIM_BEHIND);
WindowManager.LayoutParams params = getWindow().getAttributes();
params.alpha = 1.0f;
params.dimAmount = 0.1f;
params.gravity = Gravity.TOP;
getWindow().setAttributes(params);
Display display = getWindowManager().getDefaultDisplay();
int width = display.getWidth();
int height = display.getHeight();
if (height > width) getWindow().setLayout((int) (width * .85), (int) (height * .5));
else getWindow().setLayout((int) (width * .7), (int) (height * .75));
super.onCreate(bundle, ready);
}
@Override
protected void onResume() {
super.onResume();
getTitleView().setOnClickListener(null);
getComposeText().requestFocus();
getQuickAttachmentToggle().disable();
}
@Override
protected void onPause() {
super.onPause();
if (isFinishing()) overridePendingTransition(R.anim.slide_from_top, R.anim.slide_to_top);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
MenuInflater inflater = this.getMenuInflater();
menu.clear();
inflater.inflate(R.menu.conversation_popup, menu);
return true;
}
@Override
public void onInitializeToolbar(Toolbar toolbar) {
}
@Override
public void onSendComplete(long threadId) {
finish();
}
@Override
public boolean onUpdateReminders() {
if (getReminderView().resolved()) {
getReminderView().get().setVisibility(View.GONE);
}
return false;
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.conversation
import android.os.Bundle
import android.view.Gravity
import android.view.WindowManager
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.conversation.v2.ConversationActivity
/**
* Flavor of [ConversationActivity] used for quick replies to notifications in pre-API 24 devices.
*/
class ConversationPopupActivity : ConversationActivity() {
override fun onPreCreate() {
super.onPreCreate()
overridePendingTransition(R.anim.slide_from_top, R.anim.slide_to_top)
}
@Suppress("DEPRECATION")
override fun onCreate(bundle: Bundle?, ready: Boolean) {
window.setFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND, WindowManager.LayoutParams.FLAG_DIM_BEHIND)
window.attributes = window.attributes.apply {
alpha = 1.0f
dimAmount = 0.1f
gravity = Gravity.TOP
}
val display = windowManager.defaultDisplay
val width = display.width
val height = display.height
if (height > width) {
window.setLayout((width * .85).toInt(), (height * .5).toInt())
} else {
window.setLayout((width * .7).toInt(), (height * .75).toInt())
}
super.onCreate(bundle, ready)
}
override fun onPause() {
super.onPause()
if (isFinishing) {
overridePendingTransition(R.anim.slide_from_top, R.anim.slide_to_top)
}
}
}

View file

@ -23,7 +23,6 @@ import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.Barrier;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import androidx.core.content.ContextCompat;
@ -33,7 +32,6 @@ import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat;
import com.annimon.stream.Stream;
import org.signal.core.util.DimensionUnit;
import org.signal.glide.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
@ -178,14 +176,6 @@ public final class ConversationReactionOverlay extends FrameLayout {
bottomNavigationBarHeight = 0;
}
if (!FeatureFlags.useConversationFragmentV2()) {
toolbarShade.setVisibility(VISIBLE);
toolbarShade.setAlpha(1f);
inputShade.setVisibility(VISIBLE);
inputShade.setAlpha(1f);
}
Bitmap conversationItemSnapshot = selectedConversationModel.getBitmap();
conversationItem.setLayoutParams(new LayoutParams(conversationItemSnapshot.getWidth(), conversationItemSnapshot.getHeight()));
@ -211,8 +201,8 @@ public final class ConversationReactionOverlay extends FrameLayout {
@NonNull ConversationMessage conversationMessage,
@NonNull PointF lastSeenDownPoint,
boolean isMessageOnLeft) {
updateToolbarShade(activity);
updateInputShade(activity);
updateToolbarShade();
updateInputShade();
contextMenu = new ConversationContextMenu(dropdownAnchor, getMenuActionItems(conversationMessage));
@ -394,50 +384,18 @@ public final class ConversationReactionOverlay extends FrameLayout {
return Math.max(reactionStartingPoint - reactionBarOffset - reactionBarHeight, spaceNeededBetweenTopOfScreenAndTopOfReactionBar);
}
private void updateToolbarShade(@NonNull Activity activity) {
if (FeatureFlags.useConversationFragmentV2()) {
LayoutParams layoutParams = (LayoutParams) toolbarShade.getLayoutParams();
layoutParams.height = 0;
toolbarShade.setLayoutParams(layoutParams);
return;
}
View toolbar = activity.findViewById(R.id.toolbar);
View bannerContainer = activity.findViewById(FeatureFlags.useConversationFragmentV2() ? R.id.conversation_banner
: R.id.conversation_banner_container);
private void updateToolbarShade() {
LayoutParams layoutParams = (LayoutParams) toolbarShade.getLayoutParams();
layoutParams.height = toolbar.getHeight() + bannerContainer.getHeight();
layoutParams.height = 0;
toolbarShade.setLayoutParams(layoutParams);
}
private void updateInputShade(@NonNull Activity activity) {
if (FeatureFlags.useConversationFragmentV2()) {
LayoutParams layoutParams = (LayoutParams) inputShade.getLayoutParams();
layoutParams.height = 0;
inputShade.setLayoutParams(layoutParams);
return;
}
private void updateInputShade() {
LayoutParams layoutParams = (LayoutParams) inputShade.getLayoutParams();
layoutParams.bottomMargin = bottomNavigationBarHeight;
layoutParams.height = getInputPanelHeight(activity);
layoutParams.height = 0;
inputShade.setLayoutParams(layoutParams);
}
private int getInputPanelHeight(@NonNull Activity activity) {
if (FeatureFlags.useConversationFragmentV2()) {
View bottomPanel = activity.findViewById(R.id.conversation_input_panel);
return bottomPanel.getHeight();
}
View bottomPanel = activity.findViewById(R.id.conversation_activity_panel_parent);
View emojiDrawer = activity.findViewById(R.id.emoji_drawer);
return bottomPanel.getHeight() + (emojiDrawer != null && emojiDrawer.getVisibility() == VISIBLE ? emojiDrawer.getHeight() : 0);
}
/**
* Returns true when the device is in a configuration where the navigation bar doesn't take up
* space at the bottom of the screen.
@ -915,22 +873,6 @@ public final class ConversationReactionOverlay extends FrameLayout {
itemYAnim.setDuration(duration);
animators.add(itemYAnim);
if (!FeatureFlags.useConversationFragmentV2()) {
ObjectAnimator toolbarShadeAnim = new ObjectAnimator();
toolbarShadeAnim.setProperty(View.ALPHA);
toolbarShadeAnim.setFloatValues(0f);
toolbarShadeAnim.setTarget(toolbarShade);
toolbarShadeAnim.setDuration(duration);
animators.add(toolbarShadeAnim);
ObjectAnimator inputShadeAnim = new ObjectAnimator();
inputShadeAnim.setProperty(View.ALPHA);
inputShadeAnim.setFloatValues(0f);
inputShadeAnim.setTarget(inputShade);
inputShadeAnim.setDuration(duration);
animators.add(inputShadeAnim);
}
if (activity != null) {
ValueAnimator statusBarAnim = ValueAnimator.ofArgb(activity.getWindow().getStatusBarColor(), originalStatusBarColor);
statusBarAnim.setDuration(duration);

View file

@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.util.BubbleUtil;
import org.thoughtcrime.securesms.util.ConversationUtil;
import org.thoughtcrime.securesms.util.MessageRecordUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.push.ServiceId;
import java.io.IOException;
@ -53,17 +54,6 @@ public class ConversationRepository {
this.context = ApplicationDependencies.getApplication();
}
@WorkerThread
boolean canShowAsBubble(long threadId) {
if (Build.VERSION.SDK_INT >= ConversationUtil.CONVERSATION_SUPPORT_VERSION) {
Recipient recipient = SignalDatabase.threads().getRecipientForThreadId(threadId);
return recipient != null && BubbleUtil.canBubble(context, recipient.getId(), threadId);
} else {
return false;
}
}
@WorkerThread
public @NonNull ConversationData getConversationData(long threadId, @NonNull Recipient conversationRecipient, int jumpToPosition) {
ThreadTable.ConversationMetadata metadata = SignalDatabase.threads().getConversationMetadata(threadId);
@ -142,55 +132,6 @@ public class ConversationRepository {
});
}
/**
* Watches the given recipient id for changes, and gets the security info for the recipient
* whenever a change occurs.
*
* @param recipientId The recipient id we are interested in
*
* @return The recipient's security info.
*/
@NonNull Observable<ConversationSecurityInfo> getSecurityInfo(@NonNull RecipientId recipientId) {
return Recipient.observable(recipientId)
.distinctUntilChanged((lhs, rhs) -> lhs.isPushGroup() == rhs.isPushGroup() && lhs.getRegistered().equals(rhs.getRegistered()))
.switchMapSingle(this::getSecurityInfo)
.subscribeOn(Schedulers.io());
}
private @NonNull Single<ConversationSecurityInfo> getSecurityInfo(@NonNull Recipient recipient) {
return Single.fromCallable(() -> {
Log.i(TAG, "Resolving registered state...");
RecipientTable.RegisteredState registeredState;
if (recipient.isPushGroup()) {
Log.i(TAG, "Push group recipient...");
registeredState = RecipientTable.RegisteredState.REGISTERED;
} else {
Log.i(TAG, "Checking through resolved recipient");
registeredState = recipient.getRegistered();
}
Log.i(TAG, "Resolved registered state: " + registeredState);
boolean signalEnabled = Recipient.self().isRegistered();
if (registeredState == RecipientTable.RegisteredState.UNKNOWN) {
try {
Log.i(TAG, "Refreshing directory for user: " + recipient.getId().serialize());
registeredState = ContactDiscovery.refresh(context, recipient, false);
} catch (IOException e) {
Log.w(TAG, e);
}
}
Log.i(TAG, "Returning registered state...");
return new ConversationSecurityInfo(recipient.getId(),
registeredState == RecipientTable.RegisteredState.REGISTERED && signalEnabled,
true,
SignalStore.misc().isClientDeprecated(),
TextSecurePreferences.isUnauthorizedReceived(context));
}).subscribeOn(Schedulers.io());
}
@NonNull
public Single<ConversationMessage> resolveMessageToEdit(@NonNull ConversationMessage message) {
return Single.fromCallable(() -> {
@ -212,28 +153,4 @@ public class ConversationRepository {
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
Observable<Integer> getUnreadCount(long threadId, long afterTime) {
if (threadId <= -1L || afterTime <= 0L) {
return Observable.just(0);
}
return Observable.<Integer> create(emitter -> {
DatabaseObserver.Observer listener = () -> emitter.onNext(SignalDatabase.messages().getIncomingMeaningfulMessageCountSince(threadId, afterTime));
ApplicationDependencies.getDatabaseObserver().registerConversationObserver(threadId, listener);
emitter.setCancellable(() -> ApplicationDependencies.getDatabaseObserver().unregisterObserver(listener));
listener.onChanged();
}).subscribeOn(Schedulers.io());
}
public void setConversationMuted(@NonNull RecipientId recipientId, long until) {
SignalExecutors.BOUNDED.execute(() -> SignalDatabase.recipients().setMuted(recipientId, until));
}
public void setConversationDistributionType(long threadId, int distributionType) {
SignalExecutors.BOUNDED.execute(() -> SignalDatabase.threads().setDistributionType(threadId, distributionType));
}
}

View file

@ -1,79 +0,0 @@
package org.thoughtcrime.securesms.conversation;
import android.app.Application;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.database.DatabaseObserver;
import org.thoughtcrime.securesms.database.model.StickerRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.emoji.EmojiSource;
import org.thoughtcrime.securesms.stickers.StickerSearchRepository;
import org.thoughtcrime.securesms.util.Throttler;
import java.util.Collections;
import java.util.List;
class ConversationStickerViewModel extends ViewModel {
private final StickerSearchRepository repository;
private final MutableLiveData<List<StickerRecord>> stickers;
private final MutableLiveData<Boolean> stickersAvailable;
private final Throttler availabilityThrottler;
private final DatabaseObserver.Observer packObserver;
private ConversationStickerViewModel(@NonNull Application application, @NonNull StickerSearchRepository repository) {
this.repository = repository;
this.stickers = new MutableLiveData<>();
this.stickersAvailable = new MutableLiveData<>();
this.availabilityThrottler = new Throttler(500);
this.packObserver = () -> {
availabilityThrottler.publish(() -> repository.getStickerFeatureAvailability(stickersAvailable::postValue));
};
ApplicationDependencies.getDatabaseObserver().registerStickerPackObserver(packObserver);
}
@NonNull LiveData<List<StickerRecord>> getStickerResults() {
return stickers;
}
@NonNull LiveData<Boolean> getStickersAvailability() {
repository.getStickerFeatureAvailability(stickersAvailable::postValue);
return stickersAvailable;
}
void onInputTextUpdated(@NonNull String text) {
if (TextUtils.isEmpty(text) || text.length() > EmojiSource.getLatest().getMaxEmojiLength()) {
stickers.setValue(Collections.emptyList());
} else {
repository.searchByEmoji(text, stickers::postValue);
}
}
@Override
protected void onCleared() {
ApplicationDependencies.getDatabaseObserver().unregisterObserver(packObserver);
}
static class Factory extends ViewModelProvider.NewInstanceFactory {
private final Application application;
private final StickerSearchRepository repository;
public Factory(@NonNull Application application, @NonNull StickerSearchRepository repository) {
this.application = application;
this.repository = repository;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new ConversationStickerViewModel(application, repository));
}
}
}

View file

@ -54,6 +54,7 @@ import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.thoughtcrime.securesms.verify.VerifyIdentityActivity;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import java.util.Collection;
import java.util.Locale;
@ -300,11 +301,11 @@ public final class ConversationUpdateItem extends FrameLayout
private final Observer<Object> updater;
private LiveGroup liveGroup;
private LiveData<Boolean> liveIsSelfAdmin;
private LiveData<Set<UUID>> liveBannedMembers;
private LiveData<Set<UUID>> liveFullMembers;
private Recipient conversationRecipient;
private LiveGroup liveGroup;
private LiveData<Boolean> liveIsSelfAdmin;
private LiveData<Set<ServiceId>> liveBannedMembers;
private LiveData<Set<UUID>> liveFullMembers;
private Recipient conversationRecipient;
GroupDataManager() {
this.updater = unused -> update();
@ -324,7 +325,7 @@ public final class ConversationUpdateItem extends FrameLayout
liveBannedMembers = liveGroup.getBannedMembers();
liveFullMembers = Transformations.map(liveGroup.getFullMembers(),
members -> members.stream()
.map(m -> m.getMember().requireServiceId().uuid())
.map(m -> m.getMember().requireAci().getRawUuid())
.collect(Collectors.toSet()));
liveIsSelfAdmin.observe(lifecycleOwner, updater);
@ -350,9 +351,9 @@ public final class ConversationUpdateItem extends FrameLayout
return false;
}
Set<UUID> bannedMembers = liveBannedMembers.getValue();
Set<ServiceId> bannedMembers = liveBannedMembers.getValue();
if (bannedMembers != null) {
return recipient.getServiceId().isPresent() && bannedMembers.contains(recipient.requireServiceId().uuid());
return recipient.getServiceId().isPresent() && bannedMembers.contains(recipient.requireServiceId());
}
return false;
}
@ -364,7 +365,7 @@ public final class ConversationUpdateItem extends FrameLayout
Set<UUID> members = liveFullMembers.getValue();
if (members != null) {
return recipient.getServiceId().isPresent() && members.contains(recipient.requireServiceId().uuid());
return recipient.hasAci() && members.contains(recipient.requireAci().getRawUuid());
}
return false;
}
@ -455,12 +456,12 @@ public final class ConversationUpdateItem extends FrameLayout
}
});
} else if (conversationMessage.getMessageRecord().isGroupCall()) {
UpdateDescription updateDescription = MessageRecord.getGroupCallUpdateDescription(getContext(), conversationMessage.getMessageRecord().getBody(), true);
Collection<ServiceId> sids = updateDescription.getMentioned();
UpdateDescription updateDescription = MessageRecord.getGroupCallUpdateDescription(getContext(), conversationMessage.getMessageRecord().getBody(), true);
Collection<ACI> acis = updateDescription.getMentioned();
int text = 0;
if (Util.hasItems(sids)) {
if (sids.contains(SignalStore.account().requireAci())) {
if (Util.hasItems(acis)) {
if (acis.contains(SignalStore.account().requireAci())) {
text = R.string.ConversationUpdateItem_return_to_call;
} else if (GroupCallUpdateDetailsUtil.parse(conversationMessage.getMessageRecord().getBody()).getIsCallFull()) {
text = R.string.ConversationUpdateItem_call_is_full;

View file

@ -1,499 +0,0 @@
package org.thoughtcrime.securesms.conversation;
import android.app.Application;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.LiveDataReactiveStreams;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.util.Pair;
import org.signal.paging.ObservablePagedData;
import org.signal.paging.PagedData;
import org.signal.paging.PagingConfig;
import org.signal.paging.PagingController;
import org.signal.paging.ProxyPagingController;
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.NotificationProfilesRepository;
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper;
import org.thoughtcrime.securesms.conversation.colors.NameColor;
import org.thoughtcrime.securesms.database.DatabaseObserver;
import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.database.model.StoryViewState;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mediasend.MediaRepository;
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile;
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles;
import org.thoughtcrime.securesms.ratelimit.RecaptchaRequiredEvent;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.thoughtcrime.securesms.util.livedata.Store;
import org.thoughtcrime.securesms.util.rx.RxStore;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.BackpressureStrategy;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.processors.PublishProcessor;
import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.subjects.BehaviorSubject;
import kotlin.Unit;
public class ConversationViewModel extends ViewModel {
private static final String TAG = Log.tag(ConversationViewModel.class);
private final Application context;
private final MediaRepository mediaRepository;
private final ConversationRepository conversationRepository;
private final ScheduledMessagesRepository scheduledMessagesRepository;
private final MutableLiveData<List<Media>> recentMedia;
private final BehaviorSubject<Long> threadId;
private final Observable<MessageData> messageData;
private final MutableLiveData<Boolean> showScrollButtons;
private final MutableLiveData<Boolean> hasUnreadMentions;
private final Observable<Boolean> canShowAsBubble;
private final ProxyPagingController<MessageId> pagingController;
private final DatabaseObserver.Observer conversationObserver;
private final DatabaseObserver.MessageObserver messageUpdateObserver;
private final DatabaseObserver.MessageObserver messageInsertObserver;
private final BehaviorSubject<RecipientId> recipientId;
private final Observable<Optional<ChatWallpaper>> wallpaper;
private final SingleLiveEvent<Event> events;
private final Observable<ChatColors> chatColors;
private final MutableLiveData<Integer> toolbarBottom;
private final MutableLiveData<Integer> inlinePlayerHeight;
private final LiveData<Integer> conversationTopMargin;
private final Store<ThreadAnimationState> threadAnimationStateStore;
private final Observer<ThreadAnimationState> threadAnimationStateStoreDriver;
private final NotificationProfilesRepository notificationProfilesRepository;
private final MutableLiveData<String> searchQuery;
private final GroupAuthorNameColorHelper groupAuthorNameColorHelper;
private final RxStore<ConversationState> conversationStateStore;
private final CompositeDisposable disposables;
private final BehaviorSubject<Unit> conversationStateTick;
private final PublishProcessor<Long> markReadRequestPublisher;
private final Observable<Integer> scheduledMessageCount;
private ConversationIntents.Args args;
private int jumpToPosition;
private ConversationViewModel() {
this.context = ApplicationDependencies.getApplication();
this.mediaRepository = new MediaRepository();
this.conversationRepository = new ConversationRepository();
this.scheduledMessagesRepository = new ScheduledMessagesRepository();
this.recentMedia = new MutableLiveData<>();
this.showScrollButtons = new MutableLiveData<>(false);
this.hasUnreadMentions = new MutableLiveData<>(false);
this.events = new SingleLiveEvent<>();
this.pagingController = new ProxyPagingController<>();
this.conversationObserver = pagingController::onDataInvalidated;
this.messageUpdateObserver = pagingController::onDataItemChanged;
this.messageInsertObserver = messageId -> pagingController.onDataItemInserted(messageId, 0);
this.toolbarBottom = new MutableLiveData<>();
this.inlinePlayerHeight = new MutableLiveData<>();
this.conversationTopMargin = Transformations.distinctUntilChanged(LiveDataUtil.combineLatest(toolbarBottom, inlinePlayerHeight, Integer::sum));
this.threadAnimationStateStore = new Store<>(new ThreadAnimationState(-1L, null, false));
this.notificationProfilesRepository = new NotificationProfilesRepository();
this.searchQuery = new MutableLiveData<>();
this.recipientId = BehaviorSubject.create();
this.threadId = BehaviorSubject.create();
this.groupAuthorNameColorHelper = new GroupAuthorNameColorHelper();
this.conversationStateStore = new RxStore<>(ConversationState.create(), Schedulers.computation());
this.disposables = new CompositeDisposable();
this.conversationStateTick = BehaviorSubject.createDefault(Unit.INSTANCE);
this.markReadRequestPublisher = PublishProcessor.create();
BehaviorSubject<Recipient> recipientCache = BehaviorSubject.create();
recipientId
.observeOn(Schedulers.io())
.distinctUntilChanged()
.map(Recipient::resolved)
.subscribe(recipientCache);
Disposable disposable = conversationStateStore.update(Observable.combineLatest(recipientId.distinctUntilChanged(), conversationStateTick, (id, tick) -> id)
.switchMap(conversationRepository::getSecurityInfo)
.toFlowable(BackpressureStrategy.LATEST),
(securityInfo, state) -> state.withSecurityInfo(securityInfo));
disposables.add(disposable);
BehaviorSubject<ConversationData> conversationMetadata = BehaviorSubject.create();
Observable.combineLatest(threadId, recipientCache, Pair::new)
.observeOn(Schedulers.io())
.distinctUntilChanged()
.map(threadIdAndRecipient -> {
SignalLocalMetrics.ConversationOpen.onMetadataLoadStarted();
ConversationData conversationData = conversationRepository.getConversationData(threadIdAndRecipient.first(), threadIdAndRecipient.second(), jumpToPosition);
SignalLocalMetrics.ConversationOpen.onMetadataLoaded();
jumpToPosition = -1;
return conversationData;
})
.subscribe(conversationMetadata);
ApplicationDependencies.getDatabaseObserver().registerMessageUpdateObserver(messageUpdateObserver);
messageData = conversationMetadata
.observeOn(Schedulers.io())
.switchMap(data -> {
int startPosition;
ConversationData.MessageRequestData messageRequestData = data.getMessageRequestData();
if (data.shouldJumpToMessage()) {
startPosition = data.getJumpToPosition();
} else if (messageRequestData.isMessageRequestAccepted() && data.shouldScrollToLastSeen()) {
startPosition = data.getLastSeenPosition();
} else if (messageRequestData.isMessageRequestAccepted()) {
startPosition = data.getLastScrolledPosition();
} else {
startPosition = data.getThreadSize();
}
ApplicationDependencies.getDatabaseObserver().unregisterObserver(conversationObserver);
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageInsertObserver);
ApplicationDependencies.getDatabaseObserver().registerConversationObserver(data.getThreadId(), conversationObserver);
ApplicationDependencies.getDatabaseObserver().registerMessageInsertObserver(data.getThreadId(), messageInsertObserver);
ConversationDataSource dataSource = new ConversationDataSource(context,
data.getThreadId(),
messageRequestData,
data.showUniversalExpireTimerMessage(),
data.getThreadSize(),
data.getThreadRecipient());
PagingConfig config = new PagingConfig.Builder().setPageSize(25)
.setBufferPages(2)
.setStartIndex(Math.max(startPosition, 0))
.build();
Log.d(TAG, "Starting at position: " + startPosition + " || jumpToPosition: " + data.getJumpToPosition() + ", lastSeenPosition: " + data.getLastSeenPosition() + ", lastScrolledPosition: " + data.getLastScrolledPosition());
ObservablePagedData<MessageId, ConversationMessage> pagedData = PagedData.createForObservable(dataSource, config);
pagingController.set(pagedData.getController());
return pagedData.getData();
})
.observeOn(Schedulers.io())
.withLatestFrom(conversationMetadata, (messages, metadata) -> new MessageData(metadata, messages))
.doOnNext(a -> SignalLocalMetrics.ConversationOpen.onDataLoaded());
scheduledMessageCount = threadId
.observeOn(Schedulers.io())
.switchMap(scheduledMessagesRepository::getScheduledMessageCount);
Observable<Recipient> liveRecipient = recipientId.distinctUntilChanged().switchMap(id -> Recipient.live(id).observable());
canShowAsBubble = threadId.observeOn(Schedulers.io()).map(conversationRepository::canShowAsBubble);
wallpaper = liveRecipient.map(r -> Optional.ofNullable(r.getWallpaper())).distinctUntilChanged();
chatColors = liveRecipient.map(Recipient::getChatColors).distinctUntilChanged();
threadAnimationStateStore.update(threadId, (id, state) -> {
if (state.getThreadId() == id) {
return state;
} else {
return new ThreadAnimationState(id, null, false);
}
});
threadAnimationStateStore.update(conversationMetadata, (m, state) -> {
if (state.getThreadId() == m.getThreadId()) {
return state.copy(state.getThreadId(), m, state.getHasCommittedNonEmptyMessageList());
} else {
return state.copy(m.getThreadId(), m, false);
}
});
this.threadAnimationStateStoreDriver = state -> {};
threadAnimationStateStore.getStateLiveData().observeForever(threadAnimationStateStoreDriver);
EventBus.getDefault().register(this);
}
Observable<StoryViewState> getStoryViewState() {
return recipientId
.subscribeOn(Schedulers.io())
.switchMap(StoryViewState::getForRecipientId)
.distinctUntilChanged()
.observeOn(AndroidSchedulers.mainThread());
}
void onMessagesCommitted(@NonNull List<ConversationMessage> conversationMessages) {
if (Util.hasItems(conversationMessages)) {
threadAnimationStateStore.update(state -> {
long threadId = conversationMessages.stream()
.filter(Objects::nonNull)
.findFirst()
.map(c -> c.getMessageRecord().getThreadId())
.orElse(-2L);
if (state.getThreadId() == threadId) {
return state.copy(state.getThreadId(), state.getThreadMetadata(), true);
} else {
return state;
}
});
}
}
void setDistributionType(int distributionType) {
Long threadId = this.threadId.getValue();
if (threadId == null) {
return;
}
conversationRepository.setConversationDistributionType(threadId, distributionType);
}
void submitMarkReadRequest(long timestampSince) {
markReadRequestPublisher.onNext(timestampSince);
}
boolean shouldPlayMessageAnimations() {
return threadAnimationStateStore.getState().shouldPlayMessageAnimations();
}
void setToolbarBottom(int bottom) {
toolbarBottom.setValue(bottom);
}
void setInlinePlayerVisible(boolean isVisible) {
inlinePlayerHeight.setValue(isVisible ? ViewUtil.dpToPx(36) : 0);
}
void onAttachmentKeyboardOpen() {
mediaRepository.getMediaInBucket(context, Media.ALL_MEDIA_BUCKET_ID, recentMedia::postValue);
}
@MainThread
void onConversationDataAvailable(@NonNull RecipientId recipientId, long threadId, int startingPosition) {
Log.d(TAG, "[onConversationDataAvailable] recipientId: " + recipientId + ", threadId: " + threadId + ", startingPosition: " + startingPosition);
this.jumpToPosition = startingPosition;
this.threadId.onNext(threadId);
this.recipientId.onNext(recipientId);
}
void clearThreadId() {
this.jumpToPosition = -1;
this.threadId.onNext(-1L);
}
void setSearchQuery(@Nullable String query) {
searchQuery.setValue(query);
}
@NonNull Flowable<Long> getMarkReadRequests() {
return markReadRequestPublisher.onBackpressureBuffer();
}
@NonNull Observable<Integer> getThreadUnreadCount(long afterTime) {
return threadId.switchMap(id -> conversationRepository.getUnreadCount(id, afterTime));
}
@NonNull Flowable<ConversationState> getConversationState() {
return conversationStateStore.getStateFlowable().observeOn(AndroidSchedulers.mainThread());
}
@NonNull Flowable<ConversationSecurityInfo> getConversationSecurityInfo(@NonNull RecipientId recipientId) {
return getConversationState().map(ConversationState::getSecurityInfo)
.filter(info -> info.isInitialized() && Objects.equals(info.getRecipientId(), recipientId))
.distinctUntilChanged();
}
void updateSecurityInfo() {
conversationStateTick.onNext(Unit.INSTANCE);
}
boolean isDefaultSmsApplication() {
return false;
}
boolean isPushAvailable() {
return conversationStateStore.getState().getSecurityInfo().isPushAvailable();
}
void muteConversation(long until) {
conversationRepository.setConversationMuted(args.getRecipientId(), until);
}
@NonNull ConversationState getConversationStateSnapshot() {
return conversationStateStore.getState();
}
@NonNull LiveData<String> getSearchQuery() {
return searchQuery;
}
@NonNull LiveData<Integer> getConversationTopMargin() {
return conversationTopMargin;
}
@NonNull Observable<Boolean> canShowAsBubble() {
return canShowAsBubble
.observeOn(AndroidSchedulers.mainThread());
}
@NonNull LiveData<Boolean> getShowScrollToBottom() {
return Transformations.distinctUntilChanged(showScrollButtons);
}
@NonNull LiveData<Boolean> getShowMentionsButton() {
return Transformations.distinctUntilChanged(LiveDataUtil.combineLatest(showScrollButtons, hasUnreadMentions, (a, b) -> a && b));
}
@NonNull Observable<Optional<ChatWallpaper>> getWallpaper() {
return wallpaper
.observeOn(AndroidSchedulers.mainThread());
}
@NonNull LiveData<Event> getEvents() {
return events;
}
@NonNull Observable<ChatColors> getChatColors() {
return chatColors
.observeOn(AndroidSchedulers.mainThread());
}
@NonNull Observable<Integer> getScheduledMessageCount() {
return scheduledMessageCount.observeOn(AndroidSchedulers.mainThread());
}
void setHasUnreadMentions(boolean hasUnreadMentions) {
this.hasUnreadMentions.setValue(hasUnreadMentions);
}
boolean getShowScrollButtons() {
return this.showScrollButtons.getValue();
}
void setShowScrollButtons(boolean showScrollButtons) {
this.showScrollButtons.setValue(showScrollButtons);
}
@NonNull LiveData<List<Media>> getRecentMedia() {
return recentMedia;
}
@NonNull Observable<MessageData> getMessageData() {
return messageData
.observeOn(AndroidSchedulers.mainThread());
}
@NonNull PagingController<MessageId> getPagingController() {
return pagingController;
}
@NonNull Observable<Map<RecipientId, NameColor>> getNameColorsMap() {
return recipientId
.observeOn(Schedulers.io())
.distinctUntilChanged()
.map(Recipient::resolved)
.map(recipient -> {
if (recipient.getGroupId().isPresent()) {
return groupAuthorNameColorHelper.getColorMap(recipient.getGroupId().get());
} else {
return Collections.<RecipientId, NameColor>emptyMap();
}
})
.observeOn(AndroidSchedulers.mainThread());
}
@NonNull LiveData<Optional<NotificationProfile>> getActiveNotificationProfile() {
Flowable<Optional<NotificationProfile>> activeProfile = notificationProfilesRepository.getProfiles()
.map(profiles -> Optional.ofNullable(NotificationProfiles.getActiveProfile(profiles)));
return LiveDataReactiveStreams.fromPublisher(activeProfile);
}
@NonNull
public Single<ConversationMessage> resolveMessageToEdit(@NonNull ConversationMessage message) {
return conversationRepository.resolveMessageToEdit(message);
}
void setArgs(@NonNull ConversationIntents.Args args) {
this.args = args;
}
@NonNull ConversationIntents.Args getArgs() {
return Objects.requireNonNull(args);
}
@Subscribe(threadMode = ThreadMode.POSTING)
public void onRecaptchaRequiredEvent(@NonNull RecaptchaRequiredEvent event) {
events.postValue(Event.SHOW_RECAPTCHA);
}
@Override
protected void onCleared() {
super.onCleared();
threadAnimationStateStore.getStateLiveData().removeObserver(threadAnimationStateStoreDriver);
ApplicationDependencies.getDatabaseObserver().unregisterObserver(conversationObserver);
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageUpdateObserver);
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageInsertObserver);
disposables.clear();
conversationStateStore.dispose();
EventBus.getDefault().unregister(this);
}
enum Event {
SHOW_RECAPTCHA
}
static class MessageData {
private final List<ConversationMessage> messages;
private final ConversationData metadata;
MessageData(@NonNull ConversationData metadata, @NonNull List<ConversationMessage> messages) {
this.metadata = metadata;
this.messages = messages;
}
public @NonNull List<ConversationMessage> getMessages() {
return messages;
}
public @NonNull ConversationData getMetadata() {
return metadata;
}
}
static class Factory extends ViewModelProvider.NewInstanceFactory {
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new ConversationViewModel());
}
}
}

View file

@ -1,64 +0,0 @@
package org.thoughtcrime.securesms.conversation;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.conversation.ConversationAdapter.StickyHeaderViewHolder;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
class LastSeenHeader extends StickyHeaderDecoration {
private final ConversationAdapter adapter;
private final long lastSeenTimestamp;
private long unreadCount;
LastSeenHeader(ConversationAdapter adapter, long lastSeenTimestamp) {
super(adapter, false, false, ConversationAdapter.HEADER_TYPE_LAST_SEEN);
this.adapter = adapter;
this.lastSeenTimestamp = lastSeenTimestamp;
}
public void setUnreadCount(long unreadCount) {
this.unreadCount = unreadCount;
}
@Override
protected boolean hasHeader(RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) {
if (lastSeenTimestamp <= 0 || unreadCount <= 0) {
return false;
}
long currentRecordTimestamp = adapter.getReceivedTimestamp(position);
long previousRecordTimestamp = adapter.getReceivedTimestamp(position + 1);
return currentRecordTimestamp > lastSeenTimestamp && previousRecordTimestamp < lastSeenTimestamp;
}
@Override
protected int getHeaderTop(RecyclerView parent, View child, View header, int adapterPos, int layoutPos) {
return parent.getLayoutManager().getDecoratedTop(child);
}
@Override
protected @NonNull RecyclerView.ViewHolder getHeader(RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) {
StickyHeaderViewHolder viewHolder = new StickyHeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.conversation_item_last_seen, parent, false));
adapter.onBindLastSeenViewHolder(viewHolder, unreadCount);
int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED);
int childWidth = ViewGroup.getChildMeasureSpec(widthSpec, parent.getPaddingLeft() + parent.getPaddingRight(), viewHolder.itemView.getLayoutParams().width);
int childHeight = ViewGroup.getChildMeasureSpec(heightSpec, parent.getPaddingTop() + parent.getPaddingBottom(), viewHolder.itemView.getLayoutParams().height);
viewHolder.itemView.measure(childWidth, childHeight);
viewHolder.itemView.layout(0, 0, viewHolder.itemView.getMeasuredWidth(), viewHolder.itemView.getMeasuredHeight());
return viewHolder;
}
}

View file

@ -1,101 +0,0 @@
package org.thoughtcrime.securesms.conversation;
import android.app.Application;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.libsignal.protocol.util.Pair;
import org.thoughtcrime.securesms.database.DatabaseObserver;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.util.concurrent.SerialMonoLifoExecutor;
import java.util.concurrent.Executor;
public class MessageCountsViewModel extends ViewModel {
private static final Executor EXECUTOR = new SerialMonoLifoExecutor(SignalExecutors.BOUNDED);
private final Application context;
private final MutableLiveData<Long> threadId = new MutableLiveData<>(-1L);
private final LiveData<Pair<Integer, Integer>> unreadCounts;
private DatabaseObserver.Observer observer;
public MessageCountsViewModel() {
this.context = ApplicationDependencies.getApplication();
this.unreadCounts = Transformations.switchMap(Transformations.distinctUntilChanged(threadId), id -> {
MutableLiveData<Pair<Integer, Integer>> counts = new MutableLiveData<>(new Pair<>(0, 0));
if (id == -1L) {
return counts;
}
if (observer != null) {
ApplicationDependencies.getDatabaseObserver().unregisterObserver(observer);
}
observer = new DatabaseObserver.Observer() {
private int previousUnreadCount = -1;
@Override
public void onChanged() {
EXECUTOR.execute(() -> {
int unreadCount = getUnreadCount(context, id);
if (unreadCount != previousUnreadCount) {
previousUnreadCount = unreadCount;
counts.postValue(new Pair<>(unreadCount, getUnreadMentionsCount(context, id)));
}
});
}
};
observer.onChanged();
ApplicationDependencies.getDatabaseObserver().registerConversationListObserver(observer);
return counts;
});
}
void setThreadId(long threadId) {
this.threadId.setValue(threadId);
}
void clearThreadId() {
this.threadId.postValue(-1L);
}
@NonNull LiveData<Integer> getUnreadMessagesCount() {
return Transformations.map(unreadCounts, Pair::first);
}
@NonNull LiveData<Integer> getUnreadMentionsCount() {
return Transformations.map(unreadCounts, Pair::second);
}
private int getUnreadCount(@NonNull Context context, long threadId) {
ThreadRecord threadRecord = SignalDatabase.threads().getThreadRecord(threadId);
return threadRecord != null ? threadRecord.getUnreadCount() : 0;
}
private int getUnreadMentionsCount(@NonNull Context context, long threadId) {
return SignalDatabase.messages().getUnreadMentionCount(threadId);
}
@Override
protected void onCleared() {
if (observer != null) {
ApplicationDependencies.getDatabaseObserver().unregisterObserver(observer);
}
}
}

View file

@ -5,7 +5,6 @@ import android.net.Uri
import android.text.Spannable
import android.text.SpannableString
import io.reactivex.rxjava3.core.Maybe
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.StreamUtil
import org.signal.core.util.concurrent.MaybeCompat
@ -167,22 +166,16 @@ class DraftRepository(
}
}
fun saveDrafts(recipient: Recipient?, threadId: Long, distributionType: Int, drafts: Drafts) {
require(threadId != -1L || recipient != null)
fun saveDrafts(threadId: Long, drafts: Drafts) {
require(threadId != -1L)
saveDraftsExecutor.execute {
if (drafts.isNotEmpty()) {
val actualThreadId = if (threadId == -1L) {
threadTable.getOrCreateThreadIdFor(recipient!!, distributionType)
} else {
threadId
}
draftTable.replaceDrafts(actualThreadId, drafts)
draftTable.replaceDrafts(threadId, drafts)
if (drafts.shouldUpdateSnippet()) {
threadTable.updateSnippet(actualThreadId, drafts.getSnippet(context), drafts.getUriSnippet(), System.currentTimeMillis(), MessageTypes.BASE_DRAFT_TYPE, true)
threadTable.updateSnippet(threadId, drafts.getSnippet(context), drafts.getUriSnippet(), System.currentTimeMillis(), MessageTypes.BASE_DRAFT_TYPE, true)
} else {
threadTable.update(actualThreadId, unarchive = false, allowDeletion = false)
threadTable.update(threadId, unarchive = false, allowDeletion = false)
}
} else if (threadId > 0) {
draftTable.clearDrafts(threadId)
@ -191,13 +184,6 @@ class DraftRepository(
}
}
@Deprecated("Not needed for CFv2")
fun loadDrafts(threadId: Long): Single<DatabaseDraft> {
return Single.fromCallable {
loadDraftsInternal(threadId)
}.subscribeOn(Schedulers.io())
}
private fun loadDraftsInternal(threadId: Long): DatabaseDraft {
val drafts: Drafts = draftTable.getDrafts(threadId)
val bodyRangesDraft: DraftTable.Draft? = drafts.getDraftOfType(DraftTable.Draft.BODY_RANGES)
@ -218,11 +204,6 @@ class DraftRepository(
return DatabaseDraft(drafts, updatedText)
}
@Deprecated("Not needed for CFv2")
fun loadDraftQuote(serialized: String): Maybe<ConversationMessage> {
return MaybeCompat.fromCallable { loadDraftQuoteInternal(serialized) }
}
private fun loadDraftQuoteInternal(serialized: String): ConversationMessage? {
val quoteId: QuoteId = QuoteId.deserialize(context, serialized) ?: return null
val messageRecord: MessageRecord = SignalDatabase.messages.getMessageFor(quoteId.id, quoteId.author)?.let {
@ -237,11 +218,6 @@ class DraftRepository(
return ConversationMessageFactory.createWithUnresolvedData(context, messageRecord, threadRecipient)
}
@Deprecated("Not needed for CFv2")
fun loadDraftMessageEdit(serialized: String): Maybe<ConversationMessage> {
return MaybeCompat.fromCallable { loadDraftMessageEditInternal(serialized) }
}
private fun loadDraftMessageEditInternal(serialized: String): ConversationMessage? {
val messageId = MessageId.deserialize(serialized)
val messageRecord: MessageRecord = SignalDatabase.messages.getMessageRecordOrNull(messageId.id) ?: return null

View file

@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.conversation.drafts
import org.thoughtcrime.securesms.database.DraftTable
import org.thoughtcrime.securesms.database.DraftTable.Drafts
import org.thoughtcrime.securesms.recipients.RecipientId
/**
* State object responsible for holding Voice Note draft state. The intention is to allow
@ -10,11 +9,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId
* management pattern going forward for drafts.
*/
data class DraftState(
@Deprecated("Not needed for CFv2")
val recipientId: RecipientId? = null,
val threadId: Long = -1,
@Deprecated("Not needed for CFv2")
val distributionType: Int = 0,
val textDraft: DraftTable.Draft? = null,
val bodyRangesDraft: DraftTable.Draft? = null,
val quoteDraft: DraftTable.Draft? = null,
@ -24,7 +19,7 @@ data class DraftState(
) {
fun copyAndClearDrafts(threadId: Long = this.threadId): DraftState {
return DraftState(recipientId = recipientId, threadId = threadId, distributionType = distributionType)
return DraftState(threadId = threadId)
}
fun toDrafts(): Drafts {

View file

@ -4,17 +4,13 @@ import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Maybe
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.components.location.SignalPlace
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.database.DraftTable.Draft
import org.thoughtcrime.securesms.database.MentionUtil
import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
import org.thoughtcrime.securesms.mms.QuoteId
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.Base64
import org.thoughtcrime.securesms.util.rx.RxStore
@ -40,16 +36,6 @@ class DraftViewModel @JvmOverloads constructor(
store.dispose()
}
@Deprecated("Not needed for CFv2")
fun setThreadId(threadId: Long) {
store.update { it.copy(threadId = threadId) }
}
@Deprecated("Not needed for CFv2")
fun setDistributionType(distributionType: Int) {
store.update { it.copy(distributionType = distributionType) }
}
fun saveEphemeralVoiceNoteDraft(draft: Draft) {
store.update { draftState ->
saveDrafts(draftState.copy(voiceNoteDraft = draft))
@ -67,11 +53,6 @@ class DraftViewModel @JvmOverloads constructor(
}
}
@Deprecated("Not needed for CFv2")
fun onRecipientChanged(recipient: Recipient) {
store.update { it.copy(recipientId = recipient.id) }
}
fun setMessageEditDraft(messageId: MessageId, text: String, mentions: List<Mention>, styleBodyRanges: BodyRangeList?) {
store.update {
val mentionRanges: BodyRangeList? = MentionUtil.mentionsToBodyRangeList(mentions)
@ -140,34 +121,10 @@ class DraftViewModel @JvmOverloads constructor(
}
private fun saveDrafts(state: DraftState): DraftState {
repository.saveDrafts(state.recipientId?.let { Recipient.resolved(it) }, state.threadId, state.distributionType, state.toDrafts())
repository.saveDrafts(state.threadId, state.toDrafts())
return state
}
@Deprecated("Not needed for CFv2")
fun loadDrafts(threadId: Long): Single<DraftRepository.DatabaseDraft> {
return repository
.loadDrafts(threadId)
.doOnSuccess { drafts ->
store.update { saveDrafts(it.copyAndSetDrafts(threadId, drafts.drafts)) }
}
.observeOn(AndroidSchedulers.mainThread())
}
@Deprecated("Not needed for CFv2")
fun loadDraftQuote(serialized: String): Maybe<ConversationMessage> {
return repository.loadDraftQuote(serialized)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
@Deprecated("Not needed for CFv2")
fun loadDraftEditMessage(serialized: String): Maybe<ConversationMessage> {
return repository.loadDraftMessageEdit(serialized)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
fun loadShareOrDraftData(lastShareDataTimestamp: Long): Maybe<DraftRepository.ShareOrDraftData> {
return repository.getShareOrDraftData(lastShareDataTimestamp)
.doOnSuccess { (_, drafts) ->

View file

@ -36,7 +36,6 @@ import org.thoughtcrime.securesms.conversation.ConversationAdapterBridge
import org.thoughtcrime.securesms.conversation.ConversationAdapterBridge.PulseRequest
import org.thoughtcrime.securesms.conversation.v2.items.InteractiveConversationElement
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.ThemeUtil
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
@ -402,12 +401,6 @@ class MultiselectItemDecoration(
}
}
}
if (!FeatureFlags.useConversationFragmentV2()) {
canvas.clipPath(path)
canvas.drawShade()
canvas.restore()
}
}
}
@ -422,12 +415,6 @@ class MultiselectItemDecoration(
path.addRect(child.left.toFloat(), child.top.toFloat(), child.right.toFloat(), child.bottom.toFloat(), Path.Direction.CW)
}
}
if (!FeatureFlags.useConversationFragmentV2()) {
canvas.clipPath(path, Region.Op.DIFFERENCE)
canvas.drawShade()
canvas.restore()
}
}
}

View file

@ -25,7 +25,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.conversation.ConversationFragment;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.util.DeviceProperties;
@ -123,8 +122,8 @@ public final class EnableCallNotificationSettingsDialog extends DialogFragment {
@Override
public void onDismiss(@NonNull DialogInterface dialog) {
super.onDismiss(dialog);
if (getParentFragment() instanceof ConversationFragment) {
((ConversationFragment) getParentFragment()).refreshList();
if (getParentFragment() instanceof Callback) {
((Callback) getParentFragment()).onCallNotificationSettingsDialogDismissed();
}
}
@ -230,4 +229,8 @@ public final class EnableCallNotificationSettingsDialog extends DialogFragment {
return bitmask;
}
public interface Callback {
void onCallNotificationSettingsDialogDismissed();
}
}

View file

@ -176,7 +176,7 @@ public final class SafetyNumberChangeRepository {
Log.d(TAG, "Saving identity result: " + result);
if (result == SignalIdentityKeyStore.SaveResult.NO_CHANGE) {
Log.i(TAG, "Archiving sessions explicitly as they appear to be out of sync.");
ApplicationDependencies.getProtocolStore().aci().sessions().archiveSession(changedRecipient.getRecipient().getId(), SignalServiceAddress.DEFAULT_DEVICE_ID);
ApplicationDependencies.getProtocolStore().aci().sessions().archiveSessions(changedRecipient.getRecipient().getId(), SignalServiceAddress.DEFAULT_DEVICE_ID);
ApplicationDependencies.getProtocolStore().aci().sessions().archiveSiblingSessions(mismatchAddress);
SignalDatabase.senderKeyShared().deleteAllFor(changedRecipient.getRecipient().getId());
}

View file

@ -1,88 +0,0 @@
package org.thoughtcrime.securesms.conversation.ui.groupcall;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.GroupCallPeekEvent;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import java.util.Objects;
public class GroupCallViewModel extends ViewModel {
private static final String TAG = Log.tag(GroupCallViewModel.class);
private final MutableLiveData<Boolean> activeGroup;
private final MutableLiveData<Boolean> ongoingGroupCall;
private final LiveData<Boolean> activeGroupCall;
private final MutableLiveData<Boolean> groupCallHasCapacity;
private @Nullable Recipient currentRecipient;
GroupCallViewModel() {
this.activeGroup = new MutableLiveData<>(false);
this.ongoingGroupCall = new MutableLiveData<>(false);
this.groupCallHasCapacity = new MutableLiveData<>(false);
this.activeGroupCall = LiveDataUtil.combineLatest(activeGroup, ongoingGroupCall, (active, ongoing) -> active && ongoing);
}
public @NonNull LiveData<Boolean> hasActiveGroupCall() {
return activeGroupCall;
}
public @NonNull LiveData<Boolean> groupCallHasCapacity() {
return groupCallHasCapacity;
}
public void onRecipientChange(@Nullable Recipient recipient) {
activeGroup.postValue(recipient != null && recipient.isActiveGroup());
if (Objects.equals(currentRecipient, recipient)) {
return;
}
ongoingGroupCall.postValue(false);
groupCallHasCapacity.postValue(false);
currentRecipient = recipient;
peekGroupCall();
}
public void peekGroupCall() {
if (isGroupCallCapable(currentRecipient)) {
Log.i(TAG, "peek call for " + currentRecipient.getId());
ApplicationDependencies.getSignalCallManager().peekGroupCall(currentRecipient.getId());
}
}
public void onGroupCallPeekEvent(@NonNull GroupCallPeekEvent groupCallPeekEvent) {
if (isGroupCallCapable(currentRecipient) && groupCallPeekEvent.getGroupRecipientId().equals(currentRecipient.getId())) {
Log.i(TAG, "update UI with call event: ongoing call: " + groupCallPeekEvent.isOngoing() + " hasCapacity: " + groupCallPeekEvent.callHasCapacity());
ongoingGroupCall.postValue(groupCallPeekEvent.isOngoing());
groupCallHasCapacity.postValue(groupCallPeekEvent.callHasCapacity());
} else {
Log.i(TAG, "Ignore call event for different recipient.");
}
}
private static boolean isGroupCallCapable(@Nullable Recipient recipient) {
return recipient != null && recipient.isActiveGroup() && recipient.isPushV2Group();
}
public static final class Factory implements ViewModelProvider.Factory {
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new GroupCallViewModel());
}
}
}

View file

@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner
import org.thoughtcrime.securesms.conversation.ConversationIntents
import org.thoughtcrime.securesms.util.Debouncer
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
import java.util.concurrent.TimeUnit
@ -16,7 +17,7 @@ import java.util.concurrent.TimeUnit
/**
* Wrapper activity for ConversationFragment.
*/
class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner {
open class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner {
companion object {
private const val STATE_WATERMARK = "share_data_watermark"
@ -75,7 +76,11 @@ class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaControl
private fun replaceFragment() {
val fragment = ConversationFragment().apply {
arguments = intent.extras
arguments = if (ConversationIntents.isBubbleIntentUri(intent.data)) {
ConversationIntents.createParentFragmentArguments(intent)
} else {
intent.extras
}
}
supportFragmentManager

View file

@ -262,7 +262,7 @@ class ConversationAdapterV2(
}
}
fun onMessageRequestStateChanged(isMessageRequestAccepted: Boolean) {
fun setMessageRequestIsAccepted(isMessageRequestAccepted: Boolean) {
val oldState = this.isMessageRequestAccepted
this.isMessageRequestAccepted = isMessageRequestAccepted

View file

@ -9,6 +9,7 @@ import android.Manifest
import android.annotation.SuppressLint
import android.app.ActivityOptions
import android.app.PendingIntent
import android.content.ActivityNotFoundException
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@ -20,6 +21,7 @@ import android.graphics.PorterDuffColorFilter
import android.graphics.Rect
import android.net.Uri
import android.os.Bundle
import android.provider.Browser
import android.provider.Settings
import android.text.Editable
import android.text.TextWatcher
@ -113,6 +115,7 @@ import org.thoughtcrime.securesms.components.mention.MentionAnnotation
import org.thoughtcrime.securesms.components.menu.ActionItem
import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity
import org.thoughtcrime.securesms.components.spoiler.SpoilerAnnotation
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState
@ -162,6 +165,7 @@ import org.thoughtcrime.securesms.conversation.drafts.DraftViewModel
import org.thoughtcrime.securesms.conversation.mutiselect.ConversationItemAnimator
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectItemDecoration
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardBottomSheet
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs
import org.thoughtcrime.securesms.conversation.quotes.MessageQuotesBottomSheet
@ -190,6 +194,7 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
import org.thoughtcrime.securesms.databinding.V2ConversationFragmentBinding
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.events.GroupCallPeekEvent
import org.thoughtcrime.securesms.events.ReminderUpdateEvent
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ItemDecoration
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackController
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicy
@ -247,6 +252,7 @@ import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.profiles.spoofing.ReviewCardDialogFragment
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment
import org.thoughtcrime.securesms.ratelimit.RecaptchaRequiredEvent
import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment
import org.thoughtcrime.securesms.recipients.Recipient
@ -270,6 +276,7 @@ import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.BubbleUtil
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.ContextUtil
import org.thoughtcrime.securesms.util.ConversationUtil
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.Debouncer
import org.thoughtcrime.securesms.util.DeleteDialog
@ -278,6 +285,7 @@ import org.thoughtcrime.securesms.util.DrawableUtil
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.FullscreenHelper
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.MessageConstraintsUtil
import org.thoughtcrime.securesms.util.MessageConstraintsUtil.getEditMessageThresholdHours
import org.thoughtcrime.securesms.util.MessageConstraintsUtil.isValidEditMessageSend
import org.thoughtcrime.securesms.util.PlayStoreUtil
@ -323,7 +331,9 @@ class ConversationFragment :
ScheduleMessageTimePickerBottomSheet.ScheduleCallback,
ScheduleMessageDialogCallback,
ConversationBottomSheetCallback,
SafetyNumberBottomSheet.Callbacks {
SafetyNumberBottomSheet.Callbacks,
EnableCallNotificationSettingsDialog.Callback,
MultiselectForwardBottomSheet.Callback {
companion object {
private val TAG = Log.tag(ConversationFragment::class.java)
@ -363,6 +373,11 @@ class ConversationFragment :
adapter.unregisterAdapterDataObserver(it)
}
scrollListener?.let {
_binding.conversationItemRecycler.removeOnScrollListener(it)
}
scrollListener = null
_binding.conversationItemRecycler.adapter = null
textDraftSaveDebouncer.clear()
@ -449,7 +464,6 @@ class ConversationFragment :
private lateinit var threadHeaderMarginDecoration: ThreadHeaderMarginDecoration
private lateinit var conversationItemDecorations: ConversationItemDecorations
private lateinit var optionsMenuCallback: ConversationOptionsMenuCallback
private lateinit var menuProvider: ConversationOptionsMenu.Provider
private lateinit var typingIndicatorDecoration: TypingIndicatorDecoration
private lateinit var backPressedCallback: BackPressedDelegate
@ -462,6 +476,8 @@ class ConversationFragment :
private var reShowScheduleMessagesBar: Boolean = false
private var composeTextEventsListener: ComposeTextEventsListener? = null
private var dataObserver: DataObserver? = null
private var menuProvider: ConversationOptionsMenu.Provider? = null
private var scrollListener: ScrollListener? = null
private val jumpAndPulseScrollStrategy = object : ScrollToPositionDelegate.ScrollStrategy {
override fun performScroll(recyclerView: RecyclerView, layoutManager: LinearLayoutManager, position: Int, smooth: Boolean) {
@ -550,7 +566,11 @@ class ConversationFragment :
binding.conversationVideoContainer.setClipToOutline(true)
SpoilerAnnotation.resetRevealedSpoilers()
registerForResults()
inputPanel.setMediaListener(InputPanelMediaListener())
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
@ -580,6 +600,20 @@ class ConversationFragment :
}
viewModel.updateIdentityRecordsInBackground()
if (args.isFirstTimeInSelfCreatedGroup) {
conversationGroupViewModel.checkJustSelfInGroup().subscribeBy(
onSuccess = {
GroupLinkInviteFriendsBottomSheetDialogFragment.show(childFragmentManager, it)
}
).addTo(disposables)
}
ConversationUtil.refreshRecipientShortcuts()
if (SignalStore.rateLimit().needsRecaptcha()) {
RecaptchaProofBottomSheetFragment.show(childFragmentManager)
}
}
override fun onPause() {
@ -618,6 +652,22 @@ class ConversationFragment :
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
}
override fun startActivity(intent: Intent) {
if (intent.getStringArrayExtra(Browser.EXTRA_APPLICATION_ID) != null) {
intent.removeExtra(Browser.EXTRA_APPLICATION_ID)
}
try {
super.startActivity(intent)
} catch (e: ActivityNotFoundException) {
Log.w(TAG, e)
toast(
toastTextId = R.string.ConversationActivity_there_is_no_app_available_to_handle_this_link_on_your_device,
toastDuration = Toast.LENGTH_LONG
)
}
}
//endregion
//region Fragment callbacks and listeners
@ -752,6 +802,16 @@ class ConversationFragment :
override fun onCanceled() = Unit
override fun onCallNotificationSettingsDialogDismissed() {
adapter.notifyDataSetChanged()
}
override fun onFinishForwardAction() {
actionMode?.finish()
}
override fun onDismissForwardSheet() = Unit
//endregion
private fun observeConversationThread() {
@ -760,6 +820,7 @@ class ConversationFragment :
.conversationThreadState
.subscribeOn(Schedulers.io())
.doOnSuccess { state ->
adapter.setMessageRequestIsAccepted(state.meta.messageRequestData.isMessageRequestAccepted)
SignalLocalMetrics.ConversationOpen.onDataLoaded()
conversationItemDecorations.setFirstUnreadCount(state.meta.unreadCount)
colorizer.onGroupMembershipChanged(state.meta.groupMemberAcis)
@ -803,7 +864,7 @@ class ConversationFragment :
backPressedCallback = BackPressedDelegate()
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backPressedCallback)
menuProvider.afterFirstRenderMode = true
menuProvider?.afterFirstRenderMode = true
viewLifecycleOwner.lifecycle.addObserver(LastScrolledPositionUpdater(adapter, layoutManager, viewModel))
@ -812,10 +873,6 @@ class ConversationFragment :
.distinctUntilChanged { r1, r2 -> r1 === r2 || r1.hasSameContent(r2) }
.subscribeBy(onNext = this::onRecipientChanged)
disposables += viewModel.markReadRequests
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(onNext = markReadHelper::onViewsRevealed)
disposables += viewModel.scrollButtonState
.subscribeBy(onNext = this::presentScrollButtons)
@ -848,7 +905,7 @@ class ConversationFragment :
setOnClickListener(sendButtonListener)
setScheduledSendListener(sendButtonListener)
isEnabled = true
post { sendButton.triggerSelectedChangedEvent() }
sendButton.triggerSelectedChangedEvent()
}
sendEditButton.setOnClickListener { handleSendEditMessage() }
@ -946,7 +1003,6 @@ class ConversationFragment :
initializeInlineSearch()
inputPanel.setListener(InputPanelListener())
inputPanel.setMediaListener(InputPanelMediaListener())
viewModel
.getScheduledMessagesCount()
@ -971,6 +1027,11 @@ class ConversationFragment :
val conversationUpdateTick = ConversationUpdateTick { adapter.updateTimestamps() }
viewLifecycleOwner.lifecycle.addObserver(conversationUpdateTick)
if (args.conversationScreenType.isInPopup) {
composeText.requestFocus()
binding.conversationInputPanel.quickAttachmentToggle.disable()
}
}
private fun initializeInlineSearch() {
@ -1142,7 +1203,7 @@ class ConversationFragment :
presentChatColors(recipient.chatColors)
invalidateOptionsMenu()
adapter.onMessageRequestStateChanged(!viewModel.hasMessageRequestState)
adapter.setMessageRequestIsAccepted(!viewModel.hasMessageRequestState)
}
private fun invalidateOptionsMenu() {
@ -1152,15 +1213,17 @@ class ConversationFragment :
}
private fun presentActionBarMenu() {
optionsMenuCallback = ConversationOptionsMenuCallback()
menuProvider = ConversationOptionsMenu.Provider(optionsMenuCallback, disposables)
binding.toolbar.addMenuProvider(menuProvider)
invalidateOptionsMenu()
if (!args.conversationScreenType.isInPopup) {
optionsMenuCallback = ConversationOptionsMenuCallback()
menuProvider = ConversationOptionsMenu.Provider(optionsMenuCallback, disposables)
binding.toolbar.addMenuProvider(menuProvider!!)
invalidateOptionsMenu()
}
when (args.conversationScreenType) {
ConversationScreenType.NORMAL -> presentNavigationIconForNormal()
ConversationScreenType.BUBBLE -> presentNavigationIconForBubble()
ConversationScreenType.POPUP -> Unit
ConversationScreenType.BUBBLE,
ConversationScreenType.POPUP -> presentNavigationIconForBubble()
}
}
@ -1199,8 +1262,10 @@ class ConversationFragment :
titleView.clearExpiring()
}
titleView.setOnClickListener {
optionsMenuCallback.handleConversationSettings()
if (!args.conversationScreenType.isInPopup) {
titleView.setOnClickListener {
optionsMenuCallback.handleConversationSettings()
}
}
}
@ -1399,6 +1464,12 @@ class ConversationFragment :
return
}
if (!MessageConstraintsUtil.isWithinMaxEdits(editMessage)) {
Log.i(TAG, "Too many edits to the message")
Dialogs.showAlertDialog(requireContext(), null, resources.getQuantityString(R.plurals.ConversationActivity_edit_message_too_many_edits, MessageConstraintsUtil.MAX_EDIT_COUNT, MessageConstraintsUtil.MAX_EDIT_COUNT))
return
}
if (!isValidEditMessageSend(editMessage, System.currentTimeMillis())) {
Log.i(TAG, "Edit message no longer valid")
val editDurationHours = getEditMessageThresholdHours()
@ -1415,7 +1486,8 @@ class ConversationFragment :
layoutManager = ConversationLayoutManager(requireContext())
binding.conversationItemRecycler.setHasFixedSize(false)
binding.conversationItemRecycler.layoutManager = layoutManager
binding.conversationItemRecycler.addOnScrollListener(ScrollListener())
scrollListener = ScrollListener()
binding.conversationItemRecycler.addOnScrollListener(scrollListener!!)
adapter = ConversationAdapterV2(
lifecycleOwner = viewLifecycleOwner,
@ -1723,7 +1795,9 @@ class ConversationFragment :
disposables += send
.doOnSubscribe {
if (clearCompose) {
composeTextEventsListener?.typingStatusEnabled = false
composeText.setText("")
composeTextEventsListener?.typingStatusEnabled = true
attachmentManager.clear(GlideApp.with(this@ConversationFragment), false)
inputPanel.clearQuote()
}
@ -1739,9 +1813,6 @@ class ConversationFragment :
private fun onSendComplete() {
if (isDetached || activity?.isFinishing == true) {
if (args.conversationScreenType.isInPopup) {
activity?.finish()
}
return
}
@ -1753,6 +1824,10 @@ class ConversationFragment :
draftViewModel.onSendComplete()
inputPanel.exitEditMessageMode()
if (args.conversationScreenType.isInPopup) {
activity?.finish()
}
}
private fun handleRecentSafetyNumberChange(changedRecords: List<IdentityRecord>) {
@ -1964,6 +2039,7 @@ class ConversationFragment :
} else if (isSearchRequested) {
searchMenuItem?.collapseActionView()
} else if (args.conversationScreenType.isInBubble) {
isEnabled = false
requireActivity().onBackPressed()
} else {
requireActivity().finish()
@ -2087,10 +2163,17 @@ class ConversationFragment :
}
private fun handleDeleteMessages(messageParts: Set<MultiselectPart>) {
val records = messageParts.map(MultiselectPart::getMessageRecord).toSet()
disposables += DeleteDialog.show(
context = requireContext(),
messageRecords = messageParts.map(MultiselectPart::getMessageRecord).toSet()
).subscribe()
messageRecords = records
).subscribe { (deleted: Boolean, _: Boolean) ->
if (!deleted) return@subscribe
val editMessageId = inputPanel.editMessageId?.id
if (editMessageId != null && records.any { it.id == editMessageId }) {
inputPanel.exitEditMessageMode()
}
}
}
private inner class SwipeAvailabilityProvider : ConversationItemSwipeCallback.SwipeAvailabilityProvider {
@ -2180,6 +2263,10 @@ class ConversationFragment :
return layoutManager.findFirstCompletelyVisibleItemPosition() > 4
}
private fun shouldScrollToBottom(): Boolean {
return isScrolledToBottom() || layoutManager.findFirstVisibleItemPosition() <= 0
}
/**
* Controls animation and visibility of the scrollDateHeader.
*/
@ -2240,9 +2327,11 @@ class ConversationFragment :
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (isScrolledToBottom()) {
viewModel.setShowScrollButtons(false)
viewModel.setShowScrollButtonsForScrollPosition(showScrollButtons = false, willScrollToBottomOnNewMessage = true)
} else if (isScrolledPastButtonThreshold()) {
viewModel.setShowScrollButtons(true)
viewModel.setShowScrollButtonsForScrollPosition(showScrollButtons = true, willScrollToBottomOnNewMessage = false)
} else {
viewModel.setShowScrollButtonsForScrollPosition(showScrollButtons = false, willScrollToBottomOnNewMessage = shouldScrollToBottom())
}
presentComposeDivider()
@ -2276,10 +2365,9 @@ class ConversationFragment :
private inner class DataObserver : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
Log.d(TAG, "onItemRangeInserted $positionStart $itemCount")
if (positionStart == 0 && itemCount == 1 && !binding.conversationItemRecycler.canScrollVertically(1)) {
Log.d(TAG, "Requesting scroll to bottom.")
if (positionStart == 0 && itemCount == 1 && shouldScrollToBottom()) {
layoutManager.scrollToPositionWithOffset(0, 0)
scrollListener?.onScrolled(binding.conversationItemRecycler, 0, 0)
}
}
@ -2301,6 +2389,10 @@ class ConversationFragment :
actionMode?.setTitle(calculateSelectedItemCount())
}
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
scrollListener?.onScrolled(binding.conversationItemRecycler, 0, 0)
}
}
//endregion Scroll Handling
@ -2723,10 +2815,7 @@ class ConversationFragment :
ViewUtil.hideKeyboard(requireContext(), itemView)
val showScrollButtons = viewModel.showScrollButtonsSnapshot
if (showScrollButtons) {
viewModel.setShowScrollButtons(false)
}
viewModel.setHideScrollButtonsForReactionOverlay(true)
val targetViews: InteractiveConversationElement = target
handleReaction(
@ -2764,9 +2853,7 @@ class ConversationFragment :
ViewUtil.fadeIn(targetViews.quotedIndicatorView!!, 150)
}
if (showScrollButtons) {
viewModel.setShowScrollButtons(true)
}
viewModel.setHideScrollButtonsForReactionOverlay(false)
}
}
)
@ -3834,6 +3921,16 @@ class ConversationFragment :
groupCallViewModel.onGroupCallPeekEvent(groupCallPeekEvent)
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onReminderUpdateEvent(reminderUpdateEvent: ReminderUpdateEvent) {
viewModel.refreshReminder()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onRecaptchaRequiredEvent(recaptchaRequiredEvent: RecaptchaRequiredEvent) {
RecaptchaProofBottomSheetFragment.show(childFragmentManager)
}
//endregion
private inner class SearchEventListener : ConversationSearchBottomBar.EventListener {

View file

@ -288,15 +288,14 @@ class ConversationRepository(
}
fun getMessageCounts(threadId: Long): Flowable<MessageCounts> {
return RxDatabaseObserver.conversationList
return RxDatabaseObserver.conversation(threadId)
.map { getUnreadCount(threadId) }
.distinctUntilChanged()
.map { MessageCounts(it, getUnreadMentionsCount(threadId)) }
}
private fun getUnreadCount(threadId: Long): Int {
val threadRecord = SignalDatabase.threads.getThreadRecord(threadId)
return threadRecord?.unreadCount ?: 0
return SignalDatabase.messages.getUnreadCount(threadId)
}
private fun getUnreadMentionsCount(threadId: Long): Int {

View file

@ -1,7 +1,20 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.conversation.v2
/**
* State information used to display the scroll to next mention and scroll to bottom buttons.
*/
data class ConversationScrollButtonState(
val showScrollButtons: Boolean = false,
val hideScrollButtonsForReactionOverlay: Boolean = false,
val showScrollButtonsForScrollPosition: Boolean = false,
val willScrollToBottomOnNewMessage: Boolean = true,
val unreadCount: Int = 0,
val hasMentions: Boolean = false
)
) {
val showScrollButtons: Boolean
get() = !hideScrollButtonsForReactionOverlay && (showScrollButtonsForScrollPosition || (!willScrollToBottomOnNewMessage && unreadCount > 0))
}

View file

@ -20,7 +20,6 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.addTo
import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.kotlin.subscribeBy
import io.reactivex.rxjava3.processors.PublishProcessor
import io.reactivex.rxjava3.schedulers.Schedulers
import io.reactivex.rxjava3.subjects.BehaviorSubject
import io.reactivex.rxjava3.subjects.PublishSubject
@ -98,11 +97,6 @@ class ConversationViewModel(
private val _conversationThreadState: Subject<ConversationThreadState> = BehaviorSubject.create()
val conversationThreadState: Single<ConversationThreadState> = _conversationThreadState.firstOrError()
private val _markReadProcessor: PublishProcessor<Long> = PublishProcessor.create()
val markReadRequests: Flowable<Long> = _markReadProcessor
.onBackpressureBuffer()
.distinct()
val pagingController = ProxyPagingController<ConversationElementKey>()
val groupMemberServiceIds: Observable<List<ServiceId>> = recipientRepository
@ -245,13 +239,26 @@ class ConversationViewModel(
_searchQuery.onNext(query ?: "")
}
fun refreshReminder() {
refreshReminder.onNext(Unit)
}
override fun onCleared() {
disposables.clear()
}
fun setShowScrollButtons(showScrollButtons: Boolean) {
fun setShowScrollButtonsForScrollPosition(showScrollButtons: Boolean, willScrollToBottomOnNewMessage: Boolean) {
scrollButtonStateStore.update {
it.copy(showScrollButtons = showScrollButtons)
it.copy(
showScrollButtonsForScrollPosition = showScrollButtons,
willScrollToBottomOnNewMessage = willScrollToBottomOnNewMessage
)
}
}
fun setHideScrollButtonsForReactionOverlay(hide: Boolean) {
scrollButtonStateStore.update {
it.copy(hideScrollButtonsForReactionOverlay = hide)
}
}

View file

@ -11,7 +11,6 @@ import org.signal.core.util.logging.Log
import org.signal.core.util.toInt
import org.signal.paging.PagedDataSource
import org.thoughtcrime.securesms.conversation.ConversationData
import org.thoughtcrime.securesms.conversation.ConversationDataSource
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory
import org.thoughtcrime.securesms.database.MessageTable

View file

@ -3,11 +3,13 @@ package org.thoughtcrime.securesms.conversation.v2.groups
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Maybe
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.addTo
import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.schedulers.Schedulers
import io.reactivex.rxjava3.subjects.BehaviorSubject
import io.reactivex.rxjava3.subjects.Subject
import org.signal.core.util.Result
@ -15,6 +17,7 @@ import org.signal.core.util.concurrent.subscribeWithSubject
import org.thoughtcrime.securesms.conversation.v2.ConversationRecipientRepository
import org.thoughtcrime.securesms.database.GroupTable
import org.thoughtcrime.securesms.database.model.GroupRecord
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
import org.thoughtcrime.securesms.groups.v2.GroupBlockJoinRequestResult
import org.thoughtcrime.securesms.groups.v2.GroupManagementRepository
@ -37,6 +40,8 @@ class ConversationGroupViewModel(
private val _groupActiveState: Subject<ConversationGroupActiveState> = BehaviorSubject.create()
private val _memberLevel: BehaviorSubject<ConversationGroupMemberLevel> = BehaviorSubject.create()
private var firstTimeInviteFriendsTriggered: Boolean = false
val groupRecordSnapshot: GroupRecord?
get() = _groupRecord.value
@ -106,6 +111,27 @@ class ConversationGroupViewModel(
.addTo(disposables)
}
/**
* Emits the group id if we are the only member of the group.
*/
fun checkJustSelfInGroup(): Maybe<GroupId.V2> {
if (firstTimeInviteFriendsTriggered) {
return Maybe.empty()
}
firstTimeInviteFriendsTriggered = true
return _groupRecord
.firstOrError()
.flatMapMaybe { groupRecord ->
groupManagementRepository.isJustSelf(groupRecord.id).flatMapMaybe {
if (it && groupRecord.id.isV2) Maybe.just(groupRecord.id.requireV2()) else Maybe.empty()
}
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
class Factory(private val threadId: Long, private val recipientRepository: ConversationRecipientRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.cast(ConversationGroupViewModel(threadId, recipientRepository = recipientRepository)) as T

View file

@ -104,8 +104,7 @@ class AttachmentKeyboardFragment : LoggingFragment(R.layout.attachment_keyboard_
if (paymentsValues.paymentsAvailability.isSendAllowed &&
!recipient.isSelf &&
!recipient.isGroup &&
recipient.isRegistered &&
!recipient.isForceSmsSelection
recipient.isRegistered
) {
attachmentKeyboardView.filterAttachmentKeyboardButtons(null)
} else {

View file

@ -118,7 +118,6 @@ import org.thoughtcrime.securesms.contacts.paged.ContactSearchMediator;
import org.thoughtcrime.securesms.contacts.paged.ContactSearchState;
import org.thoughtcrime.securesms.contacts.sync.CdsPermanentErrorBottomSheet;
import org.thoughtcrime.securesms.contacts.sync.CdsTemporaryErrorBottomSheet;
import org.thoughtcrime.securesms.conversation.ConversationFragment;
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterRequest;
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterSource;
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationListFilterPullView;
@ -161,6 +160,7 @@ import org.thoughtcrime.securesms.util.AppForegroundObserver;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.ConversationUtil;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.PlayStoreUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
@ -919,8 +919,22 @@ public class ConversationListFragment extends MainFragment implements ActionMode
requireCallback().getSearchToolbar().get();
}
if (getContext() != null) {
ConversationFragment.prepare(getContext());
Context context = getContext();
if (context != null) {
FrameLayout parent = new FrameLayout(context);
parent.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT));
if (SignalStore.internalValues().useConversationItemV2()) {
CachedInflater.from(context).cacheUntilLimit(R.layout.v2_conversation_item_text_only_incoming, parent, 25);
CachedInflater.from(context).cacheUntilLimit(R.layout.v2_conversation_item_text_only_outgoing, parent, 25);
} else {
CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_received_text_only, parent, 25);
CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_sent_text_only, parent, 25);
}
CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_received_multimedia, parent, 10);
CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_sent_multimedia, parent, 10);
CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_update, parent, 5);
CachedInflater.from(context).cacheUntilLimit(R.layout.cursor_adapter_header_footer_view, parent, 2);
}
}

View file

@ -325,7 +325,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
joinMembersDisposable.dispose();
setSubjectViewText(null);
fromView.setText(recipient.get(), false);
fromView.setText(recipient.get(), recipient.get().getDisplayNameOrUsername(getContext()), false, null, false);
setSubjectViewText(SearchUtil.getHighlightedSpan(locale, searchStyleFactory, messageResult.getBodySnippet(), highlightSubstring, SearchUtil.MATCH_ALL));
dateView.setText(DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, messageResult.getReceivedTimestampMs()));
archivedView.setVisibility(GONE);
@ -336,7 +336,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
setSelectedConversations(new ConversationSet());
setBadgeFromRecipient(recipient.get());
contactPhotoImage.setAvatar(glideRequests, recipient.get(), !batchMode);
contactPhotoImage.setAvatar(glideRequests, recipient.get(), !batchMode, true);
}
public void bindGroupWithMembers(@NonNull LifecycleOwner lifecycleOwner,
@ -555,12 +555,17 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
}
if (highlightSubstring != null) {
String name = recipient.isSelf() ? getContext().getString(R.string.note_to_self) : recipient.getDisplayName(getContext());
fromView.setText(recipient, SearchUtil.getHighlightedSpan(locale, searchStyleFactory, new SpannableString(name), highlightSubstring, SearchUtil.MATCH_ALL), true, null);
String name;
if (thread != null && recipient.isSelf()) {
name = getContext().getString(R.string.note_to_self);
} else {
name = recipient.getDisplayName(getContext());
}
fromView.setText(recipient, SearchUtil.getHighlightedSpan(locale, searchStyleFactory, new SpannableString(name), highlightSubstring, SearchUtil.MATCH_ALL), true, null, thread != null);
} else {
fromView.setText(recipient, false);
}
contactPhotoImage.setAvatar(glideRequests, recipient, !batchMode);
contactPhotoImage.setAvatar(glideRequests, recipient, !batchMode, thread != null);
setBadgeFromRecipient(recipient);
}

View file

@ -69,8 +69,8 @@ public class SignalBaseIdentityKeyStore {
public @NonNull SaveResult saveIdentity(SignalProtocolAddress address, IdentityKey identityKey, boolean nonBlockingApproval) {
try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
IdentityStoreRecord identityRecord = cache.get(address.getName());
RecipientId recipientId = RecipientId.fromSidOrE164(address.getName());
IdentityStoreRecord identityRecord = cache.get(address.getName());
RecipientId recipientId = RecipientId.fromSidOrE164(address.getName());
if (identityRecord == null) {
Log.i(TAG, "Saving new identity for " + address);

View file

@ -6,7 +6,6 @@ import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.NoSessionException;
import org.signal.libsignal.protocol.SignalProtocolAddress;
import org.signal.libsignal.protocol.message.CiphertextMessage;
import org.signal.libsignal.protocol.state.SessionRecord;
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
import org.thoughtcrime.securesms.database.SessionTable;
@ -126,13 +125,23 @@ public class TextSecureSessionStore implements SignalServiceSessionStore {
}
}
}
public void archiveSession(@NonNull ServiceId serviceId, int deviceId) {
try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
archiveSession(new SignalProtocolAddress(serviceId.toString(), deviceId));
}
}
public void archiveSession(@NonNull RecipientId recipientId, int deviceId) {
public void archiveSessions(@NonNull RecipientId recipientId, int deviceId) {
try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
Recipient recipient = Recipient.resolved(recipientId);
if (recipient.hasServiceId()) {
archiveSession(new SignalProtocolAddress(recipient.requireServiceId().toString(), deviceId));
if (recipient.hasAci()) {
archiveSession(new SignalProtocolAddress(recipient.requireAci().toString(), deviceId));
}
if (recipient.hasPni()) {
archiveSession(new SignalProtocolAddress(recipient.requirePni().toString(), deviceId));
}
if (recipient.hasE164()) {

View file

@ -34,7 +34,7 @@ import org.thoughtcrime.securesms.jobs.CallSyncEventJob
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.CallEvent
import java.util.UUID
import java.util.concurrent.TimeUnit
@ -625,11 +625,11 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
fun insertOrUpdateGroupCallFromRingState(
ringId: Long,
groupRecipientId: RecipientId,
ringerUUID: UUID,
ringerAci: ACI,
dateReceived: Long,
ringState: RingUpdate
) {
val ringerRecipient = Recipient.externalPush(ServiceId.from(ringerUUID))
val ringerRecipient = Recipient.externalPush(ringerAci)
handleGroupRingState(ringId, groupRecipientId, ringerRecipient.id, dateReceived, ringState)
}
@ -961,7 +961,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
(
sort_name GLOB ? OR
${RecipientTable.TABLE_NAME}.${RecipientTable.USERNAME} GLOB ? OR
${RecipientTable.TABLE_NAME}.${RecipientTable.PHONE} GLOB ? OR
${RecipientTable.TABLE_NAME}.${RecipientTable.E164} GLOB ? OR
${RecipientTable.TABLE_NAME}.${RecipientTable.EMAIL} GLOB ?
)
"""

View file

@ -53,7 +53,7 @@ class DistributionListTables constructor(context: Context?, databaseHelper: Sign
RecipientTable.TABLE_NAME,
null,
contentValuesOf(
RecipientTable.GROUP_TYPE to RecipientTable.GroupType.DISTRIBUTION_LIST.id,
RecipientTable.TYPE to RecipientTable.RecipientType.DISTRIBUTION_LIST.id,
RecipientTable.DISTRIBUTION_LIST_ID to DistributionListId.MY_STORY_ID,
RecipientTable.STORAGE_SERVICE_ID to Base64.encodeBytes(StorageSyncHelper.generateKey()),
RecipientTable.PROFILE_SHARING to 1

View file

@ -55,11 +55,11 @@ import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
import org.whispersystems.signalservice.api.push.DistributionId
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.util.UuidUtil
import java.io.Closeable
import java.security.SecureRandom
import java.util.Optional
import java.util.UUID
import java.util.stream.Collectors
import javax.annotation.CheckReturnValue
@ -825,8 +825,8 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
writableDatabase.withinTransaction { db ->
val record = getGroup(groupIdV1).get()
val newMembers: MutableList<RecipientId> = DecryptedGroupUtil.membersToUuidList(decryptedGroup.membersList).toRecipientIds()
val pendingMembers: List<RecipientId> = DecryptedGroupUtil.pendingToUuidList(decryptedGroup.pendingMembersList).toRecipientIds()
val newMembers: MutableList<RecipientId> = DecryptedGroupUtil.membersToServiceIdList(decryptedGroup.membersList).toRecipientIds()
val pendingMembers: List<RecipientId> = DecryptedGroupUtil.pendingToServiceIdList(decryptedGroup.pendingMembersList).toRecipientIds()
newMembers.addAll(pendingMembers)
val droppedMembers: List<RecipientId> = SetUtil.difference(record.members, newMembers).toList()
@ -879,11 +879,11 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
val change = GroupChangeReconstruct.reconstructGroupChange(existingGroup.get().requireV2GroupProperties().decryptedGroup, decryptedGroup)
val addedMembers: Set<RecipientId> = DecryptedGroupUtil.membersToUuidList(change.newMembersList).toRecipientIds().toSet()
val removedMembers: Set<RecipientId> = DecryptedGroupUtil.removedMembersUuidList(change).toRecipientIds().toSet()
val addedInvites: Set<RecipientId> = DecryptedGroupUtil.pendingToUuidList(change.newPendingMembersList).toRecipientIds().toSet()
val removedInvites: Set<RecipientId> = DecryptedGroupUtil.removedPendingMembersUuidList(change).toRecipientIds().toSet()
val acceptedInvites: Set<RecipientId> = DecryptedGroupUtil.membersToUuidList(change.promotePendingMembersList).toRecipientIds().toSet()
val addedMembers: Set<RecipientId> = DecryptedGroupUtil.membersToServiceIdList(change.newMembersList).toRecipientIds().toSet()
val removedMembers: Set<RecipientId> = DecryptedGroupUtil.removedMembersServiceIdList(change).toRecipientIds().toSet()
val addedInvites: Set<RecipientId> = DecryptedGroupUtil.pendingToServiceIdList(change.newPendingMembersList).toRecipientIds().toSet()
val removedInvites: Set<RecipientId> = DecryptedGroupUtil.removedPendingMembersServiceIdList(change).toRecipientIds().toSet()
val acceptedInvites: Set<RecipientId> = DecryptedGroupUtil.membersToServiceIdList(change.promotePendingMembersList).toRecipientIds().toSet()
unmigratedV1Members -= addedMembers
unmigratedV1Members -= removedMembers
@ -898,7 +898,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
if (existingGroup.isPresent && existingGroup.get().isV2Group) {
val change = GroupChangeReconstruct.reconstructGroupChange(existingGroup.get().requireV2GroupProperties().decryptedGroup, decryptedGroup)
val removed: List<UUID> = DecryptedGroupUtil.removedMembersUuidList(change)
val removed: List<ServiceId> = DecryptedGroupUtil.removedMembersServiceIdList(change)
if (removed.isNotEmpty()) {
val distributionId = existingGroup.get().distributionId!!
@ -1172,15 +1172,15 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
DecryptedGroup.parseFrom(decryptedGroupBytes)
}
val bannedMembers: Set<UUID> by lazy {
DecryptedGroupUtil.bannedMembersToUuidSet(decryptedGroup.bannedMembersList)
val bannedMembers: Set<ServiceId> by lazy {
DecryptedGroupUtil.bannedMembersToServiceIdSet(decryptedGroup.bannedMembersList)
}
fun isAdmin(recipient: Recipient): Boolean {
val serviceId = recipient.serviceId
return if (serviceId.isPresent) {
DecryptedGroupUtil.findMemberByUuid(decryptedGroup.membersList, serviceId.get().uuid())
DecryptedGroupUtil.findMemberByUuid(decryptedGroup.membersList, serviceId.get().rawUuid)
.map { it.role == Member.Role.ADMINISTRATOR }
.orElse(false)
} else {
@ -1197,7 +1197,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
return MemberLevel.NOT_A_MEMBER
}
var memberLevel: Optional<MemberLevel> = DecryptedGroupUtil.findMemberByUuid(decryptedGroup.membersList, serviceId.get().uuid())
var memberLevel: Optional<MemberLevel> = DecryptedGroupUtil.findMemberByUuid(decryptedGroup.membersList, serviceId.get().rawUuid)
.map { member ->
if (member.role == Member.Role.ADMINISTRATOR) {
MemberLevel.ADMINISTRATOR
@ -1207,12 +1207,12 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
}
if (memberLevel.isAbsent()) {
memberLevel = DecryptedGroupUtil.findPendingByUuid(decryptedGroup.pendingMembersList, serviceId.get().uuid())
memberLevel = DecryptedGroupUtil.findPendingByServiceId(decryptedGroup.pendingMembersList, serviceId.get())
.map { MemberLevel.PENDING_MEMBER }
}
if (memberLevel.isAbsent()) {
memberLevel = DecryptedGroupUtil.findRequestingByUuid(decryptedGroup.requestingMembersList, serviceId.get().uuid())
memberLevel = DecryptedGroupUtil.findRequestingByUuid(decryptedGroup.requestingMembersList, serviceId.get().rawUuid)
.map { _ -> MemberLevel.REQUESTING_MEMBER }
}
@ -1229,7 +1229,8 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
fun getMemberRecipientIds(memberSet: MemberSet): List<RecipientId> {
val includeSelf = memberSet.includeSelf
val selfUuid = SignalStore.account().requireAci().uuid()
val selfAci = SignalStore.account().requireAci()
val selfAciUuid = selfAci.rawUuid
val recipients: MutableList<RecipientId> = ArrayList(decryptedGroup.membersCount + decryptedGroup.pendingMembersCount)
var unknownMembers = 0
@ -1238,17 +1239,17 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
for (uuid in DecryptedGroupUtil.toUuidList(decryptedGroup.membersList)) {
if (UuidUtil.UNKNOWN_UUID == uuid) {
unknownMembers++
} else if (includeSelf || selfUuid != uuid) {
recipients += RecipientId.from(ServiceId.from(uuid))
} else if (includeSelf || selfAciUuid != uuid) {
recipients += RecipientId.from(ACI.from(uuid))
}
}
if (memberSet.includePending) {
for (uuid in DecryptedGroupUtil.pendingToUuidList(decryptedGroup.pendingMembersList)) {
if (UuidUtil.UNKNOWN_UUID == uuid) {
for (serviceId in DecryptedGroupUtil.pendingToServiceIdList(decryptedGroup.pendingMembersList)) {
if (serviceId.isUnknown) {
unknownPending++
} else if (includeSelf || selfUuid != uuid) {
recipients += RecipientId.from(ServiceId.from(uuid))
} else if (includeSelf || selfAci != serviceId) {
recipients += RecipientId.from(serviceId)
}
}
}
@ -1266,7 +1267,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
.asSequence()
.map { UuidUtil.fromByteStringOrNull(it.uuid) }
.filterNotNull()
.map { ServiceId.from(it) }
.map { ACI.from(it) }
.sortedBy { it.toString() }
.toList()
}
@ -1341,12 +1342,12 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
private fun gv2GroupActive(decryptedGroup: DecryptedGroup): Boolean {
val aci = SignalStore.account().requireAci()
return DecryptedGroupUtil.findMemberByUuid(decryptedGroup.membersList, aci.uuid()).isPresent ||
DecryptedGroupUtil.findPendingByUuid(decryptedGroup.pendingMembersList, aci.uuid()).isPresent
return DecryptedGroupUtil.findMemberByUuid(decryptedGroup.membersList, aci.rawUuid).isPresent ||
DecryptedGroupUtil.findPendingByServiceId(decryptedGroup.pendingMembersList, aci).isPresent
}
private fun List<UUID>.toRecipientIds(): MutableList<RecipientId> {
return uuidsToRecipientIds(this)
private fun List<ServiceId>.toRecipientIds(): MutableList<RecipientId> {
return serviceIdsToRecipientIds(this.asSequence())
}
private fun Collection<RecipientId>.serialize(): String {
@ -1362,15 +1363,14 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
}
}
private fun uuidsToRecipientIds(uuids: List<UUID>): MutableList<RecipientId> {
return uuids
.asSequence()
.map { uuid ->
if (uuid == UuidUtil.UNKNOWN_UUID) {
private fun serviceIdsToRecipientIds(serviceIds: Sequence<ServiceId>): MutableList<RecipientId> {
return serviceIds
.map { serviceId ->
if (serviceId.isUnknown) {
Log.w(TAG, "Saw an unknown UUID when mapping to RecipientIds!")
null
} else {
val id = RecipientId.from(ServiceId.from(uuid))
val id = RecipientId.from(serviceId)
val remapped = RemappedRecords.getInstance().getRecipient(id)
if (remapped.isPresent) {
Log.w(TAG, "Saw that $id remapped to $remapped. Using the mapping.")
@ -1386,8 +1386,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
}
private fun getV2GroupMembers(decryptedGroup: DecryptedGroup, shouldRetry: Boolean): List<RecipientId> {
val uuids: List<UUID> = DecryptedGroupUtil.membersToUuidList(decryptedGroup.membersList)
val ids: List<RecipientId> = uuidsToRecipientIds(uuids)
val ids: List<RecipientId> = DecryptedGroupUtil.membersToServiceIdList(decryptedGroup.membersList).toRecipientIds()
return if (RemappedRecords.getInstance().areAnyRemapped(ids)) {
if (shouldRetry) {

View file

@ -70,6 +70,14 @@ class IdentityTable internal constructor(context: Context?, databaseHelper: Sign
"""
}
fun getIdentityStoreRecord(serviceId: ServiceId?): IdentityStoreRecord? {
return if (serviceId != null) {
getIdentityStoreRecord(serviceId.toString())
} else {
null
}
}
fun getIdentityStoreRecord(addressName: String): IdentityStoreRecord? {
readableDatabase
.select()

View file

@ -153,6 +153,16 @@ class LocalMetricsDatabase private constructor(
writableDatabase.delete(TABLE_NAME, null, null)
}
fun getOldestMetricTime(eventName: String): Long {
readableDatabase.rawQuery("SELECT $CREATED_AT FROM $TABLE_NAME WHERE $EVENT_NAME = ? ORDER BY $CREATED_AT ASC", SqlUtil.buildArgs(eventName)).use { cursor ->
return if (cursor.moveToFirst()) {
cursor.getLong(0)
} else {
0
}
}
}
fun getMetrics(): List<EventMetrics> {
val db = readableDatabase
@ -213,7 +223,7 @@ class LocalMetricsDatabase private constructor(
}
}
private fun eventPercent(eventName: String, percent: Int): Long {
fun eventPercent(eventName: String, percent: Int): Long {
return percentile(EventTotals.VIEW_NAME, "$EVENT_NAME = '$eventName'", percent)
}

View file

@ -96,7 +96,7 @@ public final class MentionUtil {
BodyRangeList.Builder builder = BodyRangeList.newBuilder();
for (Mention mention : mentions) {
String uuid = Recipient.resolved(mention.getRecipientId()).requireServiceId().toString();
String uuid = Recipient.resolved(mention.getRecipientId()).requireAci().toString();
builder.addRanges(BodyRangeList.BodyRange.newBuilder()
.setMentionUuid(uuid)
.setStart(mention.getStart())

View file

@ -112,7 +112,6 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchove
import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange
import org.thoughtcrime.securesms.insights.InsightsConstants
import org.thoughtcrime.securesms.jobs.OptimizeMessageSearchIndexJob
import org.thoughtcrime.securesms.jobs.ThreadUpdateJob
import org.thoughtcrime.securesms.jobs.TrimThreadJob
@ -843,7 +842,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
val threadId = threads.getOrCreateThreadIdFor(recipient)
val messageId: MessageId = writableDatabase.withinTransaction { db ->
val self = Recipient.self()
val markRead = joinedUuids.contains(self.requireServiceId().uuid()) || self.id == sender
val markRead = joinedUuids.contains(self.requireServiceId().rawUuid) || self.id == sender
val updateDetails: ByteArray = GroupCallUpdateDetails.newBuilder()
.setEraId(eraId)
.setStartedCallUuid(Recipient.resolved(sender).requireServiceId().toString())
@ -917,7 +916,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
}
val updateDetail = GroupCallUpdateDetailsUtil.parse(message.body)
val containsSelf = joinedUuids.contains(SignalStore.account().requireAci().uuid())
val containsSelf = joinedUuids.contains(SignalStore.account().requireAci().rawUuid)
val sameEraId = updateDetail.eraId == eraId && !Util.isEmpty(eraId)
val inCallUuids = if (sameEraId) joinedUuids.map { it.toString() } else emptyList()
val contentValues = contentValuesOf(
@ -952,7 +951,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
MmsReader(cursor).use { reader ->
val record = reader.getNext() ?: return@withinTransaction false
val groupCallUpdateDetails = GroupCallUpdateDetailsUtil.parse(record.body)
val containsSelf = peekJoinedUuids.contains(SignalStore.account().requireAci().uuid())
val containsSelf = peekJoinedUuids.contains(SignalStore.account().requireAci().rawUuid)
val sameEraId = groupCallUpdateDetails.eraId == peekGroupCallEraId && !Util.isEmpty(peekGroupCallEraId)
val inCallUuids = if (sameEraId) {
@ -1091,10 +1090,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
if (unread && editedMessage == null) {
threads.incrementUnread(threadId, 1, 0)
}
if (message.subscriptionId != -1) {
recipients.setDefaultSubscriptionId(message.authorId, message.subscriptionId)
}
}
id
@ -1104,6 +1099,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
return Optional.empty()
}
threads.markAsActiveEarly(threadId)
if (!silent) {
ThreadUpdateJob.enqueue(threadId)
TrimThreadJob.enqueueAsync(threadId)
@ -2514,7 +2511,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
body = body,
attachments = attachments,
timestamp = timestamp,
subscriptionId = subscriptionId,
expiresIn = expiresIn,
viewOnce = viewOnce,
distributionType = distributionType,
@ -3097,7 +3093,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
if (message.isGroupUpdate && message.isV2Group) {
members += message.requireGroupV2Properties().allActivePendingAndRemovedMembers
.distinct()
.map { uuid -> RecipientId.from(ServiceId.from(uuid)) }
.map { serviceId -> RecipientId.from(serviceId) }
.toList()
members -= Recipient.self().id
@ -3724,15 +3720,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
.readToSingleInt()
}
fun getInsecureMessageSentCount(threadId: Long): Int {
return readableDatabase
.select("COUNT(*)")
.from(TABLE_NAME)
.where("$THREAD_ID = ? AND $outgoingInsecureMessageClause AND $DATE_SENT > ?", threadId, (System.currentTimeMillis() - InsightsConstants.PERIOD_IN_MILLIS))
.run()
.readToSingleInt()
}
fun getSecureMessageCount(threadId: Long): Int {
return readableDatabase
.select("COUNT(*)")
@ -3751,14 +3738,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
.readToSingleInt()
}
fun getInsecureMessageCountForInsights(): Int {
return getMessageCountForRecipientsAndType(outgoingInsecureMessageClause)
}
fun getSecureMessageCountForInsights(): Int {
return getMessageCountForRecipientsAndType(outgoingSecureMessageClause)
}
private fun hasSmsExportMessage(threadId: Long): Boolean {
return readableDatabase
.exists(TABLE_NAME)
@ -3766,15 +3745,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
.run()
}
private fun getMessageCountForRecipientsAndType(typeClause: String): Int {
return readableDatabase
.select("COUNT(*)")
.from(TABLE_NAME)
.where("$typeClause AND $DATE_SENT > ?", (System.currentTimeMillis() - InsightsConstants.PERIOD_IN_MILLIS))
.run()
.readToSingleInt()
}
private val outgoingInsecureMessageClause = "($TYPE & ${MessageTypes.BASE_TYPE_MASK}) = ${MessageTypes.BASE_SENT_TYPE} AND NOT ($TYPE & ${MessageTypes.SECURE_MESSAGE_BIT})"
private val outgoingSecureMessageClause = "($TYPE & ${MessageTypes.BASE_TYPE_MASK}) = ${MessageTypes.BASE_SENT_TYPE} AND ($TYPE & ${MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT})"
@ -4465,7 +4435,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
FROM ${RecipientTable.TABLE_NAME}
WHERE
${RecipientTable.TABLE_NAME}.${RecipientTable.ID} = $TO_RECIPIENT_ID AND
${RecipientTable.TABLE_NAME}.${RecipientTable.GROUP_TYPE} != ${RecipientTable.GroupType.NONE.id}
${RecipientTable.TABLE_NAME}.${RecipientTable.TYPE} != ${RecipientTable.RecipientType.INDIVIDUAL.id}
)
)
$qualifierWhere

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