mirror of
https://github.com/mollyim/mollyim-insider-android.git
synced 2025-05-12 21:30:40 +01:00
Merge tag 'v6.28.6' into molly-6.28
This commit is contained in:
commit
f78f0e5b9e
366 changed files with 4797 additions and 13797 deletions
|
@ -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
|
|
@ -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\", " +
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
/**
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>()
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -163,6 +163,10 @@ public class WebRtcCallViewModel extends ViewModel {
|
|||
return shouldShowSpeakerHint;
|
||||
}
|
||||
|
||||
public WebRtcAudioOutput getCurrentAudioOutput() {
|
||||
return getWebRtcControls().getValue().getAudioOutput();
|
||||
}
|
||||
|
||||
public LiveData<WebRtcEphemeralState> getEphemeralState() {
|
||||
return ephemeralState;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -262,7 +262,7 @@ class ConversationAdapterV2(
|
|||
}
|
||||
}
|
||||
|
||||
fun onMessageRequestStateChanged(isMessageRequestAccepted: Boolean) {
|
||||
fun setMessageRequestIsAccepted(isMessageRequestAccepted: Boolean) {
|
||||
val oldState = this.isMessageRequestAccepted
|
||||
this.isMessageRequestAccepted = isMessageRequestAccepted
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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 ?
|
||||
)
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue