mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-12 13:03:17 +01:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
264a47addf | ||
|
|
1c30a077c5 |
@@ -3,6 +3,7 @@ import com.android.build.api.dsl.ManagedVirtualDevice
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
id 'com.google.protobuf'
|
||||
id 'androidx.navigation.safeargs'
|
||||
id 'org.jlleitschuh.gradle.ktlint'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
@@ -10,11 +11,25 @@ plugins {
|
||||
id 'kotlin-parcelize'
|
||||
id 'com.squareup.wire'
|
||||
id 'translations'
|
||||
id 'licenses'
|
||||
}
|
||||
|
||||
apply from: 'static-ips.gradle'
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = 'com.google.protobuf:protoc:3.18.0'
|
||||
}
|
||||
generateProtoTasks {
|
||||
all().each { task ->
|
||||
task.builtins {
|
||||
java {
|
||||
option "lite"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wire {
|
||||
kotlin {
|
||||
javaInterop = true
|
||||
@@ -33,15 +48,15 @@ ktlint {
|
||||
version = "0.49.1"
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 1332
|
||||
def canonicalVersionName = "6.34.0"
|
||||
def canonicalVersionCode = 1327
|
||||
def canonicalVersionName = "6.32.5"
|
||||
|
||||
def postFixSize = 100
|
||||
def abiPostFix = ['universal' : 0,
|
||||
'armeabi-v7a' : 1,
|
||||
'arm64-v8a' : 2,
|
||||
'x86' : 3,
|
||||
'x86_64' : 4]
|
||||
def abiPostFix = ['universal' : 5,
|
||||
'armeabi-v7a' : 6,
|
||||
'arm64-v8a' : 7,
|
||||
'x86' : 8,
|
||||
'x86_64' : 9]
|
||||
|
||||
def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties') ]
|
||||
|
||||
@@ -522,8 +537,11 @@ dependencies {
|
||||
implementation project(':photoview')
|
||||
|
||||
implementation libs.libsignal.android
|
||||
implementation libs.google.protobuf.javalite
|
||||
|
||||
implementation libs.mobilecoin
|
||||
implementation(libs.mobilecoin) {
|
||||
exclude group: 'com.google.protobuf'
|
||||
}
|
||||
|
||||
implementation libs.signal.ringrtc
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class SignalInstrumentationApplicationContext : ApplicationContext() {
|
||||
|
||||
SignalExecutors.UNBOUNDED.execute {
|
||||
Log.blockUntilAllWritesFinished()
|
||||
LogDatabase.getInstance(this).logs.trimToSize()
|
||||
LogDatabase.getInstance(this).trimToSize()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ class V2ConversationItemShapeTest {
|
||||
|
||||
private val colorizer = Colorizer()
|
||||
|
||||
override val displayMode: ConversationItemDisplayMode = ConversationItemDisplayMode.Standard
|
||||
override val displayMode: ConversationItemDisplayMode = ConversationItemDisplayMode.STANDARD
|
||||
|
||||
override val clickListener: ConversationAdapter.ItemClickListener = FakeConversationItemClickListener
|
||||
override val selectedItems: Set<MultiselectPart> = emptySet()
|
||||
|
||||
@@ -293,22 +293,22 @@ class GroupTableTest {
|
||||
|
||||
private fun insertPushGroup(
|
||||
members: List<DecryptedMember> = listOf(
|
||||
DecryptedMember.Builder()
|
||||
.aciBytes(harness.self.requireAci().toByteString())
|
||||
.joinedAtRevision(0)
|
||||
.role(Member.Role.DEFAULT)
|
||||
DecryptedMember.newBuilder()
|
||||
.setAciBytes(harness.self.requireAci().toByteString())
|
||||
.setJoinedAtRevision(0)
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build(),
|
||||
DecryptedMember.Builder()
|
||||
.aciBytes(Recipient.resolved(harness.others[0]).requireAci().toByteString())
|
||||
.joinedAtRevision(0)
|
||||
.role(Member.Role.DEFAULT)
|
||||
DecryptedMember.newBuilder()
|
||||
.setAciBytes(Recipient.resolved(harness.others[0]).requireAci().toByteString())
|
||||
.setJoinedAtRevision(0)
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build()
|
||||
)
|
||||
): GroupId {
|
||||
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
|
||||
val decryptedGroupState = DecryptedGroup.Builder()
|
||||
.members(members)
|
||||
.revision(0)
|
||||
val decryptedGroupState = DecryptedGroup.newBuilder()
|
||||
.addAllMembers(members)
|
||||
.setRevision(0)
|
||||
.build()
|
||||
|
||||
return groupTable.create(groupMasterKey, decryptedGroupState)!!
|
||||
@@ -317,23 +317,23 @@ class GroupTableTest {
|
||||
private fun insertPushGroupWithSelfAndOthers(others: List<RecipientId>): GroupId {
|
||||
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
|
||||
|
||||
val selfMember: DecryptedMember = DecryptedMember.Builder()
|
||||
.aciBytes(harness.self.requireAci().toByteString())
|
||||
.joinedAtRevision(0)
|
||||
.role(Member.Role.DEFAULT)
|
||||
val selfMember: DecryptedMember = DecryptedMember.newBuilder()
|
||||
.setAciBytes(harness.self.requireAci().toByteString())
|
||||
.setJoinedAtRevision(0)
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build()
|
||||
|
||||
val otherMembers: List<DecryptedMember> = others.map { id ->
|
||||
DecryptedMember.Builder()
|
||||
.aciBytes(Recipient.resolved(id).requireAci().toByteString())
|
||||
.joinedAtRevision(0)
|
||||
.role(Member.Role.DEFAULT)
|
||||
DecryptedMember.newBuilder()
|
||||
.setAciBytes(Recipient.resolved(id).requireAci().toByteString())
|
||||
.setJoinedAtRevision(0)
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build()
|
||||
}
|
||||
|
||||
val decryptedGroupState = DecryptedGroup.Builder()
|
||||
.members(listOf(selfMember) + otherMembers)
|
||||
.revision(0)
|
||||
val decryptedGroupState = DecryptedGroup.newBuilder()
|
||||
.addAllMembers(listOf(selfMember) + otherMembers)
|
||||
.setRevision(0)
|
||||
.build()
|
||||
|
||||
return groupTable.create(groupMasterKey, decryptedGroupState)!!
|
||||
|
||||
@@ -1,288 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import org.junit.Test
|
||||
import org.signal.core.util.forEach
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireNonNullString
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.update
|
||||
import org.thoughtcrime.securesms.crash.CrashConfig
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
|
||||
class LogDatabaseTest {
|
||||
|
||||
private val db: LogDatabase = LogDatabase.getInstance(ApplicationDependencies.getApplication())
|
||||
|
||||
@Test
|
||||
fun crashTable_matchesNamePattern() {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
db.crashes.saveCrash(
|
||||
createdAt = currentTime,
|
||||
name = "TestName",
|
||||
message = "Test Message",
|
||||
stackTrace = "test\nstack\ntrace"
|
||||
)
|
||||
|
||||
val foundMatch = db.crashes.anyMatch(
|
||||
listOf(
|
||||
CrashConfig.CrashPattern(namePattern = "Test")
|
||||
),
|
||||
promptThreshold = currentTime
|
||||
)
|
||||
|
||||
foundMatch assertIs true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun crashTable_matchesMessagePattern() {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
db.crashes.saveCrash(
|
||||
createdAt = currentTime,
|
||||
name = "TestName",
|
||||
message = "Test Message",
|
||||
stackTrace = "test\nstack\ntrace"
|
||||
)
|
||||
|
||||
val foundMatch = db.crashes.anyMatch(
|
||||
listOf(
|
||||
CrashConfig.CrashPattern(messagePattern = "Message")
|
||||
),
|
||||
promptThreshold = currentTime
|
||||
)
|
||||
|
||||
foundMatch assertIs true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun crashTable_matchesStackTracePattern() {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
db.crashes.saveCrash(
|
||||
createdAt = currentTime,
|
||||
name = "TestName",
|
||||
message = "Test Message",
|
||||
stackTrace = "test\nstack\ntrace"
|
||||
)
|
||||
|
||||
val foundMatch = db.crashes.anyMatch(
|
||||
listOf(
|
||||
CrashConfig.CrashPattern(stackTracePattern = "stack")
|
||||
),
|
||||
promptThreshold = currentTime
|
||||
)
|
||||
|
||||
foundMatch assertIs true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun crashTable_matchesNameAndMessagePattern() {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
db.crashes.saveCrash(
|
||||
createdAt = currentTime,
|
||||
name = "TestName",
|
||||
message = "Test Message",
|
||||
stackTrace = "test\nstack\ntrace"
|
||||
)
|
||||
|
||||
val foundMatch = db.crashes.anyMatch(
|
||||
listOf(
|
||||
CrashConfig.CrashPattern(namePattern = "Test", messagePattern = "Message")
|
||||
),
|
||||
promptThreshold = currentTime
|
||||
)
|
||||
|
||||
foundMatch assertIs true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun crashTable_matchesNameAndStackTracePattern() {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
db.crashes.saveCrash(
|
||||
createdAt = currentTime,
|
||||
name = "TestName",
|
||||
message = "Test Message",
|
||||
stackTrace = "test\nstack\ntrace"
|
||||
)
|
||||
|
||||
val foundMatch = db.crashes.anyMatch(
|
||||
listOf(
|
||||
CrashConfig.CrashPattern(namePattern = "Test", stackTracePattern = "stack")
|
||||
),
|
||||
promptThreshold = currentTime
|
||||
)
|
||||
|
||||
foundMatch assertIs true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun crashTable_matchesNameAndMessageAndStackTracePattern() {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
db.crashes.saveCrash(
|
||||
createdAt = currentTime,
|
||||
name = "TestName",
|
||||
message = "Test Message",
|
||||
stackTrace = "test\nstack\ntrace"
|
||||
)
|
||||
|
||||
val foundMatch = db.crashes.anyMatch(
|
||||
listOf(
|
||||
CrashConfig.CrashPattern(namePattern = "Test", messagePattern = "Message", stackTracePattern = "stack")
|
||||
),
|
||||
promptThreshold = currentTime
|
||||
)
|
||||
|
||||
foundMatch assertIs true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun crashTable_doesNotMatchNamePattern() {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
db.crashes.saveCrash(
|
||||
createdAt = currentTime,
|
||||
name = "TestName",
|
||||
message = "Test Message",
|
||||
stackTrace = "test\nstack\ntrace"
|
||||
)
|
||||
|
||||
val foundMatch = db.crashes.anyMatch(
|
||||
listOf(
|
||||
CrashConfig.CrashPattern(namePattern = "Blah")
|
||||
),
|
||||
promptThreshold = currentTime
|
||||
)
|
||||
|
||||
foundMatch assertIs false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun crashTable_matchesNameButNotMessagePattern() {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
db.crashes.saveCrash(
|
||||
createdAt = currentTime,
|
||||
name = "TestName",
|
||||
message = "Test Message",
|
||||
stackTrace = "test\nstack\ntrace"
|
||||
)
|
||||
|
||||
val foundMatch = db.crashes.anyMatch(
|
||||
listOf(
|
||||
CrashConfig.CrashPattern(namePattern = "Test", messagePattern = "Blah")
|
||||
),
|
||||
promptThreshold = currentTime
|
||||
)
|
||||
|
||||
foundMatch assertIs false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun crashTable_matchesNameButNotStackTracePattern() {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
db.crashes.saveCrash(
|
||||
createdAt = currentTime,
|
||||
name = "TestName",
|
||||
message = "Test Message",
|
||||
stackTrace = "test\nstack\ntrace"
|
||||
)
|
||||
|
||||
val foundMatch = db.crashes.anyMatch(
|
||||
listOf(
|
||||
CrashConfig.CrashPattern(namePattern = "Test", stackTracePattern = "Blah")
|
||||
),
|
||||
promptThreshold = currentTime
|
||||
)
|
||||
|
||||
foundMatch assertIs false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun crashTable_matchesNamePatternButPromptedTooRecently() {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
db.crashes.saveCrash(
|
||||
createdAt = currentTime,
|
||||
name = "TestName",
|
||||
message = "Test Message",
|
||||
stackTrace = "test\nstack\ntrace"
|
||||
)
|
||||
|
||||
db.writableDatabase
|
||||
.update(LogDatabase.CrashTable.TABLE_NAME)
|
||||
.values(LogDatabase.CrashTable.LAST_PROMPTED_AT to currentTime)
|
||||
.run()
|
||||
|
||||
val foundMatch = db.crashes.anyMatch(
|
||||
listOf(
|
||||
CrashConfig.CrashPattern(namePattern = "Test")
|
||||
),
|
||||
promptThreshold = currentTime - 100
|
||||
)
|
||||
|
||||
foundMatch assertIs false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun crashTable_noMatches() {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
val foundMatch = db.crashes.anyMatch(
|
||||
listOf(
|
||||
CrashConfig.CrashPattern(namePattern = "Test")
|
||||
),
|
||||
promptThreshold = currentTime - 100
|
||||
)
|
||||
|
||||
foundMatch assertIs false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun crashTable_updatesLastPromptTime() {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
db.crashes.saveCrash(
|
||||
createdAt = currentTime,
|
||||
name = "TestName",
|
||||
message = "Test Message",
|
||||
stackTrace = "test\nstack\ntrace"
|
||||
)
|
||||
|
||||
db.crashes.saveCrash(
|
||||
createdAt = currentTime,
|
||||
name = "XXX",
|
||||
message = "XXX",
|
||||
stackTrace = "XXX"
|
||||
)
|
||||
|
||||
db.crashes.markAsPrompted(
|
||||
listOf(
|
||||
CrashConfig.CrashPattern(namePattern = "Test")
|
||||
),
|
||||
promptedAt = currentTime
|
||||
)
|
||||
|
||||
db.writableDatabase
|
||||
.select(LogDatabase.CrashTable.NAME, LogDatabase.CrashTable.LAST_PROMPTED_AT)
|
||||
.from(LogDatabase.CrashTable.TABLE_NAME)
|
||||
.run()
|
||||
.forEach {
|
||||
if (it.requireNonNullString(LogDatabase.CrashTable.NAME) == "TestName") {
|
||||
it.requireLong(LogDatabase.CrashTable.LAST_PROMPTED_AT) assertIs currentTime
|
||||
} else {
|
||||
it.requireLong(LogDatabase.CrashTable.LAST_PROMPTED_AT) assertIs 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ class MessageTableTest_gifts {
|
||||
val messageId = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge()
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
|
||||
val result = mms.setOutgoingGiftsRevealed(listOf(messageId))
|
||||
@@ -62,7 +62,7 @@ class MessageTableTest_gifts {
|
||||
val messageId = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge()
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
mms.setOutgoingGiftsRevealed(listOf(messageId))
|
||||
|
||||
@@ -76,13 +76,13 @@ class MessageTableTest_gifts {
|
||||
val messageId = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge()
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
|
||||
MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 2,
|
||||
giftBadge = GiftBadge()
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
|
||||
val result = mms.setOutgoingGiftsRevealed(listOf(messageId))
|
||||
@@ -96,13 +96,13 @@ class MessageTableTest_gifts {
|
||||
val messageId = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge()
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
|
||||
val messageId2 = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 2,
|
||||
giftBadge = GiftBadge()
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
|
||||
val result = mms.setOutgoingGiftsRevealed(listOf(messageId, messageId2))
|
||||
@@ -115,13 +115,13 @@ class MessageTableTest_gifts {
|
||||
val messageId = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge()
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
|
||||
val messageId2 = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 2,
|
||||
giftBadge = GiftBadge()
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
|
||||
MmsHelper.insert(
|
||||
@@ -140,13 +140,13 @@ class MessageTableTest_gifts {
|
||||
val messageId = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge()
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
|
||||
val messageId2 = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 2,
|
||||
giftBadge = GiftBadge()
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
|
||||
val messageId3 = MmsHelper.insert(
|
||||
@@ -165,13 +165,13 @@ class MessageTableTest_gifts {
|
||||
MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge()
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
|
||||
MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 2,
|
||||
giftBadge = GiftBadge()
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
|
||||
val messageId3 = MmsHelper.insert(
|
||||
|
||||
@@ -1228,7 +1228,7 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
.use { cursor: Cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val bytes = Base64.decode(cursor.requireNonNullString(MessageTable.BODY))
|
||||
ThreadMergeEvent.ADAPTER.decode(bytes)
|
||||
ThreadMergeEvent.parseFrom(bytes)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -1246,7 +1246,7 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
.use { cursor: Cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val bytes = Base64.decode(cursor.requireNonNullString(MessageTable.BODY))
|
||||
SessionSwitchoverEvent.ADAPTER.decode(bytes)
|
||||
SessionSwitchoverEvent.parseFrom(bytes)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
@@ -22,9 +22,8 @@ import org.thoughtcrime.securesms.testing.MessageContentFuzzer
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import org.thoughtcrime.securesms.util.MessageTableTestUtils
|
||||
import org.whispersystems.signalservice.internal.push.Content
|
||||
import org.whispersystems.signalservice.internal.push.EditMessage
|
||||
import org.whispersystems.signalservice.internal.push.SyncMessage
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.EditMessage
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@@ -68,17 +67,16 @@ class EditMessageSyncProcessorTest {
|
||||
|
||||
val content = MessageContentFuzzer.fuzzTextMessage()
|
||||
val metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, toRecipient.id)
|
||||
val syncContent = Content.Builder().syncMessage(
|
||||
SyncMessage.Builder().sent(
|
||||
SyncMessage.Sent.Builder()
|
||||
.destinationServiceId(metadata.destinationServiceId.toString())
|
||||
.timestamp(originalTimestamp)
|
||||
.expirationStartTimestamp(originalTimestamp)
|
||||
.message(content.dataMessage)
|
||||
.build()
|
||||
).build()
|
||||
val syncContent = SignalServiceProtos.Content.newBuilder().setSyncMessage(
|
||||
SignalServiceProtos.SyncMessage.newBuilder().setSent(
|
||||
SignalServiceProtos.SyncMessage.Sent.newBuilder()
|
||||
.setDestinationServiceId(metadata.destinationServiceId.toString())
|
||||
.setTimestamp(originalTimestamp)
|
||||
.setExpirationStartTimestamp(originalTimestamp)
|
||||
.setMessage(content.dataMessage)
|
||||
)
|
||||
).build()
|
||||
SignalDatabase.recipients.setExpireMessages(toRecipient.id, content.dataMessage?.expireTimer ?: 0)
|
||||
SignalDatabase.recipients.setExpireMessages(toRecipient.id, content.dataMessage.expireTimer)
|
||||
val syncTextMessage = TestMessage(
|
||||
envelope = MessageContentFuzzer.envelope(originalTimestamp),
|
||||
content = syncContent,
|
||||
@@ -88,20 +86,18 @@ class EditMessageSyncProcessorTest {
|
||||
|
||||
val editTimestamp = originalTimestamp + 200
|
||||
val editedContent = MessageContentFuzzer.fuzzTextMessage()
|
||||
val editSyncContent = Content.Builder().syncMessage(
|
||||
SyncMessage.Builder().sent(
|
||||
SyncMessage.Sent.Builder()
|
||||
.destinationServiceId(metadata.destinationServiceId.toString())
|
||||
.timestamp(editTimestamp)
|
||||
.expirationStartTimestamp(editTimestamp)
|
||||
.editMessage(
|
||||
EditMessage.Builder()
|
||||
.dataMessage(editedContent.dataMessage)
|
||||
.targetSentTimestamp(originalTimestamp)
|
||||
.build()
|
||||
val editSyncContent = SignalServiceProtos.Content.newBuilder().setSyncMessage(
|
||||
SignalServiceProtos.SyncMessage.newBuilder().setSent(
|
||||
SignalServiceProtos.SyncMessage.Sent.newBuilder()
|
||||
.setDestinationServiceId(metadata.destinationServiceId.toString())
|
||||
.setTimestamp(editTimestamp)
|
||||
.setExpirationStartTimestamp(editTimestamp)
|
||||
.setEditMessage(
|
||||
EditMessage.newBuilder()
|
||||
.setDataMessage(editedContent.dataMessage)
|
||||
.setTargetSentTimestamp(originalTimestamp)
|
||||
)
|
||||
.build()
|
||||
).build()
|
||||
)
|
||||
).build()
|
||||
|
||||
val syncEditMessage = TestMessage(
|
||||
@@ -113,38 +109,38 @@ class EditMessageSyncProcessorTest {
|
||||
|
||||
testResult.runSync(listOf(syncTextMessage, syncEditMessage))
|
||||
|
||||
SignalDatabase.recipients.setExpireMessages(toRecipient.id, (content.dataMessage?.expireTimer ?: 0) / 1000)
|
||||
SignalDatabase.recipients.setExpireMessages(toRecipient.id, content.dataMessage.expireTimer / 1000)
|
||||
val originalTextMessage = OutgoingMessage(
|
||||
threadRecipient = toRecipient,
|
||||
sentTimeMillis = originalTimestamp,
|
||||
body = content.dataMessage?.body ?: "",
|
||||
expiresIn = content.dataMessage?.expireTimer?.seconds?.inWholeMilliseconds ?: 0,
|
||||
body = content.dataMessage.body,
|
||||
expiresIn = content.dataMessage.expireTimer.seconds.inWholeMilliseconds,
|
||||
isUrgent = true,
|
||||
isSecure = true,
|
||||
bodyRanges = content.dataMessage?.bodyRanges.toBodyRangeList()
|
||||
bodyRanges = content.dataMessage.bodyRangesList.toBodyRangeList()
|
||||
)
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(toRecipient)
|
||||
val originalMessageId = SignalDatabase.messages.insertMessageOutbox(originalTextMessage, threadId, false, null)
|
||||
SignalDatabase.messages.markAsSent(originalMessageId, true)
|
||||
if ((content.dataMessage?.expireTimer ?: 0) > 0) {
|
||||
if (content.dataMessage.expireTimer > 0) {
|
||||
SignalDatabase.messages.markExpireStarted(originalMessageId, originalTimestamp)
|
||||
}
|
||||
|
||||
val editMessage = OutgoingMessage(
|
||||
threadRecipient = toRecipient,
|
||||
sentTimeMillis = editTimestamp,
|
||||
body = editedContent.dataMessage?.body ?: "",
|
||||
expiresIn = content.dataMessage?.expireTimer?.seconds?.inWholeMilliseconds ?: 0,
|
||||
body = editedContent.dataMessage.body,
|
||||
expiresIn = content.dataMessage.expireTimer.seconds.inWholeMilliseconds,
|
||||
isUrgent = true,
|
||||
isSecure = true,
|
||||
bodyRanges = editedContent.dataMessage?.bodyRanges.toBodyRangeList(),
|
||||
bodyRanges = editedContent.dataMessage.bodyRangesList.toBodyRangeList(),
|
||||
messageToEdit = originalMessageId
|
||||
)
|
||||
|
||||
val editMessageId = SignalDatabase.messages.insertMessageOutbox(editMessage, threadId, false, null)
|
||||
SignalDatabase.messages.markAsSent(editMessageId, true)
|
||||
|
||||
if ((content.dataMessage?.expireTimer ?: 0) > 0) {
|
||||
if (content.dataMessage.expireTimer > 0) {
|
||||
SignalDatabase.messages.markExpireStarted(editMessageId, originalTimestamp)
|
||||
}
|
||||
testResult.collectLocal()
|
||||
@@ -171,7 +167,7 @@ class EditMessageSyncProcessorTest {
|
||||
|
||||
fun runSync(messages: List<TestMessage>) {
|
||||
messages.forEach { (envelope, content, metadata, serverDeliveredTimestamp) ->
|
||||
if (content.syncMessage != null) {
|
||||
if (content.hasSyncMessage()) {
|
||||
processorV2.process(
|
||||
envelope,
|
||||
content,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.toProtoByteString
|
||||
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith
|
||||
import org.thoughtcrime.securesms.testing.GroupTestingUtils
|
||||
import org.thoughtcrime.securesms.testing.GroupTestingUtils.asMember
|
||||
@@ -15,8 +15,8 @@ import org.thoughtcrime.securesms.testing.MessageContentFuzzer
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import org.thoughtcrime.securesms.util.MessageTableTestUtils
|
||||
import org.whispersystems.signalservice.internal.push.DataMessage
|
||||
import org.whispersystems.signalservice.internal.push.GroupContextV2
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@@ -41,9 +41,9 @@ class MessageContentProcessor__recipientStatusTest {
|
||||
@Test
|
||||
fun syncGroupSentTextMessageWithRecipientUpdateFollowup() {
|
||||
val (groupId, masterKey, groupRecipientId) = GroupTestingUtils.insertGroup(revision = 0, harness.self.asMember(), harness.others[0].asMember(), harness.others[1].asMember())
|
||||
val groupContextV2 = GroupContextV2.Builder().revision(0).masterKey(masterKey.serialize().toByteString()).build()
|
||||
val groupContextV2 = GroupContextV2.newBuilder().setRevision(0).setMasterKey(masterKey.serialize().toProtoByteString()).build()
|
||||
|
||||
val initialTextMessage = DataMessage.Builder().buildWith {
|
||||
val initialTextMessage = DataMessage.newBuilder().buildWith {
|
||||
body = MessageContentFuzzer.string()
|
||||
groupV2 = groupContextV2
|
||||
timestamp = envelopeTimestamp
|
||||
|
||||
@@ -6,6 +6,7 @@ import io.mockk.mockkObject
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
@@ -25,7 +26,7 @@ import org.thoughtcrime.securesms.testing.Entry
|
||||
import org.thoughtcrime.securesms.testing.FakeClientHelpers
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.awaitFor
|
||||
import org.whispersystems.signalservice.internal.push.Envelope
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
|
||||
import org.whispersystems.signalservice.internal.websocket.WebSocketMessage
|
||||
import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage
|
||||
import java.util.regex.Pattern
|
||||
@@ -92,7 +93,7 @@ class MessageProcessingPerformanceTest {
|
||||
val messageCount = 100
|
||||
val envelopes = generateInboundEnvelopes(bobClient, messageCount)
|
||||
val firstTimestamp = envelopes.first().timestamp
|
||||
val lastTimestamp = envelopes.last().timestamp ?: 0
|
||||
val lastTimestamp = envelopes.last().timestamp
|
||||
|
||||
// Inject the envelopes into the websocket
|
||||
Thread {
|
||||
@@ -189,7 +190,7 @@ class MessageProcessingPerformanceTest {
|
||||
path = "/api/v1/message",
|
||||
id = Random(System.currentTimeMillis()).nextLong(),
|
||||
headers = listOf("X-Signal-Timestamp: ${this.timestamp}"),
|
||||
body = this.encodeByteString()
|
||||
body = this.toByteArray().toByteString()
|
||||
)
|
||||
).encodeByteString()
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
||||
import org.whispersystems.signalservice.internal.push.Content
|
||||
import org.whispersystems.signalservice.internal.push.Envelope
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
|
||||
data class TestMessage(
|
||||
val envelope: Envelope,
|
||||
val content: Content,
|
||||
val envelope: SignalServiceProtos.Envelope,
|
||||
val content: SignalServiceProtos.Content,
|
||||
val metadata: EnvelopeMetadata,
|
||||
val serverDeliveredTimestamp: Long
|
||||
)
|
||||
|
||||
@@ -5,8 +5,7 @@ import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.testing.LogPredicate
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
||||
import org.whispersystems.signalservice.internal.push.Content
|
||||
import org.whispersystems.signalservice.internal.push.Envelope
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
|
||||
class TimingMessageContentProcessor(context: Context) : MessageContentProcessor(context) {
|
||||
companion object {
|
||||
@@ -20,9 +19,9 @@ class TimingMessageContentProcessor(context: Context) : MessageContentProcessor(
|
||||
fun endTag(timestamp: Long) = "$timestamp end"
|
||||
}
|
||||
|
||||
override fun process(envelope: Envelope, content: Content, metadata: EnvelopeMetadata, serverDeliveredTimestamp: Long, processingEarlyContent: Boolean, localMetric: SignalLocalMetrics.MessageReceive?) {
|
||||
Log.d(TAG, startTag(envelope.timestamp!!))
|
||||
override fun process(envelope: SignalServiceProtos.Envelope, content: SignalServiceProtos.Content, metadata: EnvelopeMetadata, serverDeliveredTimestamp: Long, processingEarlyContent: Boolean, localMetric: SignalLocalMetrics.MessageReceive?) {
|
||||
Log.d(TAG, startTag(envelope.timestamp))
|
||||
super.process(envelope, content, metadata, serverDeliveredTimestamp, processingEarlyContent, localMetric)
|
||||
Log.d(TAG, endTag(envelope.timestamp!!))
|
||||
Log.d(TAG, endTag(envelope.timestamp))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.testing.FakeClientHelpers.toEnvelope
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.internal.push.Envelope
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
|
||||
|
||||
/**
|
||||
* Welcome to Alice's Client.
|
||||
|
||||
@@ -31,10 +31,11 @@ import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
|
||||
import org.whispersystems.signalservice.api.push.DistributionId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.internal.push.Envelope
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.UnsupportedOperationException
|
||||
|
||||
/**
|
||||
* Welcome to Bob's Client.
|
||||
@@ -60,7 +61,7 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
|
||||
}
|
||||
|
||||
/** Inspired by SignalServiceMessageSender#getEncryptedMessage */
|
||||
fun encrypt(now: Long): Envelope {
|
||||
fun encrypt(now: Long): SignalServiceProtos.Envelope {
|
||||
val envelopeContent = FakeClientHelpers.encryptedTextMessage(now)
|
||||
|
||||
val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, null)
|
||||
@@ -71,10 +72,10 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
|
||||
}
|
||||
|
||||
return cipher.encrypt(getAliceProtocolAddress(), getAliceUnidentifiedAccess(), envelopeContent)
|
||||
.toEnvelope(envelopeContent.content.get().dataMessage!!.timestamp!!, getAliceServiceId())
|
||||
.toEnvelope(envelopeContent.content.get().dataMessage.timestamp, getAliceServiceId())
|
||||
}
|
||||
|
||||
fun decrypt(envelope: Envelope, serverDeliveredTimestamp: Long) {
|
||||
fun decrypt(envelope: SignalServiceProtos.Envelope, serverDeliveredTimestamp: Long) {
|
||||
val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, UnidentifiedAccessUtil.getCertificateValidator())
|
||||
cipher.decrypt(envelope, serverDeliveredTimestamp)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.libsignal.internal.Native
|
||||
import org.signal.libsignal.internal.NativeHandleGuard
|
||||
import org.signal.libsignal.metadata.certificate.CertificateValidator
|
||||
@@ -10,16 +9,15 @@ import org.signal.libsignal.protocol.ecc.Curve
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith
|
||||
import org.thoughtcrime.securesms.database.model.toProtoByteString
|
||||
import org.whispersystems.signalservice.api.crypto.ContentHint
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeContent
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.internal.push.Content
|
||||
import org.whispersystems.signalservice.internal.push.DataMessage
|
||||
import org.whispersystems.signalservice.internal.push.Envelope
|
||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
|
||||
import org.whispersystems.util.Base64
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
@@ -54,9 +52,9 @@ object FakeClientHelpers {
|
||||
}
|
||||
|
||||
fun encryptedTextMessage(now: Long, message: String = "Test body message"): EnvelopeContent {
|
||||
val content = Content.Builder().apply {
|
||||
dataMessage(
|
||||
DataMessage.Builder().buildWith {
|
||||
val content = SignalServiceProtos.Content.newBuilder().apply {
|
||||
setDataMessage(
|
||||
SignalServiceProtos.DataMessage.newBuilder().apply {
|
||||
body = message
|
||||
timestamp = now
|
||||
}
|
||||
@@ -66,16 +64,16 @@ object FakeClientHelpers {
|
||||
}
|
||||
|
||||
fun OutgoingPushMessage.toEnvelope(timestamp: Long, destination: ServiceId): Envelope {
|
||||
return Envelope.Builder()
|
||||
.type(Envelope.Type.fromValue(this.type))
|
||||
.sourceDevice(1)
|
||||
.timestamp(timestamp)
|
||||
.serverTimestamp(timestamp + 1)
|
||||
.destinationServiceId(destination.toString())
|
||||
.serverGuid(UUID.randomUUID().toString())
|
||||
.content(Base64.decode(this.content).toByteString())
|
||||
.urgent(true)
|
||||
.story(false)
|
||||
return Envelope.newBuilder()
|
||||
.setType(Envelope.Type.valueOf(this.type))
|
||||
.setSourceDevice(1)
|
||||
.setTimestamp(timestamp)
|
||||
.setServerTimestamp(timestamp + 1)
|
||||
.setDestinationServiceId(destination.toString())
|
||||
.setServerGuid(UUID.randomUUID().toString())
|
||||
.setContent(Base64.decode(this.content).toProtoByteString())
|
||||
.setUrgent(true)
|
||||
.setStory(false)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,19 +16,19 @@ import kotlin.random.Random
|
||||
*/
|
||||
object GroupTestingUtils {
|
||||
fun member(aci: ACI, revision: Int = 0, role: Member.Role = Member.Role.ADMINISTRATOR): DecryptedMember {
|
||||
return DecryptedMember.Builder()
|
||||
.aciBytes(aci.toByteString())
|
||||
.joinedAtRevision(revision)
|
||||
.role(role)
|
||||
return DecryptedMember.newBuilder()
|
||||
.setAciBytes(aci.toByteString())
|
||||
.setJoinedAtRevision(revision)
|
||||
.setRole(role)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun insertGroup(revision: Int = 0, vararg members: DecryptedMember): TestGroupInfo {
|
||||
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
|
||||
val decryptedGroupState = DecryptedGroup.Builder()
|
||||
.members(members.toList())
|
||||
.revision(revision)
|
||||
.title(MessageContentFuzzer.string())
|
||||
val decryptedGroupState = DecryptedGroup.newBuilder()
|
||||
.addAllMembers(members.toList())
|
||||
.setRevision(revision)
|
||||
.setTitle(MessageContentFuzzer.string())
|
||||
.build()
|
||||
|
||||
val groupId = SignalDatabase.groups.create(groupMasterKey, decryptedGroupState)!!
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import com.google.protobuf.ByteString
|
||||
import org.thoughtcrime.securesms.database.model.toProtoByteString
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith
|
||||
import org.thoughtcrime.securesms.messages.TestMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentPointer
|
||||
import org.whispersystems.signalservice.internal.push.BodyRange
|
||||
import org.whispersystems.signalservice.internal.push.Content
|
||||
import org.whispersystems.signalservice.internal.push.DataMessage
|
||||
import org.whispersystems.signalservice.internal.push.Envelope
|
||||
import org.whispersystems.signalservice.internal.push.GroupContextV2
|
||||
import org.whispersystems.signalservice.internal.push.SyncMessage
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.AttachmentPointer
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage
|
||||
import java.util.UUID
|
||||
import kotlin.random.Random
|
||||
import kotlin.random.nextInt
|
||||
@@ -34,10 +35,10 @@ object MessageContentFuzzer {
|
||||
* Create an [Envelope].
|
||||
*/
|
||||
fun envelope(timestamp: Long): Envelope {
|
||||
return Envelope.Builder()
|
||||
.timestamp(timestamp)
|
||||
.serverTimestamp(timestamp + 5)
|
||||
.serverGuid(UUID.randomUUID().toString())
|
||||
return Envelope.newBuilder()
|
||||
.setTimestamp(timestamp)
|
||||
.setServerTimestamp(timestamp + 5)
|
||||
.setServerGuidBytes(UuidUtil.toByteString(UUID.randomUUID()))
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -61,22 +62,20 @@ object MessageContentFuzzer {
|
||||
* - Bold style body ranges
|
||||
*/
|
||||
fun fuzzTextMessage(groupContextV2: GroupContextV2? = null): Content {
|
||||
return Content.Builder()
|
||||
.dataMessage(
|
||||
DataMessage.Builder().buildWith {
|
||||
return Content.newBuilder()
|
||||
.setDataMessage(
|
||||
DataMessage.newBuilder().buildWith {
|
||||
body = string()
|
||||
if (random.nextBoolean()) {
|
||||
expireTimer = random.nextInt(0..28.days.inWholeSeconds.toInt())
|
||||
}
|
||||
if (random.nextBoolean()) {
|
||||
bodyRanges(
|
||||
listOf(
|
||||
BodyRange.Builder().buildWith {
|
||||
start = 0
|
||||
length = 1
|
||||
style = BodyRange.Style.BOLD
|
||||
}
|
||||
)
|
||||
addBodyRanges(
|
||||
SignalServiceProtos.BodyRange.newBuilder().buildWith {
|
||||
start = 0
|
||||
length = 1
|
||||
style = SignalServiceProtos.BodyRange.Style.BOLD
|
||||
}
|
||||
)
|
||||
}
|
||||
if (groupContextV2 != null) {
|
||||
@@ -96,16 +95,16 @@ object MessageContentFuzzer {
|
||||
recipientUpdate: Boolean = false
|
||||
): Content {
|
||||
return Content
|
||||
.Builder()
|
||||
.syncMessage(
|
||||
SyncMessage.Builder().buildWith {
|
||||
sent = SyncMessage.Sent.Builder().buildWith {
|
||||
.newBuilder()
|
||||
.setSyncMessage(
|
||||
SyncMessage.newBuilder().buildWith {
|
||||
sent = SyncMessage.Sent.newBuilder().buildWith {
|
||||
timestamp = textMessage.timestamp
|
||||
message = textMessage
|
||||
isRecipientUpdate = recipientUpdate
|
||||
unidentifiedStatus(
|
||||
addAllUnidentifiedStatus(
|
||||
deliveredTo.map {
|
||||
SyncMessage.Sent.UnidentifiedDeliveryStatus.Builder().buildWith {
|
||||
SyncMessage.Sent.UnidentifiedDeliveryStatus.newBuilder().buildWith {
|
||||
destinationServiceId = Recipient.resolved(it).requireServiceId().toString()
|
||||
unidentified = true
|
||||
}
|
||||
@@ -124,9 +123,9 @@ object MessageContentFuzzer {
|
||||
* - A message with 0-2 attachment pointers and may contain a text body
|
||||
*/
|
||||
fun fuzzMediaMessageWithBody(quoteAble: List<TestMessage> = emptyList()): Content {
|
||||
return Content.Builder()
|
||||
.dataMessage(
|
||||
DataMessage.Builder().buildWith {
|
||||
return Content.newBuilder()
|
||||
.setDataMessage(
|
||||
DataMessage.newBuilder().buildWith {
|
||||
if (random.nextBoolean()) {
|
||||
body = string()
|
||||
}
|
||||
@@ -134,28 +133,28 @@ object MessageContentFuzzer {
|
||||
if (random.nextBoolean() && quoteAble.isNotEmpty()) {
|
||||
body = string()
|
||||
val quoted = quoteAble.random(random)
|
||||
quote = DataMessage.Quote.Builder().buildWith {
|
||||
quote = DataMessage.Quote.newBuilder().buildWith {
|
||||
id = quoted.envelope.timestamp
|
||||
authorAci = quoted.metadata.sourceServiceId.toString()
|
||||
text = quoted.content.dataMessage?.body
|
||||
attachments(quoted.content.dataMessage?.attachments ?: emptyList())
|
||||
bodyRanges(quoted.content.dataMessage?.bodyRanges ?: emptyList())
|
||||
text = quoted.content.dataMessage.body
|
||||
addAllAttachments(quoted.content.dataMessage.attachmentsList)
|
||||
addAllBodyRanges(quoted.content.dataMessage.bodyRangesList)
|
||||
type = DataMessage.Quote.Type.NORMAL
|
||||
}
|
||||
}
|
||||
|
||||
if (random.nextFloat() < 0.1 && quoteAble.isNotEmpty()) {
|
||||
val quoted = quoteAble.random(random)
|
||||
quote = DataMessage.Quote.Builder().buildWith {
|
||||
id = random.nextLong(quoted.envelope.timestamp!! - 1000000, quoted.envelope.timestamp!!)
|
||||
quote = DataMessage.Quote.newBuilder().buildWith {
|
||||
id = random.nextLong(quoted.envelope.timestamp - 1000000, quoted.envelope.timestamp)
|
||||
authorAci = quoted.metadata.sourceServiceId.toString()
|
||||
text = quoted.content.dataMessage?.body
|
||||
text = quoted.content.dataMessage.body
|
||||
}
|
||||
}
|
||||
|
||||
if (random.nextFloat() < 0.25) {
|
||||
val total = random.nextInt(1, 2)
|
||||
attachments((0..total).map { attachmentPointer() })
|
||||
(0..total).forEach { _ -> addAttachments(attachmentPointer()) }
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -167,12 +166,12 @@ object MessageContentFuzzer {
|
||||
* - A reaction to a prior message
|
||||
*/
|
||||
fun fuzzMediaMessageNoContent(previousMessages: List<TestMessage> = emptyList()): Content {
|
||||
return Content.Builder()
|
||||
.dataMessage(
|
||||
DataMessage.Builder().buildWith {
|
||||
return Content.newBuilder()
|
||||
.setDataMessage(
|
||||
DataMessage.newBuilder().buildWith {
|
||||
if (random.nextFloat() < 0.25) {
|
||||
val reactTo = previousMessages.random(random)
|
||||
reaction = DataMessage.Reaction.Builder().buildWith {
|
||||
reaction = DataMessage.Reaction.newBuilder().buildWith {
|
||||
emoji = emojis.random(random)
|
||||
remove = false
|
||||
targetAuthorAci = reactTo.metadata.sourceServiceId.toString()
|
||||
@@ -188,15 +187,15 @@ object MessageContentFuzzer {
|
||||
* - A sticker
|
||||
*/
|
||||
fun fuzzMediaMessageNoText(previousMessages: List<TestMessage> = emptyList()): Content {
|
||||
return Content.Builder()
|
||||
.dataMessage(
|
||||
DataMessage.Builder().buildWith {
|
||||
return Content.newBuilder()
|
||||
.setDataMessage(
|
||||
DataMessage.newBuilder().buildWith {
|
||||
if (random.nextFloat() < 0.9) {
|
||||
sticker = DataMessage.Sticker.Builder().buildWith {
|
||||
sticker = DataMessage.Sticker.newBuilder().buildWith {
|
||||
packId = byteString(length = 24)
|
||||
packKey = byteString(length = 128)
|
||||
stickerId = random.nextInt()
|
||||
data_ = attachmentPointer()
|
||||
data = attachmentPointer()
|
||||
emoji = emojis.random(random)
|
||||
}
|
||||
}
|
||||
@@ -224,14 +223,14 @@ object MessageContentFuzzer {
|
||||
* Generate a random [ByteString].
|
||||
*/
|
||||
fun byteString(length: Int = 512): ByteString {
|
||||
return random.nextBytes(length).toByteString()
|
||||
return random.nextBytes(length).toProtoByteString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random [AttachmentPointer].
|
||||
*/
|
||||
fun attachmentPointer(): AttachmentPointer {
|
||||
return AttachmentPointer.Builder().run {
|
||||
return AttachmentPointer.newBuilder().run {
|
||||
cdnKey = string()
|
||||
contentType = mediaTypes.random(random)
|
||||
key = byteString()
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
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
|
||||
import org.whispersystems.signalservice.internal.serialize.protos.AddressProto
|
||||
import org.whispersystems.signalservice.internal.serialize.protos.MetadataProto
|
||||
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto
|
||||
import java.util.UUID
|
||||
import kotlin.random.Random
|
||||
|
||||
class TestProtos private constructor() {
|
||||
fun address(
|
||||
uuid: UUID = UUID.randomUUID()
|
||||
): AddressProto.Builder {
|
||||
return AddressProto.newBuilder()
|
||||
.setUuid(ACI.from(uuid).toByteString())
|
||||
}
|
||||
|
||||
fun metadata(
|
||||
address: AddressProto = address().build()
|
||||
): MetadataProto.Builder {
|
||||
return MetadataProto.newBuilder()
|
||||
.setAddress(address)
|
||||
}
|
||||
|
||||
fun groupContextV2(
|
||||
revision: Int = 0,
|
||||
masterKeyBytes: ByteArray = Random.Default.nextBytes(GroupMasterKey.SIZE)
|
||||
): GroupContextV2.Builder {
|
||||
return GroupContextV2.newBuilder()
|
||||
.setRevision(revision)
|
||||
.setMasterKey(ByteString.copyFrom(masterKeyBytes))
|
||||
}
|
||||
|
||||
fun storyContext(
|
||||
sentTimestamp: Long = Random.nextLong(),
|
||||
authorUuid: String = UUID.randomUUID().toString()
|
||||
): DataMessage.StoryContext.Builder {
|
||||
return DataMessage.StoryContext.newBuilder()
|
||||
.setAuthorAci(authorUuid)
|
||||
.setSentTimestamp(sentTimestamp)
|
||||
}
|
||||
|
||||
fun dataMessage(): DataMessage.Builder {
|
||||
return DataMessage.newBuilder()
|
||||
}
|
||||
|
||||
fun content(): SignalServiceProtos.Content.Builder {
|
||||
return SignalServiceProtos.Content.newBuilder()
|
||||
}
|
||||
|
||||
fun serviceContent(
|
||||
localAddress: AddressProto = address().build(),
|
||||
metadata: MetadataProto = metadata().build()
|
||||
): SignalServiceContentProto.Builder {
|
||||
return SignalServiceContentProto.newBuilder()
|
||||
.setLocalAddress(localAddress)
|
||||
.setMetadata(metadata)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun <T> build(buildFn: TestProtos.() -> T): T {
|
||||
return TestProtos().buildFn()
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -302,8 +302,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
|
||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||
Log.blockUntilAllWritesFinished();
|
||||
LogDatabase.getInstance(this).logs().trimToSize();
|
||||
LogDatabase.getInstance(this).crashes().trimToSize();
|
||||
LogDatabase.getInstance(this).trimToSize();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import org.thoughtcrime.securesms.conversationlist.RelinkDevicesReminderBottomSh
|
||||
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceExitActivity;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor;
|
||||
import org.thoughtcrime.securesms.notifications.VitalsViewModel;
|
||||
import org.thoughtcrime.securesms.notifications.SlowNotificationsViewModel;
|
||||
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabRepository;
|
||||
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
@@ -45,7 +45,7 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
|
||||
private VoiceNoteMediaController mediaController;
|
||||
private ConversationListTabsViewModel conversationListTabsViewModel;
|
||||
private VitalsViewModel vitalsViewModel;
|
||||
private SlowNotificationsViewModel slowNotificationsViewModel;
|
||||
|
||||
private final LifecycleDisposable lifecycleDisposable = new LifecycleDisposable();
|
||||
|
||||
@@ -99,27 +99,25 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
conversationListTabsViewModel = new ViewModelProvider(this, factory).get(ConversationListTabsViewModel.class);
|
||||
updateTabVisibility();
|
||||
|
||||
vitalsViewModel = new ViewModelProvider(this).get(VitalsViewModel.class);
|
||||
slowNotificationsViewModel = new ViewModelProvider(this).get(SlowNotificationsViewModel.class);
|
||||
|
||||
lifecycleDisposable.add(
|
||||
vitalsViewModel
|
||||
.getVitalsState()
|
||||
.subscribe(this::presentVitalsState)
|
||||
slowNotificationsViewModel
|
||||
.getSlowNotificationState()
|
||||
.subscribe(this::presentSlowNotificationState)
|
||||
);
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
private void presentVitalsState(VitalsViewModel.State state) {
|
||||
switch (state) {
|
||||
private void presentSlowNotificationState(SlowNotificationsViewModel.State slowNotificationState) {
|
||||
switch (slowNotificationState) {
|
||||
case NONE:
|
||||
break;
|
||||
case PROMPT_BATTERY_SAVER_DIALOG:
|
||||
PromptBatterySaverDialogFragment.show(getSupportFragmentManager());
|
||||
break;
|
||||
case PROMPT_DEBUGLOGS_FOR_NOTIFICATIONS:
|
||||
DebugLogsPromptDialogFragment.show(this, getSupportFragmentManager(), DebugLogsPromptDialogFragment.Purpose.NOTIFICATIONS);
|
||||
case PROMPT_DEBUGLOGS_FOR_CRASH:
|
||||
DebugLogsPromptDialogFragment.show(this, getSupportFragmentManager(), DebugLogsPromptDialogFragment.Purpose.CRASH);
|
||||
case PROMPT_DEBUGLOGS:
|
||||
DebugLogsPromptDialogFragment.show(this, getSupportFragmentManager());
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -170,7 +168,7 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
|
||||
updateTabVisibility();
|
||||
|
||||
vitalsViewModel.checkSlowNotificationHeuristics();
|
||||
slowNotificationsViewModel.checkSlowNotificationHeuristics();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -73,7 +73,6 @@ import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcControls;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WifiToCellularPopupWindow;
|
||||
import org.thoughtcrime.securesms.components.webrtc.participantslist.CallParticipantsListDialog;
|
||||
import org.thoughtcrime.securesms.components.webrtc.requests.CallLinkIncomingRequestSheet;
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
@@ -1089,11 +1088,6 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
public void onLaunchPendingRequestsSheet() {
|
||||
new PendingParticipantsBottomSheet().show(getSupportFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLaunchRecipientSheet(@NonNull Recipient pendingRecipient) {
|
||||
CallLinkIncomingRequestSheet.show(getSupportFragmentManager(), pendingRecipient.getId());
|
||||
}
|
||||
}
|
||||
|
||||
private class WindowLayoutInfoConsumer implements Consumer<WindowLayoutInfo> {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package org.thoughtcrime.securesms.absbackup.backupables
|
||||
|
||||
import com.google.protobuf.InvalidProtocolBufferException
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.absbackup.AndroidBackupItem
|
||||
import org.thoughtcrime.securesms.absbackup.protos.SvrAuthToken
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* This backs up the not-secret KBS Auth tokens, which can be combined with a PIN to prove ownership of a phone number in order to complete the registration process.
|
||||
@@ -30,7 +30,7 @@ object SvrAuthTokens : AndroidBackupItem {
|
||||
val proto = SvrAuthToken.ADAPTER.decode(data)
|
||||
|
||||
SignalStore.svr().putAuthTokenList(proto.tokens)
|
||||
} catch (e: IOException) {
|
||||
} catch (e: InvalidProtocolBufferException) {
|
||||
Log.w(TAG, "Cannot restore KbsAuthToken from backup service.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,11 +172,7 @@ public abstract class Attachment {
|
||||
|
||||
@Nullable
|
||||
public byte[] getIncrementalDigest() {
|
||||
if (incrementalDigest != null && incrementalDigest.length > 0) {
|
||||
return incrementalDigest;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return incrementalDigest;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
@@ -13,7 +13,7 @@ import org.whispersystems.signalservice.api.InvalidMessageStructureException;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.util.AttachmentPointerUtil;
|
||||
import org.whispersystems.signalservice.internal.push.DataMessage;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@@ -152,18 +152,18 @@ public class PointerAttachment extends Attachment {
|
||||
null));
|
||||
}
|
||||
|
||||
public static Optional<Attachment> forPointer(DataMessage.Quote.QuotedAttachment quotedAttachment) {
|
||||
public static Optional<Attachment> forPointer(SignalServiceProtos.DataMessage.Quote.QuotedAttachment quotedAttachment) {
|
||||
SignalServiceAttachment thumbnail;
|
||||
try {
|
||||
thumbnail = quotedAttachment.thumbnail != null ? AttachmentPointerUtil.createSignalAttachmentPointer(quotedAttachment.thumbnail) : null;
|
||||
thumbnail = quotedAttachment.hasThumbnail() ? AttachmentPointerUtil.createSignalAttachmentPointer(quotedAttachment.getThumbnail()) : null;
|
||||
} catch (InvalidMessageStructureException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(new PointerAttachment(quotedAttachment.contentType,
|
||||
return Optional.of(new PointerAttachment(quotedAttachment.getContentType(),
|
||||
AttachmentTable.TRANSFER_PROGRESS_PENDING,
|
||||
thumbnail != null ? thumbnail.asPointer().getSize().orElse(0) : 0,
|
||||
quotedAttachment.fileName,
|
||||
quotedAttachment.getFileName(),
|
||||
thumbnail != null ? thumbnail.asPointer().getCdnNumber() : 0,
|
||||
thumbnail != null ? thumbnail.asPointer().getRemoteId().toString() : "0",
|
||||
thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeBytes(thumbnail.asPointer().getKey()) : null,
|
||||
|
||||
@@ -2,19 +2,19 @@ package org.thoughtcrime.securesms.audio;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
public class AudioFileInfo {
|
||||
private final long durationUs;
|
||||
private final byte[] waveFormBytes;
|
||||
private final float[] waveForm;
|
||||
|
||||
public static @NonNull AudioFileInfo fromDatabaseProtobuf(@NonNull AudioWaveFormData audioWaveForm) {
|
||||
return new AudioFileInfo(audioWaveForm.durationUs, audioWaveForm.waveForm.toByteArray());
|
||||
return new AudioFileInfo(audioWaveForm.getDurationUs(), audioWaveForm.getWaveForm().toByteArray());
|
||||
}
|
||||
|
||||
AudioFileInfo(long durationUs, byte[] waveFormBytes) {
|
||||
@@ -37,9 +37,9 @@ public class AudioFileInfo {
|
||||
}
|
||||
|
||||
public @NonNull AudioWaveFormData toDatabaseProtobuf() {
|
||||
return new AudioWaveFormData.Builder()
|
||||
.durationUs(durationUs)
|
||||
.waveForm(ByteString.of(waveFormBytes))
|
||||
.build();
|
||||
return AudioWaveFormData.newBuilder()
|
||||
.setDurationUs(durationUs)
|
||||
.setWaveForm(ByteString.copyFrom(waveFormBytes))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -22,13 +22,13 @@ public final class AudioHash {
|
||||
}
|
||||
|
||||
public AudioHash(@NonNull AudioWaveFormData audioWaveForm) {
|
||||
this(Base64.encodeBytes(audioWaveForm.encode()), audioWaveForm);
|
||||
this(Base64.encodeBytes(audioWaveForm.toByteArray()), audioWaveForm);
|
||||
}
|
||||
|
||||
public static @Nullable AudioHash parseOrNull(@Nullable String hash) {
|
||||
if (hash == null) return null;
|
||||
try {
|
||||
return new AudioHash(hash, AudioWaveFormData.ADAPTER.decode(Base64.decode(hash)));
|
||||
return new AudioHash(hash, AudioWaveFormData.parseFrom(Base64.decode(hash)));
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ object AudioWaveForms {
|
||||
private fun generateWaveForm(context: Context, uri: Uri, cacheKey: String, attachmentId: AttachmentId): CacheCheckResult {
|
||||
try {
|
||||
val startTime = System.currentTimeMillis()
|
||||
SignalDatabase.attachments.writeAudioHash(attachmentId, AudioWaveFormData())
|
||||
SignalDatabase.attachments.writeAudioHash(attachmentId, AudioWaveFormData.getDefaultInstance())
|
||||
|
||||
Log.i(TAG, "Starting wave form generation ($cacheKey)")
|
||||
val fileInfo: AudioFileInfo = AudioWaveFormGenerator.generateWaveForm(context, uri)
|
||||
|
||||
@@ -101,16 +101,16 @@ object Badges {
|
||||
|
||||
@JvmStatic
|
||||
fun toDatabaseBadge(badge: Badge): BadgeList.Badge {
|
||||
return BadgeList.Badge(
|
||||
id = badge.id,
|
||||
category = badge.category.code,
|
||||
description = badge.description,
|
||||
expiration = badge.expirationTimestamp,
|
||||
visible = badge.visible,
|
||||
name = badge.name,
|
||||
imageUrl = badge.imageUrl.toString(),
|
||||
imageDensity = badge.imageDensity
|
||||
)
|
||||
return BadgeList.Badge.newBuilder()
|
||||
.setId(badge.id)
|
||||
.setCategory(badge.category.code)
|
||||
.setDescription(badge.description)
|
||||
.setExpiration(badge.expirationTimestamp)
|
||||
.setVisible(badge.visible)
|
||||
.setName(badge.name)
|
||||
.setImageUrl(badge.imageUrl.toString())
|
||||
.setImageDensity(badge.imageDensity)
|
||||
.build()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
||||
@@ -79,11 +79,12 @@ class GiftMessageView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
actionView.setText(
|
||||
when (giftBadge.redemptionState) {
|
||||
when (giftBadge.redemptionState ?: GiftBadge.RedemptionState.UNRECOGNIZED) {
|
||||
GiftBadge.RedemptionState.PENDING -> R.string.GiftMessageView__redeem
|
||||
GiftBadge.RedemptionState.STARTED -> R.string.GiftMessageView__redeeming
|
||||
GiftBadge.RedemptionState.REDEEMED -> R.string.GiftMessageView__redeemed
|
||||
GiftBadge.RedemptionState.FAILED -> R.string.GiftMessageView__redeem
|
||||
GiftBadge.RedemptionState.UNRECOGNIZED -> R.string.GiftMessageView__redeem
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ object Gifts {
|
||||
): OutgoingMessage {
|
||||
return OutgoingMessage(
|
||||
threadRecipient = recipient,
|
||||
body = Base64.encodeBytes(giftBadge.encode()),
|
||||
body = Base64.encodeBytes(giftBadge.toByteArray()),
|
||||
isSecure = true,
|
||||
sentTimeMillis = sentTimestamp,
|
||||
expiresIn = expiresIn,
|
||||
|
||||
@@ -63,7 +63,7 @@ class ViewReceivedGiftBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
ViewReceivedGiftBottomSheet().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(ARG_SENT_FROM, messageRecord.fromRecipient.id)
|
||||
putByteArray(ARG_GIFT_BADGE, messageRecord.giftBadge!!.encode())
|
||||
putByteArray(ARG_GIFT_BADGE, messageRecord.giftBadge!!.toByteArray())
|
||||
putLong(ARG_MESSAGE_ID, messageRecord.id)
|
||||
}
|
||||
show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
|
||||
@@ -34,7 +34,7 @@ class ViewSentGiftBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
ViewSentGiftBottomSheet().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(ARG_SENT_TO, messageRecord.toRecipient.id)
|
||||
putByteArray(ARG_GIFT_BADGE, messageRecord.giftBadge!!.encode())
|
||||
putByteArray(ARG_GIFT_BADGE, messageRecord.giftBadge!!.toByteArray())
|
||||
}
|
||||
show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
}
|
||||
@@ -45,7 +45,7 @@ class ViewSentGiftBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
get() = requireArguments().getParcelableCompat(ARG_SENT_TO, RecipientId::class.java)!!
|
||||
|
||||
private val giftBadge: GiftBadge
|
||||
get() = GiftBadge.ADAPTER.decode(requireArguments().getByteArray(ARG_GIFT_BADGE)!!)
|
||||
get() = GiftBadge.parseFrom(requireArguments().getByteArray(ARG_GIFT_BADGE))
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
|
||||
@@ -7,16 +7,12 @@ package org.thoughtcrime.securesms.calls.links
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.appcompat.widget.LinearLayoutCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
* ConversationItem action button for joining a call link.
|
||||
*/
|
||||
class CallLinkJoinButton @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
@@ -26,19 +22,10 @@ class CallLinkJoinButton @JvmOverloads constructor(
|
||||
inflate(context, R.layout.call_link_join_button, this)
|
||||
}
|
||||
|
||||
private val joinStroke: View = findViewById(R.id.join_stroke)
|
||||
private val joinButton: MaterialButton = findViewById(R.id.join_button)
|
||||
|
||||
fun setTextColor(@ColorRes textColorResId: Int) {
|
||||
val color = ContextCompat.getColor(context, textColorResId)
|
||||
|
||||
joinButton.setTextColor(color)
|
||||
}
|
||||
|
||||
fun setStrokeColor(@ColorRes strokeColorResId: Int) {
|
||||
val color = ContextCompat.getColor(context, strokeColorResId)
|
||||
|
||||
joinStroke.setBackgroundColor(color)
|
||||
joinButton.setTextColor(ContextCompat.getColor(context, textColorResId))
|
||||
}
|
||||
|
||||
fun setJoinClickListener(onClickListener: OnClickListener) {
|
||||
|
||||
@@ -59,6 +59,7 @@ object CallLinks {
|
||||
}
|
||||
|
||||
if (!url.startsWith(HTTPS_LINK_PREFIX) && !url.startsWith(SNGL_LINK_PREFIX)) {
|
||||
Log.w(TAG, "Invalid url prefix.")
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -133,10 +132,6 @@ fun SignalCallRow(
|
||||
|
||||
Buttons.Small(
|
||||
onClick = onJoinClicked,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface
|
||||
),
|
||||
modifier = Modifier.align(CenterVertically)
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__join))
|
||||
|
||||
@@ -106,13 +106,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Rows.TextRow(
|
||||
text = stringResource(
|
||||
id = if (callLink.state.name.isEmpty()) {
|
||||
R.string.CreateCallLinkBottomSheetDialogFragment__add_call_name
|
||||
} else {
|
||||
R.string.CreateCallLinkBottomSheetDialogFragment__edit_call_name
|
||||
}
|
||||
),
|
||||
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__add_call_name),
|
||||
onClick = this@CreateCallLinkBottomSheetDialogFragment::onAddACallNameClicked
|
||||
)
|
||||
|
||||
|
||||
@@ -246,13 +246,7 @@ private fun CallLinkDetails(
|
||||
|
||||
if (state.callLink.credentials?.adminPassBytes != null) {
|
||||
Rows.TextRow(
|
||||
text = stringResource(
|
||||
id = if (state.callLink.state.name.isEmpty()) {
|
||||
R.string.CreateCallLinkBottomSheetDialogFragment__add_call_name
|
||||
} else {
|
||||
R.string.CreateCallLinkBottomSheetDialogFragment__edit_call_name
|
||||
}
|
||||
),
|
||||
text = stringResource(id = R.string.CallLinkDetailsFragment__add_call_name),
|
||||
onClick = callback::onEditNameClicked
|
||||
)
|
||||
|
||||
|
||||
@@ -57,7 +57,6 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.stories.tabs.ConversationListTab
|
||||
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.doAfterNextLayout
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
@@ -231,13 +230,6 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
val count = callLogActionMode.getCount()
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(resources.getQuantityString(R.plurals.CallLogFragment__delete_d_calls, count, count))
|
||||
.setMessage(
|
||||
if (FeatureFlags.adHocCalling()) {
|
||||
getString(R.string.CallLogFragment__call_links_youve_created)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
.setPositiveButton(R.string.CallLogFragment__delete) { _, _ ->
|
||||
performDeletion(count, viewModel.stageSelectionDeletion())
|
||||
callLogActionMode.end()
|
||||
@@ -371,13 +363,6 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
override fun deleteCall(call: CallLogRow) {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(resources.getQuantityString(R.plurals.CallLogFragment__delete_d_calls, 1, 1))
|
||||
.setMessage(
|
||||
if (FeatureFlags.adHocCalling()) {
|
||||
getString(R.string.CallLogFragment__call_links_youve_created)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
.setPositiveButton(R.string.CallLogFragment__delete) { _, _ ->
|
||||
performDeletion(1, viewModel.stageCallDeletion(call))
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ class CallLogPagedDataSource(
|
||||
remaining -= callEvents.size
|
||||
}
|
||||
|
||||
if (hasFilter && start <= clearFilterStart && remaining > 0) {
|
||||
if (start <= clearFilterStart && remaining > 0) {
|
||||
callLogRows.add(CallLogRow.ClearFilter)
|
||||
}
|
||||
|
||||
|
||||
@@ -93,11 +93,11 @@ sealed class CallLogRow {
|
||||
return FULL
|
||||
}
|
||||
|
||||
if (groupCallUpdateDetails.inCallUuids.contains(Recipient.self().requireAci().rawUuid.toString())) {
|
||||
if (groupCallUpdateDetails.inCallUuidsList.contains(Recipient.self().requireAci().rawUuid.toString())) {
|
||||
return LOCAL_USER_JOINED
|
||||
}
|
||||
|
||||
return if (groupCallUpdateDetails.inCallUuids.isNotEmpty()) {
|
||||
return if (groupCallUpdateDetails.inCallUuidsCount > 0) {
|
||||
ACTIVE
|
||||
} else {
|
||||
NONE
|
||||
|
||||
@@ -12,7 +12,6 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.transfercontrols.TransferControlView;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||
@@ -55,7 +54,7 @@ public class AlbumThumbnailView extends FrameLayout {
|
||||
inflate(getContext(), R.layout.album_thumbnail_view, this);
|
||||
|
||||
albumCellContainer = findViewById(R.id.album_cell_container);
|
||||
transferControls = new Stub<>(findViewById(R.id.album_transfer_controls_stub));
|
||||
transferControls = new Stub<>(findViewById(R.id.album_transfer_controls_stub));
|
||||
}
|
||||
|
||||
public void setSlides(@NonNull GlideRequests glideRequests, @NonNull List<Slide> slides, boolean showControls) {
|
||||
@@ -65,13 +64,12 @@ public class AlbumThumbnailView extends FrameLayout {
|
||||
|
||||
if (showControls) {
|
||||
transferControls.get().setShowDownloadText(true);
|
||||
transferControls.get().setSlides(slides);
|
||||
transferControls.get().setDownloadClickListener(v -> {
|
||||
if (downloadClickListener != null) {
|
||||
downloadClickListener.onClick(v, slides);
|
||||
}
|
||||
});
|
||||
transferControls.get().setSlides(slides);
|
||||
transferControls.setVisibility(VISIBLE);
|
||||
} else {
|
||||
if (transferControls.resolved()) {
|
||||
transferControls.get().setVisibility(GONE);
|
||||
@@ -87,7 +85,6 @@ public class AlbumThumbnailView extends FrameLayout {
|
||||
|
||||
showSlides(glideRequests, slides);
|
||||
applyCorners();
|
||||
forceLayout();
|
||||
}
|
||||
|
||||
public void setCellBackgroundColor(@ColorInt int color) {
|
||||
@@ -120,25 +117,22 @@ public class AlbumThumbnailView extends FrameLayout {
|
||||
private void inflateLayout(int sizeClass) {
|
||||
albumCellContainer.removeAllViews();
|
||||
|
||||
int resId = switch (sizeClass) {
|
||||
case 2 -> R.layout.album_thumbnail_2;
|
||||
case 3 -> R.layout.album_thumbnail_3;
|
||||
case 4 -> R.layout.album_thumbnail_4;
|
||||
case 5 -> R.layout.album_thumbnail_5;
|
||||
default -> R.layout.album_thumbnail_many;
|
||||
};
|
||||
|
||||
inflate(getContext(), resId, albumCellContainer);
|
||||
if (transferControls.resolved()) {
|
||||
int size = switch (sizeClass) {
|
||||
case 2 -> R.dimen.album_2_total_height;
|
||||
case 3 -> R.dimen.album_3_total_height;
|
||||
case 4 -> R.dimen.album_4_total_height;
|
||||
default -> R.dimen.album_5_total_height;
|
||||
};
|
||||
final ViewGroup.LayoutParams params = transferControls.get().getLayoutParams();
|
||||
params.height = getContext().getResources().getDimensionPixelSize(size);
|
||||
transferControls.get().setLayoutParams(params);
|
||||
switch (sizeClass) {
|
||||
case 2:
|
||||
inflate(getContext(), R.layout.album_thumbnail_2, albumCellContainer);
|
||||
break;
|
||||
case 3:
|
||||
inflate(getContext(), R.layout.album_thumbnail_3, albumCellContainer);
|
||||
break;
|
||||
case 4:
|
||||
inflate(getContext(), R.layout.album_thumbnail_4, albumCellContainer);
|
||||
break;
|
||||
case 5:
|
||||
inflate(getContext(), R.layout.album_thumbnail_5, albumCellContainer);
|
||||
break;
|
||||
default:
|
||||
inflate(getContext(), R.layout.album_thumbnail_many, albumCellContainer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -322,12 +322,12 @@ public class ConversationItemFooter extends ConstraintLayout {
|
||||
} else {
|
||||
long timestamp = messageRecord.getTimestamp();
|
||||
if (messageRecord.isEditMessage()) {
|
||||
if (displayMode == ConversationItemDisplayMode.EditHistory.INSTANCE) {
|
||||
if (displayMode == ConversationItemDisplayMode.EDIT_HISTORY) {
|
||||
timestamp = messageRecord.getDateSent();
|
||||
}
|
||||
}
|
||||
String date = DateUtils.getDatelessRelativeTimeSpanString(getContext(), locale, timestamp);
|
||||
if (displayMode != ConversationItemDisplayMode.Detailed.INSTANCE && messageRecord.isEditMessage() && messageRecord.isLatestRevision()) {
|
||||
if (displayMode != ConversationItemDisplayMode.DETAILED && messageRecord.isEditMessage() && messageRecord.isLatestRevision()) {
|
||||
date = getContext().getString(R.string.ConversationItem_edited_timestamp_footer, date);
|
||||
}
|
||||
dateView.setText(date);
|
||||
|
||||
@@ -255,17 +255,9 @@ class ConversationItemThumbnail @JvmOverloads constructor(
|
||||
state.applyState(thumbnail, album)
|
||||
}
|
||||
|
||||
fun setPlayVideoClickListener(listener: SlideClickListener?) {
|
||||
fun setProgressWheelClickListener(listener: SlideClickListener?) {
|
||||
state = state.copy(
|
||||
thumbnailViewState = state.thumbnailViewState.copy(playVideoClickListener = listener)
|
||||
)
|
||||
|
||||
state.applyState(thumbnail, album)
|
||||
}
|
||||
|
||||
fun setCancelDownloadClickListener(listener: SlidesClickedListener?) {
|
||||
state = state.copy(
|
||||
thumbnailViewState = state.thumbnailViewState.copy(cancelDownloadClickListener = listener)
|
||||
thumbnailViewState = state.thumbnailViewState.copy(progressWheelClickListener = listener)
|
||||
)
|
||||
|
||||
state.applyState(thumbnail, album)
|
||||
|
||||
@@ -31,9 +31,7 @@ data class ConversationItemThumbnailState(
|
||||
@IgnoredOnParcel
|
||||
private val downloadClickListener: SlidesClickedListener? = null,
|
||||
@IgnoredOnParcel
|
||||
private val cancelDownloadClickListener: SlidesClickedListener? = null,
|
||||
@IgnoredOnParcel
|
||||
private val playVideoClickListener: SlideClickListener? = null,
|
||||
private val progressWheelClickListener: SlideClickListener? = null,
|
||||
@IgnoredOnParcel
|
||||
private val longClickListener: OnLongClickListener? = null,
|
||||
private val visibility: Int = View.GONE,
|
||||
@@ -59,8 +57,7 @@ data class ConversationItemThumbnailState(
|
||||
thumbnailView.get().setRadii(cornerTopLeft, cornerTopRight, cornerBottomRight, cornerBottomLeft)
|
||||
thumbnailView.get().setThumbnailClickListener(clickListener)
|
||||
thumbnailView.get().setDownloadClickListener(downloadClickListener)
|
||||
thumbnailView.get().setCancelDownloadClickListener(cancelDownloadClickListener)
|
||||
thumbnailView.get().setPlayVideoClickListener(playVideoClickListener)
|
||||
thumbnailView.get().setProgressWheelClickListener(progressWheelClickListener)
|
||||
thumbnailView.get().setOnLongClickListener(longClickListener)
|
||||
thumbnailView.get().setBounds(minWidth, maxWidth, minHeight, maxHeight)
|
||||
}
|
||||
|
||||
@@ -13,12 +13,11 @@ import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.signal.core.util.ResourceUtil
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.databinding.PromptLogsBottomSheetBinding
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
@@ -28,21 +27,14 @@ import org.thoughtcrime.securesms.util.SupportEmailUtil
|
||||
class DebugLogsPromptDialogFragment : FixedRoundedCornerBottomSheetDialogFragment() {
|
||||
|
||||
companion object {
|
||||
private const val KEY_PURPOSE = "purpose"
|
||||
|
||||
@JvmStatic
|
||||
fun show(context: Context, fragmentManager: FragmentManager, purpose: Purpose) {
|
||||
fun show(context: Context, fragmentManager: FragmentManager) {
|
||||
if (NetworkUtil.isConnected(context) && fragmentManager.findFragmentByTag(BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) == null) {
|
||||
DebugLogsPromptDialogFragment().apply {
|
||||
arguments = bundleOf(
|
||||
KEY_PURPOSE to purpose.serialized
|
||||
)
|
||||
arguments = bundleOf()
|
||||
}.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
|
||||
when (purpose) {
|
||||
Purpose.NOTIFICATIONS -> SignalStore.uiHints().lastNotificationLogsPrompt = System.currentTimeMillis()
|
||||
Purpose.CRASH -> SignalStore.uiHints().lastCrashPrompt = System.currentTimeMillis()
|
||||
}
|
||||
SignalStore.uiHints().lastNotificationLogsPrompt = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,12 +44,7 @@ class DebugLogsPromptDialogFragment : FixedRoundedCornerBottomSheetDialogFragmen
|
||||
|
||||
private val binding by ViewBinderDelegate(PromptLogsBottomSheetBinding::bind)
|
||||
|
||||
private val viewModel: PromptLogsViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
val purpose = Purpose.deserialize(requireArguments().getInt(KEY_PURPOSE))
|
||||
PromptLogsViewModel.Factory(ApplicationDependencies.getApplication(), purpose)
|
||||
}
|
||||
)
|
||||
private lateinit var viewModel: PromptLogsViewModel
|
||||
|
||||
private val disposables: LifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
@@ -68,21 +55,11 @@ class DebugLogsPromptDialogFragment : FixedRoundedCornerBottomSheetDialogFragmen
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
disposables.bindTo(viewLifecycleOwner)
|
||||
|
||||
val purpose = Purpose.deserialize(requireArguments().getInt(KEY_PURPOSE))
|
||||
|
||||
when (purpose) {
|
||||
Purpose.NOTIFICATIONS -> {
|
||||
binding.title.setText(R.string.PromptLogsSlowNotificationsDialog__title)
|
||||
}
|
||||
Purpose.CRASH -> {
|
||||
binding.title.setText(R.string.PromptLogsSlowNotificationsDialog__title_crash)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(this).get(PromptLogsViewModel::class.java)
|
||||
binding.submit.setOnClickListener {
|
||||
val progressDialog = SignalProgressDialog.show(requireContext())
|
||||
disposables += viewModel.submitLogs().subscribe({ result ->
|
||||
submitLogs(result, purpose)
|
||||
submitLogs(result)
|
||||
progressDialog.dismiss()
|
||||
dismiss()
|
||||
}, { _ ->
|
||||
@@ -91,40 +68,30 @@ class DebugLogsPromptDialogFragment : FixedRoundedCornerBottomSheetDialogFragmen
|
||||
dismiss()
|
||||
})
|
||||
}
|
||||
|
||||
binding.decline.setOnClickListener {
|
||||
if (purpose == Purpose.NOTIFICATIONS) {
|
||||
SignalStore.uiHints().markDeclinedShareNotificationLogs()
|
||||
}
|
||||
|
||||
SignalStore.uiHints().markDeclinedShareNotificationLogs()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun submitLogs(debugLog: String, purpose: Purpose) {
|
||||
private fun submitLogs(debugLog: String) {
|
||||
CommunicationActions.openEmail(
|
||||
requireContext(),
|
||||
SupportEmailUtil.getSupportEmailAddress(requireContext()),
|
||||
getString(R.string.DebugLogsPromptDialogFragment__signal_android_support_request),
|
||||
getEmailBody(debugLog, purpose)
|
||||
getEmailBody(debugLog)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getEmailBody(debugLog: String?, purpose: Purpose): String {
|
||||
private fun getEmailBody(debugLog: String?): String {
|
||||
val suffix = StringBuilder()
|
||||
|
||||
if (debugLog != null) {
|
||||
suffix.append("\n")
|
||||
suffix.append(getString(R.string.HelpFragment__debug_log))
|
||||
suffix.append(" ")
|
||||
suffix.append(debugLog)
|
||||
}
|
||||
|
||||
val category = when (purpose) {
|
||||
Purpose.NOTIFICATIONS -> ResourceUtil.getEnglishResources(requireContext()).getString(R.string.DebugLogsPromptDialogFragment__slow_notifications_category)
|
||||
Purpose.CRASH -> ResourceUtil.getEnglishResources(requireContext()).getString(R.string.DebugLogsPromptDialogFragment__crash_category)
|
||||
}
|
||||
|
||||
val category = ResourceUtil.getEnglishResources(requireContext()).getString(R.string.DebugLogsPromptDialogFragment__slow_notifications_category)
|
||||
return SupportEmailUtil.generateSupportEmailBody(
|
||||
requireContext(),
|
||||
R.string.DebugLogsPromptDialogFragment__signal_android_support_request,
|
||||
@@ -133,21 +100,4 @@ class DebugLogsPromptDialogFragment : FixedRoundedCornerBottomSheetDialogFragmen
|
||||
suffix.toString()
|
||||
)
|
||||
}
|
||||
|
||||
enum class Purpose(val serialized: Int) {
|
||||
|
||||
NOTIFICATIONS(1), CRASH(2);
|
||||
|
||||
companion object {
|
||||
fun deserialize(serialized: Int): Purpose {
|
||||
for (value in values()) {
|
||||
if (value.serialized == serialized) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
throw IllegalArgumentException("Invalid value: $serialized")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,10 +163,10 @@ public class LinkPreviewView extends FrameLayout {
|
||||
}
|
||||
|
||||
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail) {
|
||||
setLinkPreview(glideRequests, linkPreview, showThumbnail, true, false);
|
||||
setLinkPreview(glideRequests, linkPreview, showThumbnail, true);
|
||||
}
|
||||
|
||||
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail, boolean showDescription, boolean scheduleMessageMode) {
|
||||
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail, boolean showDescription) {
|
||||
spinner.setVisibility(GONE);
|
||||
noPreview.setVisibility(GONE);
|
||||
|
||||
@@ -216,7 +216,7 @@ public class LinkPreviewView extends FrameLayout {
|
||||
if (showThumbnail && linkPreview.getThumbnail().isPresent()) {
|
||||
thumbnail.setVisibility(VISIBLE);
|
||||
thumbnailState.applyState(thumbnail);
|
||||
thumbnail.get().setImageResource(glideRequests, new ImageSlide(linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION && !scheduleMessageMode, false);
|
||||
thumbnail.get().setImageResource(glideRequests, new ImageSlide(linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION, false);
|
||||
thumbnail.get().showDownloadText(false);
|
||||
} else if (callLinkRootKey != null) {
|
||||
thumbnail.setVisibility(VISIBLE);
|
||||
|
||||
@@ -5,37 +5,17 @@
|
||||
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.subjects.SingleSubject
|
||||
import org.thoughtcrime.securesms.crash.CrashConfig
|
||||
import org.thoughtcrime.securesms.database.LogDatabase
|
||||
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogRepository
|
||||
|
||||
class PromptLogsViewModel(private val context: Application, purpose: DebugLogsPromptDialogFragment.Purpose) : AndroidViewModel(context) {
|
||||
class PromptLogsViewModel : ViewModel() {
|
||||
|
||||
private val submitDebugLogRepository = SubmitDebugLogRepository()
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
init {
|
||||
if (purpose == DebugLogsPromptDialogFragment.Purpose.CRASH) {
|
||||
disposables += Single
|
||||
.fromCallable {
|
||||
LogDatabase.getInstance(context).crashes.markAsPrompted(CrashConfig.patterns, System.currentTimeMillis())
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
fun submitLogs(): Single<String> {
|
||||
val singleSubject = SingleSubject.create<String?>()
|
||||
submitDebugLogRepository.buildAndSubmitLog { result ->
|
||||
@@ -48,14 +28,4 @@ class PromptLogsViewModel(private val context: Application, purpose: DebugLogsPr
|
||||
|
||||
return singleSubject.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
class Factory(private val context: Application, private val purpose: DebugLogsPromptDialogFragment.Purpose) : ViewModelProvider.NewInstanceFactory() {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(PromptLogsViewModel(context, purpose))!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ import org.signal.core.util.logging.Log;
|
||||
import org.signal.glide.transforms.SignalDownsampleStrategy;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.components.transfercontrols.TransferControlView;
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequest;
|
||||
@@ -42,6 +41,7 @@ import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
||||
import org.thoughtcrime.securesms.mms.VideoSlide;
|
||||
import org.thoughtcrime.securesms.stories.StoryTextPostModel;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
@@ -80,13 +80,12 @@ public class ThumbnailView extends FrameLayout {
|
||||
|
||||
private final CornerMask cornerMask;
|
||||
|
||||
private ThumbnailViewTransferControlsState transferControlsState = new ThumbnailViewTransferControlsState();
|
||||
private ThumbnailViewTransferControlsState transferControlsState = new ThumbnailViewTransferControlsState();
|
||||
private Stub<TransferControlView> transferControlViewStub;
|
||||
private SlideClickListener thumbnailClickListener = null;
|
||||
private SlidesClickedListener downloadClickListener = null;
|
||||
private SlidesClickedListener cancelDownloadClickListener = null;
|
||||
private SlideClickListener playVideoClickListener = null;
|
||||
private Slide slide = null;
|
||||
private SlideClickListener thumbnailClickListener = null;
|
||||
private SlidesClickedListener downloadClickListener = null;
|
||||
private SlideClickListener progressWheelClickListener = null;
|
||||
private Slide slide = null;
|
||||
|
||||
|
||||
public ThumbnailView(Context context) {
|
||||
@@ -368,11 +367,10 @@ public class ThumbnailView extends FrameLayout {
|
||||
}
|
||||
|
||||
transferControlsState = transferControlsState.withSlide(slide)
|
||||
.withDownloadClickListener(new DownloadClickDispatcher())
|
||||
.withCancelDownloadClickListener(new CancelClickDispatcher());
|
||||
.withDownloadClickListener(new DownloadClickDispatcher());
|
||||
|
||||
if (MediaUtil.isInstantVideoSupported(slide)) {
|
||||
transferControlsState = transferControlsState.withInstantPlaybackClickListener(new ProgressWheelClickDispatcher());
|
||||
if (FeatureFlags.instantVideoPlayback()) {
|
||||
transferControlsState = transferControlsState.withProgressWheelClickListener(new ProgressWheelClickDispatcher());
|
||||
}
|
||||
|
||||
transferControlsState.applyState(transferControlViewStub);
|
||||
@@ -527,12 +525,8 @@ public class ThumbnailView extends FrameLayout {
|
||||
this.downloadClickListener = listener;
|
||||
}
|
||||
|
||||
public void setCancelDownloadClickListener(SlidesClickedListener listener) {
|
||||
this.cancelDownloadClickListener = listener;
|
||||
}
|
||||
|
||||
public void setPlayVideoClickListener(SlideClickListener listener) {
|
||||
this.playVideoClickListener = listener;
|
||||
public void setProgressWheelClickListener(SlideClickListener listener) {
|
||||
this.progressWheelClickListener = listener;
|
||||
}
|
||||
|
||||
public void clear(GlideRequests glideRequests) {
|
||||
@@ -574,9 +568,9 @@ public class ThumbnailView extends FrameLayout {
|
||||
|
||||
private GlideRequest<Drawable> buildThumbnailGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
|
||||
GlideRequest<Drawable> request = applySizing(glideRequests.load(new DecryptableUri(Objects.requireNonNull(slide.getUri())))
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE)
|
||||
.transition(withCrossFade()));
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE)
|
||||
.transition(withCrossFade()));
|
||||
|
||||
boolean doNotShowMissingThumbnailImage = Build.VERSION.SDK_INT < 23;
|
||||
|
||||
@@ -631,7 +625,7 @@ public class ThumbnailView extends FrameLayout {
|
||||
if (Util.equals(slide, other)) {
|
||||
|
||||
if (slide != null && other != null) {
|
||||
byte[] digestLeft = slide.asAttachment().getDigest();
|
||||
byte[] digestLeft = slide.asAttachment().getDigest();
|
||||
byte[] digestRight = other.asAttachment().getDigest();
|
||||
|
||||
return Arrays.equals(digestLeft, digestRight);
|
||||
@@ -676,26 +670,14 @@ public class ThumbnailView extends FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
private class CancelClickDispatcher implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Log.i(TAG, "onClick() for cancel button");
|
||||
if (cancelDownloadClickListener != null && slide != null) {
|
||||
cancelDownloadClickListener.onClick(view, Collections.singletonList(slide));
|
||||
} else {
|
||||
Log.w(TAG, "Received a cancel button click, but unable to execute it. slide: " + slide + " cancelDownloadClickListener: " + cancelDownloadClickListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ProgressWheelClickDispatcher implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Log.i(TAG, "onClick() for instant video playback");
|
||||
if (playVideoClickListener != null && slide != null) {
|
||||
playVideoClickListener.onClick(view, slide);
|
||||
Log.i(TAG, "onClick() for progress wheel");
|
||||
if (progressWheelClickListener != null && slide != null) {
|
||||
progressWheelClickListener.onClick(view, slide);
|
||||
} else {
|
||||
Log.w(TAG, "Received an instant video click, but unable to execute it. slide: " + slide + " progressWheelClickListener: " + playVideoClickListener);
|
||||
Log.w(TAG, "Received a progress wheel click, but unable to execute it. slide: " + slide + " progressWheelClickListener: " + progressWheelClickListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.view.View.OnClickListener
|
||||
import org.thoughtcrime.securesms.components.transfercontrols.TransferControlView
|
||||
import org.thoughtcrime.securesms.mms.Slide
|
||||
import org.thoughtcrime.securesms.util.views.Stub
|
||||
|
||||
@@ -13,8 +12,7 @@ data class ThumbnailViewTransferControlsState(
|
||||
val isClickable: Boolean = true,
|
||||
val slide: Slide? = null,
|
||||
val downloadClickedListener: OnClickListener? = null,
|
||||
val cancelDownloadClickedListener: OnClickListener? = null,
|
||||
val instantPlaybackClickListener: OnClickListener? = null,
|
||||
val progressWheelClickedListener: OnClickListener? = null,
|
||||
val showDownloadText: Boolean = true
|
||||
) {
|
||||
|
||||
@@ -22,8 +20,7 @@ data class ThumbnailViewTransferControlsState(
|
||||
fun withClickable(isClickable: Boolean): ThumbnailViewTransferControlsState = copy(isClickable = isClickable)
|
||||
fun withSlide(slide: Slide?): ThumbnailViewTransferControlsState = copy(slide = slide)
|
||||
fun withDownloadClickListener(downloadClickedListener: OnClickListener): ThumbnailViewTransferControlsState = copy(downloadClickedListener = downloadClickedListener)
|
||||
fun withCancelDownloadClickListener(cancelClickListener: OnClickListener): ThumbnailViewTransferControlsState = copy(cancelDownloadClickedListener = cancelClickListener)
|
||||
fun withInstantPlaybackClickListener(instantPlaybackClickListener: OnClickListener): ThumbnailViewTransferControlsState = copy(instantPlaybackClickListener = instantPlaybackClickListener)
|
||||
fun withProgressWheelClickListener(progressWheelClickedListener: OnClickListener): ThumbnailViewTransferControlsState = copy(progressWheelClickedListener = progressWheelClickedListener)
|
||||
fun withDownloadText(showDownloadText: Boolean): ThumbnailViewTransferControlsState = copy(showDownloadText = showDownloadText)
|
||||
|
||||
fun applyState(transferControlView: Stub<TransferControlView>) {
|
||||
@@ -34,9 +31,8 @@ data class ThumbnailViewTransferControlsState(
|
||||
transferControlView.get().setSlide(slide)
|
||||
}
|
||||
transferControlView.get().setDownloadClickListener(downloadClickedListener)
|
||||
transferControlView.get().setProgressWheelClickListener(progressWheelClickedListener)
|
||||
transferControlView.get().setShowDownloadText(showDownloadText)
|
||||
transferControlView.get().setCancelClickListener(cancelDownloadClickedListener)
|
||||
transferControlView.get().setInstantPlaybackClickListener(instantPlaybackClickListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.animation.LayoutTransition;
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.pnikosis.materialishprogress.ProgressWheel;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable;
|
||||
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class TransferControlView extends FrameLayout {
|
||||
|
||||
private static final String TAG = "TransferControlView";
|
||||
private static final int UPLOAD_TASK_WEIGHT = 1;
|
||||
|
||||
/**
|
||||
* A weighting compared to {@link #UPLOAD_TASK_WEIGHT}
|
||||
*/
|
||||
private static final int COMPRESSION_TASK_WEIGHT = 3;
|
||||
|
||||
@Nullable private List<Slide> slides;
|
||||
@Nullable private View current;
|
||||
|
||||
private final ProgressWheel progressWheel;
|
||||
private final View downloadDetails;
|
||||
private final TextView downloadDetailsText;
|
||||
|
||||
private final Map<Attachment, Float> networkProgress;
|
||||
private final Map<Attachment, Float> compresssionProgress;
|
||||
|
||||
public TransferControlView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public TransferControlView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public TransferControlView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
inflate(context, R.layout.transfer_controls_view, this);
|
||||
|
||||
setLongClickable(false);
|
||||
setBackground(ContextCompat.getDrawable(context, R.drawable.transfer_controls_background));
|
||||
setVisibility(GONE);
|
||||
setLayoutTransition(new LayoutTransition());
|
||||
|
||||
this.networkProgress = new HashMap<>();
|
||||
this.compresssionProgress = new HashMap<>();
|
||||
|
||||
this.progressWheel = findViewById(R.id.progress_wheel);
|
||||
this.downloadDetails = findViewById(R.id.download_details);
|
||||
this.downloadDetailsText = findViewById(R.id.download_details_text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFocusable(boolean focusable) {
|
||||
super.setFocusable(focusable);
|
||||
downloadDetails.setFocusable(focusable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClickable(boolean clickable) {
|
||||
super.setClickable(clickable);
|
||||
downloadDetails.setClickable(clickable);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
if (!EventBus.getDefault().isRegistered(this)) EventBus.getDefault().register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
public void setSlide(final @NonNull Slide slides) {
|
||||
setSlides(Collections.singletonList(slides));
|
||||
}
|
||||
|
||||
public void setSlides(final @NonNull List<Slide> slides) {
|
||||
if (slides.isEmpty()) {
|
||||
throw new IllegalArgumentException("Must provide at least one slide.");
|
||||
}
|
||||
|
||||
this.slides = slides;
|
||||
|
||||
if (!isUpdateToExistingSet(slides)) {
|
||||
networkProgress.clear();
|
||||
compresssionProgress.clear();
|
||||
Stream.of(slides).forEach(s -> networkProgress.put(s.asAttachment(), 0f));
|
||||
}
|
||||
|
||||
for (Slide slide : slides) {
|
||||
if (slide.asAttachment().getTransferState() == AttachmentTable.TRANSFER_PROGRESS_DONE) {
|
||||
networkProgress.put(slide.asAttachment(), 1f);
|
||||
}
|
||||
}
|
||||
|
||||
switch (getTransferState(slides)) {
|
||||
case AttachmentTable.TRANSFER_PROGRESS_STARTED:
|
||||
showProgressSpinner(calculateProgress(networkProgress, compresssionProgress));
|
||||
break;
|
||||
case AttachmentTable.TRANSFER_PROGRESS_PENDING:
|
||||
case AttachmentTable.TRANSFER_PROGRESS_FAILED:
|
||||
String downloadText = getDownloadText(this.slides);
|
||||
if (!Objects.equals(downloadText, downloadDetailsText.getText().toString())) {
|
||||
downloadDetailsText.setText(getDownloadText(this.slides));
|
||||
}
|
||||
|
||||
display(downloadDetails);
|
||||
break;
|
||||
default:
|
||||
display(null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void showProgressSpinner() {
|
||||
showProgressSpinner(calculateProgress(networkProgress, compresssionProgress));
|
||||
}
|
||||
|
||||
public void showProgressSpinner(float progress) {
|
||||
if (progress == 0) {
|
||||
progressWheel.spin();
|
||||
} else {
|
||||
progressWheel.setInstantProgress(progress);
|
||||
}
|
||||
|
||||
display(progressWheel);
|
||||
}
|
||||
|
||||
public void setDownloadClickListener(final @Nullable OnClickListener listener) {
|
||||
downloadDetails.setOnClickListener(listener);
|
||||
}
|
||||
|
||||
public void setProgressWheelClickListener(final @Nullable OnClickListener listener) {
|
||||
progressWheel.setOnClickListener(listener);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
clearAnimation();
|
||||
setVisibility(GONE);
|
||||
if (current != null) {
|
||||
current.clearAnimation();
|
||||
current.setVisibility(GONE);
|
||||
}
|
||||
current = null;
|
||||
slides = null;
|
||||
}
|
||||
|
||||
public void setShowDownloadText(boolean showDownloadText) {
|
||||
downloadDetailsText.setVisibility(showDownloadText ? VISIBLE : GONE);
|
||||
forceLayout();
|
||||
}
|
||||
|
||||
private boolean isUpdateToExistingSet(@NonNull List<Slide> slides) {
|
||||
if (slides.size() != networkProgress.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (Slide slide : slides) {
|
||||
if (!networkProgress.containsKey(slide.asAttachment())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static int getTransferState(@NonNull List<Slide> slides) {
|
||||
int transferState = AttachmentTable.TRANSFER_PROGRESS_DONE;
|
||||
boolean allFailed = true;
|
||||
|
||||
for (Slide slide : slides) {
|
||||
if (slide.getTransferState() != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE) {
|
||||
allFailed = false;
|
||||
if (slide.getTransferState() == AttachmentTable.TRANSFER_PROGRESS_PENDING && transferState == AttachmentTable.TRANSFER_PROGRESS_DONE) {
|
||||
transferState = slide.getTransferState();
|
||||
} else {
|
||||
transferState = Math.max(transferState, slide.getTransferState());
|
||||
}
|
||||
}
|
||||
}
|
||||
return allFailed ? AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE : transferState;
|
||||
}
|
||||
|
||||
private String getDownloadText(@NonNull List<Slide> slides) {
|
||||
if (slides.size() == 1) {
|
||||
return slides.get(0).getContentDescription(getContext());
|
||||
} else {
|
||||
int downloadCount = Stream.of(slides).reduce(0, (count, slide) -> slide.getTransferState() != AttachmentTable.TRANSFER_PROGRESS_DONE ? count + 1 : count);
|
||||
return getContext().getResources().getQuantityString(R.plurals.TransferControlView_n_items, downloadCount, downloadCount);
|
||||
}
|
||||
}
|
||||
|
||||
private void display(@Nullable final View view) {
|
||||
if (current == view) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (current != null) {
|
||||
current.setVisibility(GONE);
|
||||
}
|
||||
|
||||
if (view != null) {
|
||||
view.setVisibility(VISIBLE);
|
||||
setVisibility(VISIBLE);
|
||||
} else {
|
||||
setVisibility(GONE);
|
||||
}
|
||||
|
||||
current = view;
|
||||
}
|
||||
|
||||
private static float calculateProgress(@NonNull Map<Attachment, Float> uploadDownloadProgress, Map<Attachment, Float> compresssionProgress) {
|
||||
float totalDownloadProgress = 0;
|
||||
float totalCompressionProgress = 0;
|
||||
|
||||
for (float progress : uploadDownloadProgress.values()) {
|
||||
totalDownloadProgress += progress;
|
||||
}
|
||||
|
||||
for (float progress : compresssionProgress.values()) {
|
||||
totalCompressionProgress += progress;
|
||||
}
|
||||
|
||||
float weightedProgress = UPLOAD_TASK_WEIGHT * totalDownloadProgress + COMPRESSION_TASK_WEIGHT * totalCompressionProgress;
|
||||
float weightedTotal = UPLOAD_TASK_WEIGHT * uploadDownloadProgress.size() + COMPRESSION_TASK_WEIGHT * compresssionProgress.size();
|
||||
|
||||
return weightedProgress / weightedTotal;
|
||||
}
|
||||
|
||||
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||
public void onEventAsync(final PartProgressEvent event) {
|
||||
final Attachment attachment = event.attachment;
|
||||
if (networkProgress.containsKey(attachment)) {
|
||||
float proportionCompleted = ((float) event.progress) / event.total;
|
||||
|
||||
if (event.type == PartProgressEvent.Type.COMPRESSION) {
|
||||
compresssionProgress.put(attachment, proportionCompleted);
|
||||
} else {
|
||||
networkProgress.put(attachment, proportionCompleted);
|
||||
}
|
||||
|
||||
progressWheel.setInstantProgress(calculateProgress(networkProgress, compresssionProgress));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import androidx.core.view.GestureDetectorCompat;
|
||||
import androidx.core.view.ViewKt;
|
||||
import androidx.core.widget.TextViewCompat;
|
||||
|
||||
import org.signal.core.util.StringUtil;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
@@ -311,6 +312,13 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
setText(previousText, BufferType.SPANNABLE);
|
||||
}
|
||||
|
||||
public void setForceCustomEmoji(boolean forceCustom) {
|
||||
if (this.forceCustom != forceCustom) {
|
||||
this.forceCustom = forceCustom;
|
||||
setText(previousText, BufferType.SPANNABLE);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
public void bindGestureListener() {
|
||||
GestureDetectorCompat gestureDetectorCompat = new GestureDetectorCompat(getContext(), new OnGestureListener());
|
||||
@@ -354,10 +362,10 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
return;
|
||||
}
|
||||
|
||||
int overflowEnd = getLayout().getLineEnd(maxLines);
|
||||
int overflowEnd = getLayout().getLineEnd(maxLines - 1);
|
||||
CharSequence overflow = getText().subSequence(overflowStart, overflowEnd);
|
||||
float adjust = overflowText != null ? getPaint().measureText(overflowText, 0, overflowText.length()) : 0f;
|
||||
CharSequence ellipsized = TextUtils.ellipsize(overflow, getPaint(), getWidth() - adjust, TextUtils.TruncateAt.END);
|
||||
CharSequence ellipsized = StringUtil.trim(TextUtils.ellipsize(overflow, getPaint(), getWidth() - adjust, TextUtils.TruncateAt.END));
|
||||
|
||||
SpannableStringBuilder newContent = new SpannableStringBuilder();
|
||||
newContent.append(getText().subSequence(0, overflowStart).toString())
|
||||
|
||||
@@ -4,7 +4,6 @@ import androidx.annotation.WorkerThread
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
import org.signal.libsignal.protocol.SignalProtocolAddress
|
||||
@@ -18,6 +17,7 @@ import org.thoughtcrime.securesms.crypto.PreKeyUtil
|
||||
import org.thoughtcrime.securesms.database.IdentityTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingChangeNumberMetadata
|
||||
import org.thoughtcrime.securesms.database.model.toProtoByteString
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob
|
||||
import org.thoughtcrime.securesms.keyvalue.CertificateType
|
||||
@@ -42,7 +42,7 @@ import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity
|
||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage
|
||||
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
|
||||
import org.whispersystems.signalservice.internal.push.SyncMessage
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
import org.whispersystems.signalservice.internal.push.WhoAmIResponse
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException
|
||||
@@ -367,13 +367,13 @@ class ChangeNumberRepository(
|
||||
|
||||
// Device Messages
|
||||
if (deviceId != primaryDeviceId) {
|
||||
val pniChangeNumber = SyncMessage.PniChangeNumber(
|
||||
identityKeyPair = pniIdentity.serialize().toByteString(),
|
||||
signedPreKey = signedPreKeyRecord.serialize().toByteString(),
|
||||
lastResortKyberPreKey = lastResortKyberPreKeyRecord.serialize().toByteString(),
|
||||
registrationId = pniRegistrationId,
|
||||
newE164 = newE164
|
||||
)
|
||||
val pniChangeNumber = SyncMessage.PniChangeNumber.newBuilder()
|
||||
.setIdentityKeyPair(pniIdentity.serialize().toProtoByteString())
|
||||
.setSignedPreKey(signedPreKeyRecord.serialize().toProtoByteString())
|
||||
.setLastResortKyberPreKey(lastResortKyberPreKeyRecord.serialize().toProtoByteString())
|
||||
.setRegistrationId(pniRegistrationId)
|
||||
.setNewE164(newE164)
|
||||
.build()
|
||||
|
||||
deviceMessages += messageSender.getEncryptedSyncPniInitializeDeviceMessage(deviceId, pniChangeNumber)
|
||||
}
|
||||
@@ -391,12 +391,12 @@ class ChangeNumberRepository(
|
||||
pniRegistrationIds.mapKeys { it.key.toString() }
|
||||
)
|
||||
|
||||
val metadata = PendingChangeNumberMetadata(
|
||||
previousPni = SignalStore.account().pni!!.toByteString(),
|
||||
pniIdentityKeyPair = pniIdentity.serialize().toByteString(),
|
||||
pniRegistrationId = pniRegistrationIds[primaryDeviceId]!!,
|
||||
pniSignedPreKeyId = devicePniSignedPreKeys[primaryDeviceId]!!.keyId
|
||||
)
|
||||
val metadata = PendingChangeNumberMetadata.newBuilder()
|
||||
.setPreviousPni(SignalStore.account().pni!!.toByteString())
|
||||
.setPniIdentityKeyPair(pniIdentity.serialize().toProtoByteString())
|
||||
.setPniRegistrationId(pniRegistrationIds[primaryDeviceId]!!)
|
||||
.setPniSignedPreKeyId(devicePniSignedPreKeys[primaryDeviceId]!!.keyId)
|
||||
.build()
|
||||
|
||||
return ChangeNumberRequestData(request, metadata)
|
||||
}
|
||||
|
||||
@@ -768,7 +768,7 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
|
||||
private fun clearKeepLongerLogs() {
|
||||
SimpleTask.run({
|
||||
LogDatabase.getInstance(requireActivity().application).logs.clearKeepLonger()
|
||||
LogDatabase.getInstance(requireActivity().application).clearKeepLonger()
|
||||
}) {
|
||||
Toast.makeText(requireContext(), "Cleared keep longer logs", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ class InternalSettingsRepository(context: Context) {
|
||||
val title = "Release Note Title"
|
||||
val bodyText = "Release note body. Aren't I awesome?"
|
||||
val body = "$title\n\n$bodyText"
|
||||
val bodyRangeList = BodyRangeList.Builder()
|
||||
val bodyRangeList = BodyRangeList.newBuilder()
|
||||
.addStyle(BodyRangeList.BodyRange.Style.BOLD, 0, title.length)
|
||||
|
||||
val recipientId = SignalStore.releaseChannelValues().releaseChannelRecipientId!!
|
||||
|
||||
@@ -10,6 +10,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember
|
||||
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery
|
||||
import org.thoughtcrime.securesms.database.CallTable
|
||||
import org.thoughtcrime.securesms.database.MediaTable
|
||||
@@ -151,9 +152,9 @@ class ConversationSettingsRepository(
|
||||
consumer(
|
||||
if (groupRecord.isV2Group) {
|
||||
val decryptedGroup: DecryptedGroup = groupRecord.requireV2GroupProperties().decryptedGroup
|
||||
val pendingMembers: List<RecipientId> = decryptedGroup.pendingMembers
|
||||
.map { m -> m.serviceIdBytes }
|
||||
.map { s -> GroupProtoUtil.serviceIdBinaryToRecipientId(s) }
|
||||
val pendingMembers: List<RecipientId> = decryptedGroup.pendingMembersList
|
||||
.map(DecryptedPendingMember::getServiceIdBytes)
|
||||
.map(GroupProtoUtil::serviceIdBinaryToRecipientId)
|
||||
|
||||
val members = mutableListOf<RecipientId>()
|
||||
|
||||
|
||||
@@ -1,289 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.thoughtcrime.securesms.components.transfercontrols
|
||||
|
||||
import android.animation.LayoutTransition
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.text.format.Formatter
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.Space
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.annimon.stream.Stream
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.events.PartProgressEvent
|
||||
import org.thoughtcrime.securesms.mms.Slide
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.thoughtcrime.securesms.util.ThrottledDebouncer
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
class TransferControlView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) {
|
||||
private var slides: List<Slide> = emptyList()
|
||||
private var current: MutableSet<View> = HashSet()
|
||||
private var playableWhileDownloading = false
|
||||
private var showDownloadText = true
|
||||
private val downloadDetails: View
|
||||
private val downloadDetailsText: TextView
|
||||
private val primaryDetailsText: TextView
|
||||
private val secondaryViewSpace: Space
|
||||
private val playVideoButton: AppCompatImageView
|
||||
private val primaryProgressView: TransferProgressView
|
||||
private val secondaryProgressView: TransferProgressView
|
||||
private val networkProgress: MutableMap<Attachment, Progress>
|
||||
private val compressionProgress: MutableMap<Attachment, Progress>
|
||||
private val debouncer: ThrottledDebouncer = ThrottledDebouncer(8) // frame time for 120 Hz
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.transfer_controls_view, this)
|
||||
isLongClickable = false
|
||||
visibility = GONE
|
||||
layoutTransition = LayoutTransition()
|
||||
networkProgress = HashMap()
|
||||
compressionProgress = HashMap()
|
||||
primaryProgressView = findViewById(R.id.primary_progress_view)
|
||||
secondaryProgressView = findViewById(R.id.secondary_progress_view)
|
||||
playVideoButton = findViewById(R.id.play_video_button)
|
||||
downloadDetails = findViewById(R.id.secondary_background)
|
||||
downloadDetailsText = findViewById(R.id.download_details_text)
|
||||
secondaryViewSpace = findViewById(R.id.secondary_view_space)
|
||||
primaryDetailsText = findViewById(R.id.primary_details_text)
|
||||
}
|
||||
|
||||
override fun setFocusable(focusable: Boolean) {
|
||||
super.setFocusable(focusable)
|
||||
progressView.isFocusable = focusable
|
||||
if (playVideoButton.visibility == VISIBLE) {
|
||||
playVideoButton.isFocusable = focusable
|
||||
}
|
||||
}
|
||||
|
||||
override fun setClickable(clickable: Boolean) {
|
||||
super.setClickable(clickable)
|
||||
secondaryProgressView.isClickable = secondaryProgressView.visible && clickable
|
||||
primaryProgressView.isClickable = primaryProgressView.visible && clickable
|
||||
playVideoButton.isClickable = playVideoButton.visible && clickable
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
if (!EventBus.getDefault().isRegistered(this)) EventBus.getDefault().register(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
EventBus.getDefault().unregister(this)
|
||||
}
|
||||
|
||||
fun setSlide(slides: Slide) {
|
||||
setSlides(listOf(slides))
|
||||
}
|
||||
|
||||
fun setSlides(slides: List<Slide>) {
|
||||
require(slides.isNotEmpty()) { "Must provide at least one slide." }
|
||||
this.slides = slides
|
||||
if (!isUpdateToExistingSet(slides)) {
|
||||
networkProgress.clear()
|
||||
compressionProgress.clear()
|
||||
slides.forEach { networkProgress[it.asAttachment()] = Progress(0L, it.fileSize) }
|
||||
}
|
||||
var allStreamableOrDone = true
|
||||
for (slide in slides) {
|
||||
val attachment = slide.asAttachment()
|
||||
if (attachment.transferState == AttachmentTable.TRANSFER_PROGRESS_DONE) {
|
||||
networkProgress[attachment] = Progress(1L, attachment.size)
|
||||
} else if (!MediaUtil.isInstantVideoSupported(slide)) {
|
||||
allStreamableOrDone = false
|
||||
}
|
||||
}
|
||||
playableWhileDownloading = allStreamableOrDone
|
||||
setPlayableWhileDownloading(playableWhileDownloading)
|
||||
val uploading = slides.any { it.asAttachment().uploadTimestamp == 0L }
|
||||
when (getTransferState(slides)) {
|
||||
AttachmentTable.TRANSFER_PROGRESS_STARTED -> showProgressSpinner(calculateProgress(), uploading)
|
||||
AttachmentTable.TRANSFER_PROGRESS_PENDING -> {
|
||||
updateDownloadText()
|
||||
progressView.setStopped(false)
|
||||
this.visible = true
|
||||
}
|
||||
|
||||
AttachmentTable.TRANSFER_PROGRESS_FAILED -> {
|
||||
downloadDetailsText.setText(R.string.NetworkFailure__retry)
|
||||
progressView.setStopped(false)
|
||||
this.visible = true
|
||||
}
|
||||
|
||||
else -> this.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
private val progressView: TransferProgressView
|
||||
get() = if (playableWhileDownloading) {
|
||||
secondaryProgressView
|
||||
} else {
|
||||
primaryProgressView
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun showProgressSpinner(progress: Float = calculateProgress(), uploading: Boolean = false) {
|
||||
if (uploading || progress == 0f) {
|
||||
progressView.setUploading(progress)
|
||||
} else {
|
||||
progressView.setDownloading(progress)
|
||||
}
|
||||
}
|
||||
|
||||
fun setDownloadClickListener(listener: OnClickListener?) {
|
||||
primaryProgressView.startClickListener = listener
|
||||
secondaryProgressView.startClickListener = listener
|
||||
}
|
||||
|
||||
fun setCancelClickListener(listener: OnClickListener?) {
|
||||
primaryProgressView.cancelClickListener = listener
|
||||
secondaryProgressView.cancelClickListener = listener
|
||||
}
|
||||
|
||||
fun setInstantPlaybackClickListener(onPlayClickedListener: OnClickListener?) {
|
||||
playVideoButton.setOnClickListener(onPlayClickedListener)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
clearAnimation()
|
||||
visibility = GONE
|
||||
if (current.isNotEmpty()) {
|
||||
for (v in current) {
|
||||
v.clearAnimation()
|
||||
v.visibility = GONE
|
||||
}
|
||||
}
|
||||
current.clear()
|
||||
slides = emptyList()
|
||||
}
|
||||
|
||||
fun setShowDownloadText(showDownloadText: Boolean) {
|
||||
this.showDownloadText = showDownloadText
|
||||
updateDownloadText()
|
||||
}
|
||||
|
||||
private fun isUpdateToExistingSet(slides: List<Slide>): Boolean {
|
||||
if (slides.size != networkProgress.size) {
|
||||
return false
|
||||
}
|
||||
for (slide in slides) {
|
||||
if (!networkProgress.containsKey(slide.asAttachment())) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun updateDownloadText() {
|
||||
val byteCount = slides.sumOf { it.asAttachment().size }
|
||||
downloadDetailsText.text = Formatter.formatShortFileSize(context, byteCount)
|
||||
downloadDetailsText.invalidate()
|
||||
|
||||
if (slides.size > 1) {
|
||||
val downloadCount = Stream.of(slides).reduce(0) { count: Int, slide: Slide -> if (slide.transferState != AttachmentTable.TRANSFER_PROGRESS_DONE) count + 1 else count }
|
||||
primaryDetailsText.text = context.resources.getQuantityString(R.plurals.TransferControlView_n_items, downloadCount, downloadCount)
|
||||
primaryDetailsText.visible = showDownloadText
|
||||
} else {
|
||||
primaryDetailsText.text = ""
|
||||
primaryDetailsText.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun updateDownloadProgressText(isCompression: Boolean) {
|
||||
val context = context
|
||||
val progress = if (isCompression) compressionProgress.values.sumOf { it.completed } else networkProgress.values.sumOf { it.completed }
|
||||
val total = if (isCompression) compressionProgress.values.sumOf { it.total } else networkProgress.values.sumOf { it.total }
|
||||
val progressText = Formatter.formatShortFileSize(context, progress)
|
||||
val totalText = Formatter.formatShortFileSize(context, total)
|
||||
downloadDetailsText.text = "$progressText/$totalText"
|
||||
downloadDetailsText.invalidate()
|
||||
}
|
||||
|
||||
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||
fun onEventAsync(event: PartProgressEvent) {
|
||||
val attachment = event.attachment
|
||||
if (networkProgress.containsKey(attachment)) {
|
||||
val proportionCompleted = event.progress.toFloat() / event.total
|
||||
if (event.type == PartProgressEvent.Type.COMPRESSION) {
|
||||
compressionProgress[attachment] = Progress.fromEvent(event)
|
||||
} else {
|
||||
networkProgress[attachment] = Progress.fromEvent(event)
|
||||
}
|
||||
debouncer.publish {
|
||||
val progress = calculateProgress()
|
||||
if (attachment.uploadTimestamp == 0L) {
|
||||
progressView.setUploading(progress)
|
||||
} else {
|
||||
progressView.setDownloading(progress)
|
||||
}
|
||||
updateDownloadProgressText(event.type == PartProgressEvent.Type.COMPRESSION)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setPlayableWhileDownloading(playableWhileDownloading: Boolean) {
|
||||
playVideoButton.visible = playableWhileDownloading
|
||||
secondaryProgressView.visible = playableWhileDownloading
|
||||
secondaryViewSpace.visible = !playableWhileDownloading // exists because constraint layout was being very weird about margins and this was the only way
|
||||
primaryProgressView.visibility = if (playableWhileDownloading) INVISIBLE else VISIBLE
|
||||
val textPadding = if (playableWhileDownloading) 0 else context.resources.getDimensionPixelSize(R.dimen.transfer_control_view_progressbar_to_textview_margin)
|
||||
ViewUtil.setPaddingStart(downloadDetailsText, textPadding)
|
||||
}
|
||||
|
||||
private fun calculateProgress(): Float {
|
||||
val totalDownloadProgress: Float = networkProgress.values.map { it.completed.toFloat() / it.total }.sum()
|
||||
val totalCompressionProgress: Float = compressionProgress.values.map { it.completed.toFloat() / it.total }.sum()
|
||||
val weightedProgress = UPLOAD_TASK_WEIGHT * totalDownloadProgress + COMPRESSION_TASK_WEIGHT * totalCompressionProgress
|
||||
val weightedTotal = (UPLOAD_TASK_WEIGHT * networkProgress.size + COMPRESSION_TASK_WEIGHT * compressionProgress.size).toFloat()
|
||||
return weightedProgress / weightedTotal
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "TransferControlView"
|
||||
private const val UPLOAD_TASK_WEIGHT = 1
|
||||
|
||||
/**
|
||||
* A weighting compared to [.UPLOAD_TASK_WEIGHT]
|
||||
*/
|
||||
private const val COMPRESSION_TASK_WEIGHT = 3
|
||||
|
||||
@JvmStatic
|
||||
fun getTransferState(slides: List<Slide>): Int {
|
||||
var transferState = AttachmentTable.TRANSFER_PROGRESS_DONE
|
||||
var allFailed = true
|
||||
for (slide in slides) {
|
||||
if (slide.transferState != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE) {
|
||||
allFailed = false
|
||||
transferState = if (slide.transferState == AttachmentTable.TRANSFER_PROGRESS_PENDING && transferState == AttachmentTable.TRANSFER_PROGRESS_DONE) {
|
||||
slide.transferState
|
||||
} else {
|
||||
transferState.coerceAtLeast(slide.transferState)
|
||||
}
|
||||
}
|
||||
}
|
||||
return if (allFailed) AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE else transferState
|
||||
}
|
||||
}
|
||||
|
||||
data class Progress(val completed: Long, val total: Long) {
|
||||
companion object {
|
||||
fun fromEvent(event: PartProgressEvent): Progress {
|
||||
return Progress(event.progress, event.total)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.transfercontrols
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.graphics.RectF
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.withTranslation
|
||||
import org.signal.core.util.dp
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class TransferProgressView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0,
|
||||
defStyleRes: Int = 0
|
||||
) : View(context, attrs, defStyleAttr, defStyleRes) {
|
||||
companion object {
|
||||
const val TAG = "TransferProgressView"
|
||||
private const val PROGRESS_ARC_STROKE_WIDTH = 2f
|
||||
private const val ICON_INSET_PERCENT = 0.2f
|
||||
}
|
||||
|
||||
private val progressRect = RectF()
|
||||
private val stopIconRect = RectF()
|
||||
private val progressPaint = progressPaint()
|
||||
private val stopIconPaint = stopIconPaint()
|
||||
private val trackPaint = trackPaint()
|
||||
|
||||
private var progressPercent = 0f
|
||||
private var currentState = State.READY_TO_DOWNLOAD
|
||||
|
||||
private val downloadDrawable = ContextCompat.getDrawable(context, R.drawable.ic_arrow_down_24)
|
||||
private val uploadDrawable = ContextCompat.getDrawable(context, R.drawable.ic_arrow_up_16)
|
||||
|
||||
var startClickListener: OnClickListener? = null
|
||||
var cancelClickListener: OnClickListener? = null
|
||||
|
||||
init {
|
||||
val tint = ContextCompat.getColor(context, R.color.signal_colorOnCustom)
|
||||
val filter = PorterDuffColorFilter(tint, PorterDuff.Mode.SRC_ATOP)
|
||||
downloadDrawable?.colorFilter = filter
|
||||
uploadDrawable?.colorFilter = filter
|
||||
}
|
||||
|
||||
override fun requestLayout() {
|
||||
super.requestLayout()
|
||||
Log.d(TAG, "Requesting new layout.", Exception())
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
when (currentState) {
|
||||
State.IN_PROGRESS_CANCELABLE, State.IN_PROGRESS_NON_CANCELABLE -> drawProgress(canvas, progressPercent)
|
||||
State.READY_TO_UPLOAD -> sizeAndDrawDrawable(canvas, uploadDrawable)
|
||||
State.READY_TO_DOWNLOAD -> sizeAndDrawDrawable(canvas, downloadDrawable)
|
||||
}
|
||||
}
|
||||
|
||||
fun setDownloading(progress: Float) {
|
||||
if (currentState != State.IN_PROGRESS_CANCELABLE) {
|
||||
currentState = State.IN_PROGRESS_CANCELABLE
|
||||
setOnClickListener(cancelClickListener)
|
||||
}
|
||||
progressPercent = progress
|
||||
}
|
||||
|
||||
fun setUploading(progress: Float) {
|
||||
if (currentState != State.IN_PROGRESS_NON_CANCELABLE) {
|
||||
currentState = State.IN_PROGRESS_NON_CANCELABLE
|
||||
setOnClickListener(null)
|
||||
}
|
||||
progressPercent = progress
|
||||
}
|
||||
|
||||
fun setStopped(isUpload: Boolean) {
|
||||
val newState = if (isUpload) State.READY_TO_UPLOAD else State.READY_TO_DOWNLOAD
|
||||
if (currentState != newState) {
|
||||
currentState = newState
|
||||
setOnClickListener(startClickListener)
|
||||
}
|
||||
progressPercent = 0f
|
||||
}
|
||||
|
||||
private fun drawProgress(canvas: Canvas, progressPercent: Float) {
|
||||
if (currentState == State.IN_PROGRESS_CANCELABLE) {
|
||||
val miniIcon = height < 32.dp
|
||||
val stopIconCornerRadius = if (miniIcon) 1f.dp else 4f.dp
|
||||
val iconSize: Float = if (miniIcon) 6.6f.dp else 16f.dp
|
||||
stopIconRect.set(0f, 0f, iconSize, iconSize)
|
||||
|
||||
canvas.withTranslation(width / 2 - (iconSize / 2), height / 2 - (iconSize / 2)) {
|
||||
drawRoundRect(stopIconRect, stopIconCornerRadius, stopIconCornerRadius, stopIconPaint)
|
||||
}
|
||||
}
|
||||
|
||||
val widthDp = PROGRESS_ARC_STROKE_WIDTH.dp
|
||||
val inset = 2.dp
|
||||
progressRect.top = widthDp + inset
|
||||
progressRect.left = widthDp + inset
|
||||
progressRect.right = (width - widthDp) - inset
|
||||
progressRect.bottom = (height - widthDp) - inset
|
||||
|
||||
canvas.drawArc(progressRect, 0f, 360f, false, trackPaint)
|
||||
canvas.drawArc(progressRect, 270f, 360f * progressPercent, false, progressPaint)
|
||||
}
|
||||
|
||||
private fun stopIconPaint(): Paint {
|
||||
val stopIconPaint = Paint()
|
||||
stopIconPaint.color = ContextCompat.getColor(context, R.color.signal_colorOnCustom)
|
||||
stopIconPaint.isAntiAlias = true
|
||||
stopIconPaint.style = Paint.Style.FILL
|
||||
return stopIconPaint
|
||||
}
|
||||
|
||||
private fun trackPaint(): Paint {
|
||||
val trackPaint = Paint()
|
||||
trackPaint.color = ContextCompat.getColor(context, R.color.signal_colorTransparent2)
|
||||
trackPaint.isAntiAlias = true
|
||||
trackPaint.style = Paint.Style.STROKE
|
||||
trackPaint.strokeWidth = PROGRESS_ARC_STROKE_WIDTH.dp
|
||||
return trackPaint
|
||||
}
|
||||
|
||||
private fun progressPaint(): Paint {
|
||||
val progressPaint = Paint()
|
||||
progressPaint.color = ContextCompat.getColor(context, R.color.signal_colorOnCustom)
|
||||
progressPaint.isAntiAlias = true
|
||||
progressPaint.style = Paint.Style.STROKE
|
||||
progressPaint.strokeWidth = PROGRESS_ARC_STROKE_WIDTH.dp
|
||||
return progressPaint
|
||||
}
|
||||
|
||||
private fun sizeAndDrawDrawable(canvas: Canvas, drawable: Drawable?) {
|
||||
if (drawable == null) {
|
||||
Log.w(TAG, "Could not load icon for $currentState")
|
||||
return
|
||||
}
|
||||
|
||||
drawable.setBounds(
|
||||
(width * ICON_INSET_PERCENT).roundToInt(),
|
||||
(height * ICON_INSET_PERCENT).roundToInt(),
|
||||
(width * (1 - ICON_INSET_PERCENT)).roundToInt(),
|
||||
(height * (1 - ICON_INSET_PERCENT)).roundToInt()
|
||||
)
|
||||
|
||||
drawable.draw(canvas)
|
||||
}
|
||||
|
||||
private enum class State {
|
||||
IN_PROGRESS_CANCELABLE,
|
||||
IN_PROGRESS_NON_CANCELABLE,
|
||||
READY_TO_UPLOAD,
|
||||
READY_TO_DOWNLOAD
|
||||
}
|
||||
}
|
||||
@@ -113,10 +113,18 @@ public class VoiceNotePlaybackService extends MediaSessionService {
|
||||
private class VoiceNotePlayerEventListener implements Player.Listener {
|
||||
private int previousPlaybackState = player.getPlaybackState();
|
||||
|
||||
@Override
|
||||
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
|
||||
onPlaybackStateChanged(playWhenReady, player.getPlaybackState());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackStateChanged(int playbackState) {
|
||||
Log.d(TAG, "[onPlaybackStateChanged] playbackState: " + playbackState);
|
||||
boolean playWhenReady = player.getPlayWhenReady();
|
||||
onPlaybackStateChanged(player.getPlayWhenReady(), playbackState);
|
||||
}
|
||||
|
||||
private void onPlaybackStateChanged(boolean playWhenReady, int playbackState) {
|
||||
Log.d(TAG, "playWhenReady: " + playWhenReady + "\nplaybackState: " + playbackState);
|
||||
switch (playbackState) {
|
||||
case Player.STATE_BUFFERING:
|
||||
case Player.STATE_READY:
|
||||
@@ -162,10 +170,9 @@ public class VoiceNotePlaybackService extends MediaSessionService {
|
||||
player.seekTo(mediaItemIndex, 1);
|
||||
player.setPlayWhenReady(true);
|
||||
}
|
||||
} else if (reason == Player.DISCONTINUITY_REASON_SEEK) {
|
||||
player.setPlayWhenReady(true);
|
||||
}
|
||||
|
||||
|
||||
boolean isWithinThreshold = mediaItemIndex < LOAD_MORE_THRESHOLD ||
|
||||
mediaItemIndex + LOAD_MORE_THRESHOLD >= player.getMediaItemCount();
|
||||
|
||||
|
||||
@@ -102,8 +102,6 @@ class CallLinkInfoSheet : ComposeBottomSheetDialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun isDarkTheme(): Boolean = true
|
||||
|
||||
private val webRtcCallViewModel: WebRtcCallViewModel by activityViewModels()
|
||||
private val callLinkDetailsViewModel: CallLinkDetailsViewModel by viewModels(factoryProducer = {
|
||||
CallLinkDetailsViewModel.Factory(BundleCompat.getParcelable(requireArguments(), CALL_LINK_ROOM_ID, CallLinkRoomId::class.java)!!)
|
||||
|
||||
@@ -53,8 +53,6 @@ class PendingParticipantsView @JvmOverloads constructor(
|
||||
|
||||
val firstRecipient: Recipient = unresolvedPendingParticipants.first()
|
||||
avatar.setAvatar(firstRecipient)
|
||||
avatar.setOnClickListener { listener?.onLaunchRecipientSheet(firstRecipient) }
|
||||
|
||||
name.text = firstRecipient.getShortDisplayName(context)
|
||||
|
||||
allow.setOnClickListener { listener?.onAllowPendingRecipient(firstRecipient) }
|
||||
@@ -72,11 +70,6 @@ class PendingParticipantsView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
/**
|
||||
* Display the sheet containing the request for the top level participant
|
||||
*/
|
||||
fun onLaunchRecipientSheet(pendingRecipient: Recipient)
|
||||
|
||||
/**
|
||||
* Given recipient should be admitted to the call
|
||||
*/
|
||||
|
||||
@@ -30,7 +30,7 @@ class WebRtcAudioPicker31(private val audioOutputChangedListener: OnAudioOutputC
|
||||
return null
|
||||
}
|
||||
|
||||
val devices: List<AudioOutputOption> = am.availableCommunicationDevices.map { AudioOutputOption(it.toFriendlyName(fragmentActivity).toString(), AudioDeviceMapping.fromPlatformType(it.type), it.id) }.distinctBy { it.deviceType.name }.filterNot { it.deviceType == SignalAudioManager.AudioDevice.NONE }
|
||||
val devices: List<AudioOutputOption> = am.availableCommunicationDevices.map { AudioOutputOption(it.toFriendlyName(fragmentActivity).toString(), AudioDeviceMapping.fromPlatformType(it.type), it.id) }.distinct().filterNot { it.deviceType == SignalAudioManager.AudioDevice.NONE }
|
||||
val currentDeviceId = am.communicationDevice?.id ?: -1
|
||||
if (devices.size < threshold) {
|
||||
Log.d(TAG, "Only found $devices devices,\nnot showing picker.")
|
||||
|
||||
@@ -600,15 +600,7 @@ public class WebRtcCallView extends ConstraintLayout {
|
||||
public void setStatus(@Nullable String status) {
|
||||
ThreadUtil.assertMainThread();
|
||||
this.status.setText(status);
|
||||
try {
|
||||
// Toolbar's subtitle view sometimes already has a parent somehow,
|
||||
// so we clear it out first so that it removes the view from its parent.
|
||||
// In addition, we catch the ISE to prevent a crash.
|
||||
collapsedToolbar.setSubtitle(null);
|
||||
collapsedToolbar.setSubtitle(status);
|
||||
} catch (IllegalStateException e) {
|
||||
Log.w(TAG, "IllegalStateException trying to set status on collapsed Toolbar.");
|
||||
}
|
||||
collapsedToolbar.setSubtitle(status);
|
||||
}
|
||||
|
||||
private void setStatus(@StringRes int statusRes) {
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.thoughtcrime.securesms.components.webrtc.requests
|
||||
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.contacts.paged.GroupsInCommon
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
class CallLinkIncomingRequestRepository {
|
||||
|
||||
fun getGroupsInCommon(recipientId: RecipientId): Observable<GroupsInCommon> {
|
||||
return Recipient.observable(recipientId).flatMapSingle { recipient ->
|
||||
if (recipient.hasGroupsInCommon()) {
|
||||
Single.fromCallable {
|
||||
val groupsInCommon = SignalDatabase.groups.getGroupsContainingMember(recipient.id, true)
|
||||
val total = groupsInCommon.size
|
||||
val names = groupsInCommon.take(2).map { it.title!! }
|
||||
GroupsInCommon(total, names)
|
||||
}.observeOn(Schedulers.io())
|
||||
} else {
|
||||
Single.just(GroupsInCommon(0, listOf()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.webrtc.requests
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rxjava3.subscribeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import org.signal.core.ui.BottomSheets
|
||||
import org.signal.core.ui.Dividers
|
||||
import org.signal.core.ui.Rows
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.signal.core.util.getParcelableCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
/**
|
||||
* Displayed when the user presses the user avatar in the call link join request
|
||||
* bar.
|
||||
*/
|
||||
class CallLinkIncomingRequestSheet : ComposeBottomSheetDialogFragment() {
|
||||
|
||||
companion object {
|
||||
private const val RECIPIENT_ID = "recipient_id"
|
||||
|
||||
@JvmStatic
|
||||
fun show(fragmentManager: FragmentManager, recipientId: RecipientId) {
|
||||
CallLinkIncomingRequestSheet().apply {
|
||||
arguments = bundleOf(
|
||||
RECIPIENT_ID to recipientId
|
||||
)
|
||||
}.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isDarkTheme(): Boolean = true
|
||||
|
||||
private val recipientId: RecipientId by lazy {
|
||||
requireArguments().getParcelableCompat(RECIPIENT_ID, RecipientId::class.java)!!
|
||||
}
|
||||
|
||||
private val viewModel by viewModel {
|
||||
CallLinkIncomingRequestViewModel(recipientId)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun SheetContent() {
|
||||
val state = viewModel.observeState(LocalContext.current).subscribeAsState(initial = CallLinkIncomingRequestState())
|
||||
if (state.value.recipient == Recipient.UNKNOWN) {
|
||||
return
|
||||
}
|
||||
|
||||
CallLinkIncomingRequestSheetContent(
|
||||
state = state.value,
|
||||
onApproveEntry = this::onApproveEntry,
|
||||
onDenyEntry = this::onDenyEntry
|
||||
)
|
||||
}
|
||||
|
||||
private fun onApproveEntry() {
|
||||
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestAccepted(recipientId)
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
|
||||
private fun onDenyEntry() {
|
||||
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestRejected(recipientId)
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun CallLinkIncomingRequestSheetContentPreview() {
|
||||
SignalTheme(isDarkMode = true) {
|
||||
Surface {
|
||||
CallLinkIncomingRequestSheetContent(
|
||||
state = CallLinkIncomingRequestState(
|
||||
name = "Miles Morales",
|
||||
subtitle = "+1 (555) 555-5555",
|
||||
groupsInCommon = "Member of Webheads",
|
||||
isSystemContact = true
|
||||
),
|
||||
onApproveEntry = {},
|
||||
onDenyEntry = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CallLinkIncomingRequestSheetContent(
|
||||
state: CallLinkIncomingRequestState,
|
||||
onApproveEntry: () -> Unit,
|
||||
onDenyEntry: () -> Unit
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
item { BottomSheets.Handle() }
|
||||
item { Avatar(state.recipient) }
|
||||
item {
|
||||
Title(
|
||||
recipientName = state.name,
|
||||
isSystemContact = state.isSystemContact
|
||||
)
|
||||
}
|
||||
|
||||
if (state.subtitle.isNotEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = state.subtitle,
|
||||
modifier = Modifier.padding(4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.groupsInCommon.isNotEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = state.groupsInCommon,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(6.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Dividers.Default()
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.CallLinkIncomingRequestSheet__approve_entry),
|
||||
icon = ImageVector.vectorResource(R.drawable.symbol_check_circle_24),
|
||||
onClick = onApproveEntry
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.CallLinkIncomingRequestSheet__deny_entry),
|
||||
icon = ImageVector.vectorResource(R.drawable.symbol_x_circle_24),
|
||||
onClick = onDenyEntry
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.size(32.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Avatar(
|
||||
recipient: Recipient
|
||||
) {
|
||||
if (LocalInspectionMode.current) {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.padding(top = 13.dp)
|
||||
.size(80.dp)
|
||||
.background(color = Color.Red, shape = CircleShape)
|
||||
)
|
||||
} else {
|
||||
AndroidView(
|
||||
factory = ::AvatarImageView,
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.padding(top = 13.dp)
|
||||
) {
|
||||
it.setAvatarUsingProfile(recipient)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Title(
|
||||
recipientName: String,
|
||||
isSystemContact: Boolean
|
||||
) {
|
||||
if (isSystemContact) {
|
||||
Row(modifier = Modifier.padding(top = 12.dp)) {
|
||||
Text(
|
||||
text = recipientName,
|
||||
style = MaterialTheme.typography.headlineMedium
|
||||
)
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.symbol_person_circle_24),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(start = 6.dp)
|
||||
.align(CenterVertically)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = recipientName,
|
||||
style = MaterialTheme.typography.headlineMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.webrtc.requests
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
data class CallLinkIncomingRequestState(
|
||||
val recipient: Recipient = Recipient.UNKNOWN,
|
||||
val name: String = "",
|
||||
val isSystemContact: Boolean = false,
|
||||
val subtitle: String = "",
|
||||
@Stable val groupsInCommon: String = ""
|
||||
)
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.webrtc.requests
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.core.BackpressureStrategy
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
|
||||
class CallLinkIncomingRequestViewModel(
|
||||
private val recipientId: RecipientId
|
||||
) : ViewModel() {
|
||||
|
||||
private val repository = CallLinkIncomingRequestRepository()
|
||||
private val store = RxStore(CallLinkIncomingRequestState())
|
||||
private val disposables = CompositeDisposable().apply {
|
||||
add(store)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.dispose()
|
||||
}
|
||||
|
||||
fun observeState(context: Context): Flowable<CallLinkIncomingRequestState> {
|
||||
disposables += store.update(Recipient.observable(recipientId).toFlowable(BackpressureStrategy.LATEST)) { r, s ->
|
||||
s.copy(
|
||||
recipient = r,
|
||||
name = r.getShortDisplayName(context),
|
||||
subtitle = r.e164.orElse(""),
|
||||
isSystemContact = r.isSystemContact
|
||||
)
|
||||
}
|
||||
|
||||
disposables += store.update(repository.getGroupsInCommon(recipientId).toFlowable(BackpressureStrategy.LATEST)) { g, s ->
|
||||
s.copy(groupsInCommon = g.toDisplayText(context))
|
||||
}
|
||||
|
||||
return store.stateFlowable
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
@@ -16,14 +17,12 @@ import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
|
||||
abstract class ComposeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFragment() {
|
||||
|
||||
protected open fun isDarkTheme(): Boolean = DynamicTheme.isDarkTheme(requireContext())
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
return ComposeView(requireContext()).apply {
|
||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||
setContent {
|
||||
SignalTheme(
|
||||
isDarkMode = isDarkTheme()
|
||||
isDarkMode = DynamicTheme.isDarkTheme(LocalContext.current)
|
||||
) {
|
||||
Surface(shape = RoundedCornerShape(18.dp, 18.dp)) {
|
||||
SheetContent()
|
||||
|
||||
@@ -9,7 +9,6 @@ import androidx.lifecycle.ViewModelProvider
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterRequest
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits
|
||||
@@ -51,10 +50,6 @@ class ContactSearchMediator(
|
||||
arbitraryRepository: ArbitraryRepository? = null
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ContactSearchMediator::class.java)
|
||||
}
|
||||
|
||||
private val queryDebouncer = Debouncer(300, TimeUnit.MILLISECONDS)
|
||||
|
||||
private val viewModel: ContactSearchViewModel = ViewModelProvider(
|
||||
@@ -75,17 +70,14 @@ class ContactSearchMediator(
|
||||
displayOptions = displayOptions,
|
||||
callbacks = object : ContactSearchAdapter.ClickCallbacks {
|
||||
override fun onStoryClicked(view: View, story: ContactSearchData.Story, isSelected: Boolean) {
|
||||
Log.d(TAG, "onStoryClicked() Recipient: ${story.recipient.id}")
|
||||
toggleStorySelection(view, story, isSelected)
|
||||
}
|
||||
|
||||
override fun onKnownRecipientClicked(view: View, knownRecipient: ContactSearchData.KnownRecipient, isSelected: Boolean) {
|
||||
Log.d(TAG, "onKnownRecipientClicked() Recipient: ${knownRecipient.recipient.id}")
|
||||
toggleSelection(view, knownRecipient, isSelected)
|
||||
}
|
||||
|
||||
override fun onExpandClicked(expand: ContactSearchData.Expand) {
|
||||
Log.d(TAG, "onExpandClicked()")
|
||||
viewModel.expandSection(expand.sectionKey)
|
||||
}
|
||||
},
|
||||
@@ -127,7 +119,6 @@ class ContactSearchMediator(
|
||||
}
|
||||
|
||||
fun setKeysSelected(keys: Set<ContactSearchKey>) {
|
||||
Log.d(TAG, "setKeysSelected() Keys: ${keys.map { it.toString() }}")
|
||||
viewModel.setKeysSelected(callbacks.onBeforeContactsSelected(null, keys))
|
||||
}
|
||||
|
||||
@@ -176,11 +167,9 @@ class ContactSearchMediator(
|
||||
|
||||
private fun toggleSelection(view: View, contactSearchData: ContactSearchData, isSelected: Boolean) {
|
||||
return if (isSelected) {
|
||||
Log.d(TAG, "toggleSelection(OFF) ${contactSearchData.contactSearchKey}")
|
||||
callbacks.onContactDeselected(view, contactSearchData.contactSearchKey)
|
||||
viewModel.setKeysNotSelected(setOf(contactSearchData.contactSearchKey))
|
||||
} else {
|
||||
Log.d(TAG, "toggleSelection(ON) ${contactSearchData.contactSearchKey}")
|
||||
viewModel.setKeysSelected(callbacks.onBeforeContactsSelected(view, setOf(contactSearchData.contactSearchKey)))
|
||||
}
|
||||
}
|
||||
@@ -223,13 +212,10 @@ class ContactSearchMediator(
|
||||
|
||||
open class SimpleCallbacks : Callbacks {
|
||||
override fun onBeforeContactsSelected(view: View?, contactSearchKeys: Set<ContactSearchKey>): Set<ContactSearchKey> {
|
||||
Log.d(TAG, "onBeforeContactsSelected() Selecting: ${contactSearchKeys.map { it.toString() }}")
|
||||
return contactSearchKeys
|
||||
}
|
||||
|
||||
override fun onContactDeselected(view: View?, contactSearchKey: ContactSearchKey) {
|
||||
Log.i(TAG, "onContactDeselected() Deselected: $contactSearchKey}")
|
||||
}
|
||||
override fun onContactDeselected(view: View?, contactSearchKey: ContactSearchKey) = Unit
|
||||
override fun onAdapterListCommitted(size: Int) = Unit
|
||||
}
|
||||
|
||||
|
||||
@@ -57,13 +57,9 @@ object ContactDiscovery {
|
||||
}
|
||||
|
||||
if (!SignalStore.registrationValues().isRegistrationComplete) {
|
||||
if (SignalStore.account().isRegistered && SignalStore.svr().lastPinCreateFailed()) {
|
||||
Log.w(TAG, "Registration isn't complete, but only because PIN creation failed. Allowing CDS to continue.")
|
||||
} else {
|
||||
Log.w(TAG, "Registration is not yet complete. Skipping, but running a routine to possibly mark it complete.")
|
||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||
return
|
||||
}
|
||||
Log.w(TAG, "Registration is not yet complete. Skipping, but running a routine to possibly mark it complete.")
|
||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||
return
|
||||
}
|
||||
|
||||
refreshRecipients(
|
||||
|
||||
@@ -9,7 +9,7 @@ import org.whispersystems.signalservice.api.InvalidMessageStructureException;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
import org.whispersystems.signalservice.api.util.AttachmentPointerUtil;
|
||||
import org.whispersystems.signalservice.internal.push.DataMessage;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
@@ -125,47 +125,47 @@ public class ContactModelMapper {
|
||||
return new Contact(name, sharedContact.getOrganization().orElse(null), phoneNumbers, emails, postalAddresses, avatar);
|
||||
}
|
||||
|
||||
public static Contact remoteToLocal(@NonNull DataMessage.Contact contact) {
|
||||
Name name = new Name(contact.name.displayName,
|
||||
contact.name.givenName,
|
||||
contact.name.familyName,
|
||||
contact.name.prefix,
|
||||
contact.name.suffix,
|
||||
contact.name.middleName);
|
||||
public static Contact remoteToLocal(@NonNull SignalServiceProtos.DataMessage.Contact contact) {
|
||||
Name name = new Name(contact.getName().getDisplayName(),
|
||||
contact.getName().getGivenName(),
|
||||
contact.getName().getFamilyName(),
|
||||
contact.getName().getPrefix(),
|
||||
contact.getName().getSuffix(),
|
||||
contact.getName().getMiddleName());
|
||||
|
||||
List<Phone> phoneNumbers = new ArrayList<>(contact.number.size());
|
||||
for (DataMessage.Contact.Phone phone : contact.number) {
|
||||
phoneNumbers.add(new Phone(phone.value_,
|
||||
remoteToLocalType(phone.type),
|
||||
phone.label));
|
||||
List<Phone> phoneNumbers = new ArrayList<>(contact.getNumberCount());
|
||||
for (SignalServiceProtos.DataMessage.Contact.Phone phone : contact.getNumberList()) {
|
||||
phoneNumbers.add(new Phone(phone.getValue(),
|
||||
remoteToLocalType(phone.getType()),
|
||||
phone.getLabel()));
|
||||
}
|
||||
|
||||
List<Email> emails = new ArrayList<>(contact.email.size());
|
||||
for (DataMessage.Contact.Email email : contact.email) {
|
||||
emails.add(new Email(email.value_,
|
||||
remoteToLocalType(email.type),
|
||||
email.label));
|
||||
List<Email> emails = new ArrayList<>(contact.getEmailCount());
|
||||
for (SignalServiceProtos.DataMessage.Contact.Email email : contact.getEmailList()) {
|
||||
emails.add(new Email(email.getValue(),
|
||||
remoteToLocalType(email.getType()),
|
||||
email.getLabel()));
|
||||
}
|
||||
|
||||
List<PostalAddress> postalAddresses = new ArrayList<>(contact.address.size());
|
||||
for (DataMessage.Contact.PostalAddress postalAddress : contact.address) {
|
||||
postalAddresses.add(new PostalAddress(remoteToLocalType(postalAddress.type),
|
||||
postalAddress.label,
|
||||
postalAddress.street,
|
||||
postalAddress.pobox,
|
||||
postalAddress.neighborhood,
|
||||
postalAddress.city,
|
||||
postalAddress.region,
|
||||
postalAddress.postcode,
|
||||
postalAddress.country));
|
||||
List<PostalAddress> postalAddresses = new ArrayList<>(contact.getAddressCount());
|
||||
for (SignalServiceProtos.DataMessage.Contact.PostalAddress postalAddress : contact.getAddressList()) {
|
||||
postalAddresses.add(new PostalAddress(remoteToLocalType(postalAddress.getType()),
|
||||
postalAddress.getLabel(),
|
||||
postalAddress.getStreet(),
|
||||
postalAddress.getPobox(),
|
||||
postalAddress.getNeighborhood(),
|
||||
postalAddress.getCity(),
|
||||
postalAddress.getRegion(),
|
||||
postalAddress.getPostcode(),
|
||||
postalAddress.getCountry()));
|
||||
}
|
||||
|
||||
Avatar avatar = null;
|
||||
if (contact.avatar != null) {
|
||||
if (contact.hasAvatar()) {
|
||||
try {
|
||||
SignalServiceAttachmentPointer attachmentPointer = AttachmentPointerUtil.createSignalAttachmentPointer(contact.avatar.avatar);
|
||||
SignalServiceAttachmentPointer attachmentPointer = AttachmentPointerUtil.createSignalAttachmentPointer(contact.getAvatar().getAvatar());
|
||||
Attachment attachment = PointerAttachment.forPointer(Optional.of(attachmentPointer.asPointer())).get();
|
||||
boolean isProfile = contact.avatar.isProfile;
|
||||
boolean isProfile = contact.getAvatar().getIsProfile();
|
||||
|
||||
avatar = new Avatar(null, attachment, isProfile);
|
||||
} catch (InvalidMessageStructureException e) {
|
||||
@@ -173,7 +173,7 @@ public class ContactModelMapper {
|
||||
}
|
||||
}
|
||||
|
||||
return new Contact(name, contact.organization, phoneNumbers, emails, postalAddresses, avatar);
|
||||
return new Contact(name, contact.getOrganization(), phoneNumbers, emails, postalAddresses, avatar);
|
||||
}
|
||||
|
||||
private static Phone.Type remoteToLocalType(SharedContact.Phone.Type type) {
|
||||
@@ -185,7 +185,7 @@ public class ContactModelMapper {
|
||||
}
|
||||
}
|
||||
|
||||
private static Phone.Type remoteToLocalType(DataMessage.Contact.Phone.Type type) {
|
||||
private static Phone.Type remoteToLocalType(SignalServiceProtos.DataMessage.Contact.Phone.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return Phone.Type.HOME;
|
||||
case MOBILE: return Phone.Type.MOBILE;
|
||||
@@ -203,7 +203,7 @@ public class ContactModelMapper {
|
||||
}
|
||||
}
|
||||
|
||||
private static Email.Type remoteToLocalType(DataMessage.Contact.Email.Type type) {
|
||||
private static Email.Type remoteToLocalType(SignalServiceProtos.DataMessage.Contact.Email.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return Email.Type.HOME;
|
||||
case MOBILE: return Email.Type.MOBILE;
|
||||
@@ -220,7 +220,7 @@ public class ContactModelMapper {
|
||||
}
|
||||
}
|
||||
|
||||
private static PostalAddress.Type remoteToLocalType(DataMessage.Contact.PostalAddress.Type type) {
|
||||
private static PostalAddress.Type remoteToLocalType(SignalServiceProtos.DataMessage.Contact.PostalAddress.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return PostalAddress.Type.HOME;
|
||||
case WORK: return PostalAddress.Type.WORK;
|
||||
|
||||
@@ -114,7 +114,8 @@ public class ConversationAdapter
|
||||
private ConversationMessage inlineContent;
|
||||
private Colorizer colorizer;
|
||||
private boolean isTypingViewEnabled;
|
||||
private ConversationItemDisplayMode displayMode;
|
||||
private ConversationItemDisplayMode condensedMode;
|
||||
private boolean scheduledMessagesMode;
|
||||
private PulseRequest pulseRequest;
|
||||
|
||||
public ConversationAdapter(@NonNull Context context,
|
||||
@@ -251,7 +252,12 @@ public class ConversationAdapter
|
||||
}
|
||||
|
||||
public void setCondensedMode(ConversationItemDisplayMode condensedMode) {
|
||||
this.displayMode = condensedMode;
|
||||
this.condensedMode = condensedMode;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setScheduledMessagesMode(boolean scheduledMessagesMode) {
|
||||
this.scheduledMessagesMode = scheduledMessagesMode;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@@ -270,7 +276,7 @@ public class ConversationAdapter
|
||||
ConversationMessage previousMessage = adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1) ? getItem(adapterPosition + 1) : null;
|
||||
ConversationMessage nextMessage = adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1) ? getItem(adapterPosition - 1) : null;
|
||||
|
||||
ConversationItemDisplayMode itemDisplayMode = displayMode != null ? displayMode : ConversationItemDisplayMode.Standard.INSTANCE;
|
||||
ConversationItemDisplayMode displayMode = condensedMode != null ? condensedMode : ConversationItemDisplayMode.STANDARD;
|
||||
|
||||
conversationViewHolder.getBindable().bind(lifecycleOwner,
|
||||
conversationMessage,
|
||||
@@ -282,11 +288,11 @@ public class ConversationAdapter
|
||||
conversationMessage.getThreadRecipient(),
|
||||
searchQuery,
|
||||
conversationMessage == recordToPulse,
|
||||
hasWallpaper && itemDisplayMode.displayWallpaper(),
|
||||
hasWallpaper && displayMode.displayWallpaper(),
|
||||
isMessageRequestAccepted,
|
||||
conversationMessage == inlineContent,
|
||||
colorizer,
|
||||
itemDisplayMode);
|
||||
displayMode);
|
||||
|
||||
if (conversationMessage == recordToPulse) {
|
||||
recordToPulse = null;
|
||||
@@ -325,9 +331,9 @@ public class ConversationAdapter
|
||||
|
||||
if (conversationMessage == null) return -1;
|
||||
|
||||
if (displayMode.getScheduleMessageMode()) {
|
||||
if (scheduledMessagesMode) {
|
||||
calendar.setTimeInMillis(((MediaMmsMessageRecord) conversationMessage.getMessageRecord()).getScheduledDate());
|
||||
} else if (displayMode == ConversationItemDisplayMode.EditHistory.INSTANCE) {
|
||||
} else if (condensedMode == ConversationItemDisplayMode.EDIT_HISTORY) {
|
||||
calendar.setTimeInMillis(conversationMessage.getMessageRecord().getDateSent());
|
||||
} else {
|
||||
calendar.setTimeInMillis(conversationMessage.getConversationTimestamp());
|
||||
@@ -345,9 +351,9 @@ public class ConversationAdapter
|
||||
Context context = viewHolder.itemView.getContext();
|
||||
ConversationMessage conversationMessage = Objects.requireNonNull(getItem(position));
|
||||
|
||||
if (displayMode.getScheduleMessageMode()) {
|
||||
if (scheduledMessagesMode) {
|
||||
viewHolder.setText(DateUtils.getScheduledMessagesDateHeaderString(viewHolder.itemView.getContext(), locale, ((MediaMmsMessageRecord) conversationMessage.getMessageRecord()).getScheduledDate()));
|
||||
} else if (displayMode == ConversationItemDisplayMode.EditHistory.INSTANCE) {
|
||||
} else if (condensedMode == ConversationItemDisplayMode.EDIT_HISTORY) {
|
||||
viewHolder.setText(DateUtils.getConversationDateHeaderString(viewHolder.itemView.getContext(), locale, conversationMessage.getMessageRecord().getDateSent()));
|
||||
} else {
|
||||
viewHolder.setText(DateUtils.getConversationDateHeaderString(viewHolder.itemView.getContext(), locale, conversationMessage.getConversationTimestamp()));
|
||||
|
||||
@@ -63,16 +63,12 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.signal.core.util.DimensionUnit;
|
||||
import org.signal.core.util.StringUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.ringrtc.CallLinkRootKey;
|
||||
import org.thoughtcrime.securesms.BindableConversationItem;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView;
|
||||
import org.thoughtcrime.securesms.badges.gifts.GiftMessageView;
|
||||
@@ -110,10 +106,8 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.Quote;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicy;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicyEnforcer;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.MmsDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.MmsSendJob;
|
||||
@@ -247,8 +241,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
|
||||
private final PassthroughClickListener passthroughClickListener = new PassthroughClickListener();
|
||||
private final AttachmentDownloadClickListener downloadClickListener = new AttachmentDownloadClickListener();
|
||||
private final PlayVideoClickListener playVideoClickListener = new PlayVideoClickListener();
|
||||
private final AttachmentCancelClickListener attachmentCancelClickListener = new AttachmentCancelClickListener();
|
||||
private final ProgressWheelClickListener progressWheelClickListener = new ProgressWheelClickListener();
|
||||
private final SlideClickPassthroughListener singleDownloadClickListener = new SlideClickPassthroughListener(downloadClickListener);
|
||||
private final SharedContactEventListener sharedContactEventListener = new SharedContactEventListener();
|
||||
private final SharedContactClickListener sharedContactClickListener = new SharedContactClickListener();
|
||||
@@ -526,8 +519,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
return;
|
||||
}
|
||||
|
||||
reactionsView.setBubbleWidth(bodyBubble.getWidth());
|
||||
|
||||
boolean needsMeasure = false;
|
||||
|
||||
if (hasQuote(messageRecord)) {
|
||||
@@ -937,7 +928,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
* Today this is only {@link org.thoughtcrime.securesms.conversation.quotes.MessageQuotesBottomSheet}.
|
||||
*/
|
||||
private boolean isCondensedMode() {
|
||||
return displayMode instanceof ConversationItemDisplayMode.Condensed;
|
||||
return displayMode == ConversationItemDisplayMode.CONDENSED;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1067,10 +1058,10 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
|
||||
if (conversationMessage.getBottomButton() != null) {
|
||||
callToActionStub.get().setVisibility(View.VISIBLE);
|
||||
callToActionStub.get().setText(conversationMessage.getBottomButton().label);
|
||||
callToActionStub.get().setText(conversationMessage.getBottomButton().getLabel());
|
||||
callToActionStub.get().setOnClickListener(v -> {
|
||||
if (eventListener != null) {
|
||||
eventListener.onCallToAction(conversationMessage.getBottomButton().action);
|
||||
eventListener.onCallToAction(conversationMessage.getBottomButton().getAction());
|
||||
}
|
||||
});
|
||||
} else if (callToActionStub.resolved()) {
|
||||
@@ -1162,8 +1153,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
CallLinkRootKey callLinkRootKey = CallLinks.parseUrl(linkPreview.getUrl());
|
||||
if (callLinkRootKey != null) {
|
||||
joinCallLinkStub.setVisibility(View.VISIBLE);
|
||||
joinCallLinkStub.get().setTextColor(messageRecord.isOutgoing() ? R.color.signal_colorOnCustom : R.color.signal_colorPrimary);
|
||||
joinCallLinkStub.get().setStrokeColor(messageRecord.isOutgoing() ? R.color.signal_colorOnCustom : R.color.signal_colorOutline);
|
||||
joinCallLinkStub.get().setTextColor(messageRecord.isOutgoing() ? R.color.signal_colorOnCustom : R.color.signal_colorOnSurface);
|
||||
joinCallLinkStub.get().setJoinClickListener(v -> {
|
||||
if (eventListener != null) {
|
||||
eventListener.onJoinCallLink(callLinkRootKey);
|
||||
@@ -1179,8 +1169,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
mediaThumbnailStub.require().setImageResource(glideRequests, Collections.singletonList(new ImageSlide(linkPreview.getThumbnail().get())), showControls, false);
|
||||
mediaThumbnailStub.require().setThumbnailClickListener(new LinkPreviewThumbnailClickListener());
|
||||
mediaThumbnailStub.require().setDownloadClickListener(downloadClickListener);
|
||||
mediaThumbnailStub.require().setCancelDownloadClickListener(attachmentCancelClickListener);
|
||||
mediaThumbnailStub.require().setPlayVideoClickListener(playVideoClickListener);
|
||||
mediaThumbnailStub.require().setProgressWheelClickListener(progressWheelClickListener);
|
||||
mediaThumbnailStub.require().setOnLongClickListener(passthroughClickListener);
|
||||
|
||||
linkPreviewStub.get().setLinkPreview(glideRequests, linkPreview, false);
|
||||
@@ -1192,7 +1181,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.setTopMargin(linkPreviewStub.get(), 0);
|
||||
} else {
|
||||
linkPreviewStub.get().setLinkPreview(glideRequests, linkPreview, true, !isContentCondensed(), displayMode.getScheduleMessageMode());
|
||||
linkPreviewStub.get().setLinkPreview(glideRequests, linkPreview, true, !isContentCondensed());
|
||||
linkPreviewStub.get().setDownloadClickedListener(downloadClickListener);
|
||||
setLinkPreviewCorners(messageRecord, previousRecord, nextRecord, isGroupThread, false);
|
||||
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
@@ -1252,7 +1241,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
documentViewStub.get().setDocument(
|
||||
((MediaMmsMessageRecord) messageRecord).getSlideDeck().getDocumentSlide(),
|
||||
showControls,
|
||||
displayMode != ConversationItemDisplayMode.Detailed.INSTANCE
|
||||
displayMode != ConversationItemDisplayMode.DETAILED
|
||||
);
|
||||
documentViewStub.get().setDocumentClickListener(new ThumbnailClickListener());
|
||||
documentViewStub.get().setDownloadClickListener(singleDownloadClickListener);
|
||||
@@ -1320,8 +1309,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
false);
|
||||
mediaThumbnailStub.require().setThumbnailClickListener(new ThumbnailClickListener());
|
||||
mediaThumbnailStub.require().setDownloadClickListener(downloadClickListener);
|
||||
mediaThumbnailStub.require().setCancelDownloadClickListener(attachmentCancelClickListener);
|
||||
mediaThumbnailStub.require().setPlayVideoClickListener(playVideoClickListener);
|
||||
mediaThumbnailStub.require().setProgressWheelClickListener(progressWheelClickListener);
|
||||
mediaThumbnailStub.require().setOnLongClickListener(passthroughClickListener);
|
||||
mediaThumbnailStub.require().setOnClickListener(passthroughClickListener);
|
||||
mediaThumbnailStub.require().showShade(messageRecord.isDisplayBodyEmpty(getContext()) && !hasExtraText(messageRecord));
|
||||
@@ -1775,7 +1763,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
}
|
||||
|
||||
private void setHasBeenQuoted(@NonNull ConversationMessage message) {
|
||||
if (message.hasBeenQuoted() && !isCondensedMode() && quotedIndicator != null && batchSelected.isEmpty() && displayMode != ConversationItemDisplayMode.EditHistory.INSTANCE) {
|
||||
if (message.hasBeenQuoted() && !isCondensedMode() && quotedIndicator != null && batchSelected.isEmpty() && displayMode != ConversationItemDisplayMode.EDIT_HISTORY) {
|
||||
quotedIndicator.setVisibility(VISIBLE);
|
||||
quotedIndicator.setOnClickListener(quotedIndicatorClickListener);
|
||||
} else if (quotedIndicator != null) {
|
||||
@@ -1798,11 +1786,11 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
}
|
||||
|
||||
private boolean forceFooter(@NonNull MessageRecord messageRecord) {
|
||||
return hasAudio(messageRecord) || MessageRecordUtil.isEditMessage(messageRecord) || displayMode == ConversationItemDisplayMode.EditHistory.INSTANCE;
|
||||
return hasAudio(messageRecord) || MessageRecordUtil.isEditMessage(messageRecord) || displayMode == ConversationItemDisplayMode.EDIT_HISTORY;
|
||||
}
|
||||
|
||||
private boolean forceGroupHeader(@NonNull MessageRecord messageRecord) {
|
||||
return displayMode == ConversationItemDisplayMode.EditHistory.INSTANCE;
|
||||
return displayMode == ConversationItemDisplayMode.EDIT_HISTORY;
|
||||
}
|
||||
|
||||
private ConversationItemFooter getActiveFooter(@NonNull MessageRecord messageRecord) {
|
||||
@@ -1906,7 +1894,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
|
||||
int background;
|
||||
|
||||
if (isSingularMessage(current, previous, next, isGroupThread) || displayMode == ConversationItemDisplayMode.EditHistory.INSTANCE) {
|
||||
if (isSingularMessage(current, previous, next, isGroupThread) || displayMode == ConversationItemDisplayMode.EDIT_HISTORY) {
|
||||
if (current.isOutgoing()) {
|
||||
background = R.drawable.message_bubble_background_sent_alone;
|
||||
outliner.setRadius(bigRadius);
|
||||
@@ -2002,11 +1990,11 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
int spacingTop = readDimen(context, R.dimen.conversation_vertical_message_spacing_collapse);
|
||||
int spacingBottom = spacingTop;
|
||||
|
||||
if (isStartOfMessageCluster(current, previous, isGroupThread) && (displayMode != ConversationItemDisplayMode.EditHistory.INSTANCE || next.isEmpty())) {
|
||||
if (isStartOfMessageCluster(current, previous, isGroupThread) && (displayMode != ConversationItemDisplayMode.EDIT_HISTORY || next.isEmpty())) {
|
||||
spacingTop = readDimen(context, R.dimen.conversation_vertical_message_spacing_default);
|
||||
}
|
||||
|
||||
if (isEndOfMessageCluster(current, next, isGroupThread) || displayMode == ConversationItemDisplayMode.EditHistory.INSTANCE) {
|
||||
if (isEndOfMessageCluster(current, next, isGroupThread) || displayMode == ConversationItemDisplayMode.EDIT_HISTORY) {
|
||||
spacingBottom = readDimen(context, R.dimen.conversation_vertical_message_spacing_default);
|
||||
}
|
||||
|
||||
@@ -2452,84 +2440,18 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
}
|
||||
}
|
||||
|
||||
private class AttachmentCancelClickListener implements SlidesClickedListener {
|
||||
@Override
|
||||
public void onClick(View v, List<Slide> slides) {
|
||||
Log.i(TAG, "onClick() for attachment cancellation");
|
||||
final JobManager jobManager = ApplicationDependencies.getJobManager();
|
||||
if (messageRecord.isMmsNotification()) {
|
||||
Log.i(TAG, "Canceling MMS attachments download");
|
||||
jobManager.cancel("mms-operation");
|
||||
} else {
|
||||
Log.i(TAG, "Canceling push attachment downloads for " + slides.size() + " items");
|
||||
|
||||
for (Slide slide : slides) {
|
||||
final String queue = AttachmentDownloadJob.constructQueueString(((DatabaseAttachment) slide.asAttachment()).getAttachmentId());
|
||||
jobManager.cancelAllInQueue(queue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PlayVideoClickListener implements SlideClickListener {
|
||||
private static final float MINIMUM_DOWNLOADED_THRESHOLD = 0.05f;
|
||||
private View parentView;
|
||||
private Slide activeSlide;
|
||||
private class ProgressWheelClickListener implements SlideClickListener {
|
||||
|
||||
@Override
|
||||
public void onClick(View v, Slide slide) {
|
||||
if (messageRecord.isOutgoing()) {
|
||||
Log.d(TAG, "Video player button for outgoing slide clicked.");
|
||||
return;
|
||||
}
|
||||
if (MediaUtil.isInstantVideoSupported(slide)) {
|
||||
final DatabaseAttachment databaseAttachment = (DatabaseAttachment) slide.asAttachment();
|
||||
if (databaseAttachment.getTransferState() != AttachmentTable.TRANSFER_PROGRESS_STARTED) {
|
||||
final AttachmentId attachmentId = databaseAttachment.getAttachmentId();
|
||||
final JobManager jobManager = ApplicationDependencies.getJobManager();
|
||||
final String queue = AttachmentDownloadJob.constructQueueString(attachmentId);
|
||||
setup(v, slide);
|
||||
jobManager.add(new AttachmentDownloadJob(messageRecord.getId(),
|
||||
attachmentId,
|
||||
true));
|
||||
jobManager.addListener(queue, (job, jobState) -> {
|
||||
if (jobState.isComplete()) {
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
launchMediaPreview(v, slide);
|
||||
cleanup();
|
||||
}
|
||||
final boolean isIncremental = slide.asAttachment().getIncrementalDigest() != null;
|
||||
final boolean contentTypeSupported = MediaUtil.isVideoType(slide.getContentType());
|
||||
if (FeatureFlags.instantVideoPlayback() && isIncremental && contentTypeSupported) {
|
||||
launchMediaPreview(v, slide);
|
||||
} else {
|
||||
Log.d(TAG, "Non-eligible slide clicked.");
|
||||
Log.d(TAG, "Non-eligible slide clicked: " + "\tisIncremental: " + isIncremental + "\tcontentTypeSupported: " + contentTypeSupported);
|
||||
}
|
||||
}
|
||||
|
||||
private void setup(View v, Slide slide) {
|
||||
parentView = v;
|
||||
activeSlide = slide;
|
||||
if (!EventBus.getDefault().isRegistered(this)) EventBus.getDefault().register(this);
|
||||
}
|
||||
|
||||
private void cleanup() {
|
||||
parentView = null;
|
||||
activeSlide = null;
|
||||
if (EventBus.getDefault().isRegistered(this)) {
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||
public void onEventAsync(PartProgressEvent event) {
|
||||
float progressPercent = ((float) event.progress) / event.total;
|
||||
final View currentParentView = parentView;
|
||||
final Slide currentActiveSlide = activeSlide;
|
||||
if (progressPercent >= MINIMUM_DOWNLOADED_THRESHOLD && currentParentView != null && currentActiveSlide != null) {
|
||||
cleanup();
|
||||
launchMediaPreview(currentParentView, currentActiveSlide);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SlideClickPassthroughListener implements SlideClickListener {
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
package org.thoughtcrime.securesms.conversation
|
||||
|
||||
sealed class ConversationItemDisplayMode(val scheduleMessageMode: Boolean = false) {
|
||||
enum class ConversationItemDisplayMode {
|
||||
/** Normal rendering, used for normal bubbles in the conversation view */
|
||||
object Standard : ConversationItemDisplayMode()
|
||||
STANDARD,
|
||||
|
||||
/** Smaller bubbles, often trimming text and shrinking images. Used for quote threads. */
|
||||
class Condensed(scheduleMessageMode: Boolean) : ConversationItemDisplayMode(scheduleMessageMode)
|
||||
CONDENSED,
|
||||
|
||||
/** Smaller bubbles, always singular bubbles, with a footer. Used for edit message history. */
|
||||
object EditHistory : ConversationItemDisplayMode()
|
||||
EDIT_HISTORY,
|
||||
|
||||
/** Less length restrictions. Used to show more info in message details. */
|
||||
object Detailed : ConversationItemDisplayMode()
|
||||
DETAILED;
|
||||
|
||||
fun displayWallpaper(): Boolean {
|
||||
return this == Standard || this == Detailed
|
||||
return this == STANDARD || this == DETAILED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.Multiselect;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectCollection;
|
||||
import org.thoughtcrime.securesms.conversation.v2.computed.FormattedDate;
|
||||
import org.thoughtcrime.securesms.database.BodyRangeUtil;
|
||||
import org.thoughtcrime.securesms.database.MentionUtil;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
@@ -46,7 +45,7 @@ public class ConversationMessage {
|
||||
@NonNull private final Recipient threadRecipient;
|
||||
private final boolean hasBeenQuoted;
|
||||
@Nullable private final MessageRecord originalMessage;
|
||||
@NonNull private final ComputedProperties computedProperties;
|
||||
@NonNull private final String formattedDate;
|
||||
|
||||
private ConversationMessage(@NonNull MessageRecord messageRecord,
|
||||
@Nullable CharSequence body,
|
||||
@@ -55,15 +54,15 @@ public class ConversationMessage {
|
||||
@Nullable MessageStyler.Result styleResult,
|
||||
@NonNull Recipient threadRecipient,
|
||||
@Nullable MessageRecord originalMessage,
|
||||
@NonNull ComputedProperties computedProperties)
|
||||
@NonNull String formattedDate)
|
||||
{
|
||||
this.messageRecord = messageRecord;
|
||||
this.hasBeenQuoted = hasBeenQuoted;
|
||||
this.mentions = mentions != null ? mentions : Collections.emptyList();
|
||||
this.styleResult = styleResult != null ? styleResult : MessageStyler.Result.none();
|
||||
this.threadRecipient = threadRecipient;
|
||||
this.originalMessage = originalMessage;
|
||||
this.computedProperties = computedProperties;
|
||||
this.messageRecord = messageRecord;
|
||||
this.hasBeenQuoted = hasBeenQuoted;
|
||||
this.mentions = mentions != null ? mentions : Collections.emptyList();
|
||||
this.styleResult = styleResult != null ? styleResult : MessageStyler.Result.none();
|
||||
this.threadRecipient = threadRecipient;
|
||||
this.originalMessage = originalMessage;
|
||||
this.formattedDate = formattedDate;
|
||||
|
||||
if (body != null) {
|
||||
this.body = SpannableString.valueOf(body);
|
||||
@@ -96,8 +95,9 @@ public class ConversationMessage {
|
||||
return hasBeenQuoted;
|
||||
}
|
||||
|
||||
public @NonNull ComputedProperties getComputedProperties() {
|
||||
return computedProperties;
|
||||
@NonNull
|
||||
public String getFormattedDate() {
|
||||
return formattedDate;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -160,27 +160,6 @@ public class ConversationMessage {
|
||||
return threadRecipient;
|
||||
}
|
||||
|
||||
public static @NonNull FormattedDate getFormattedDate(@NonNull Context context, @NonNull MessageRecord messageRecord) {
|
||||
return MessageRecordUtil.isScheduled(messageRecord) ? new FormattedDate(false, DateUtils.getOnlyTimeString(context, Locale.getDefault(), ((MediaMmsMessageRecord) messageRecord).getScheduledDate()))
|
||||
: DateUtils.getDatelessRelativeTimeSpanFormattedDate(context, Locale.getDefault(), messageRecord.getTimestamp());
|
||||
}
|
||||
|
||||
public static class ComputedProperties {
|
||||
private @NonNull FormattedDate formattedDate;
|
||||
|
||||
ComputedProperties(@NonNull FormattedDate formattedDate) {
|
||||
this.formattedDate = formattedDate;
|
||||
}
|
||||
|
||||
public synchronized FormattedDate getFormattedDate() {
|
||||
return formattedDate;
|
||||
}
|
||||
|
||||
public synchronized void setFormattedDate(@NonNull FormattedDate formattedDate) {
|
||||
this.formattedDate = formattedDate;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory providing multiple ways of creating {@link ConversationMessage}s.
|
||||
*/
|
||||
@@ -225,8 +204,8 @@ public class ConversationMessage {
|
||||
}
|
||||
}
|
||||
|
||||
FormattedDate formattedDate = getFormattedDate(context, messageRecord);
|
||||
|
||||
String formattedDate = MessageRecordUtil.isScheduled(messageRecord) ? DateUtils.getOnlyTimeString(context, Locale.getDefault(), ((MediaMmsMessageRecord) messageRecord).getScheduledDate())
|
||||
: DateUtils.getDatelessRelativeTimeSpanString(context, Locale.getDefault(), messageRecord.getTimestamp());
|
||||
return new ConversationMessage(messageRecord,
|
||||
styledAndMentionBody != null ? styledAndMentionBody : mentionsUpdate != null ? mentionsUpdate.getBody() : body,
|
||||
mentionsUpdate != null ? mentionsUpdate.getMentions() : null,
|
||||
@@ -234,7 +213,7 @@ public class ConversationMessage {
|
||||
styleResult,
|
||||
threadRecipient,
|
||||
originalMessage,
|
||||
new ComputedProperties(formattedDate));
|
||||
formattedDate);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -454,7 +454,7 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
if (Util.hasItems(acis)) {
|
||||
if (acis.contains(SignalStore.account().requireAci())) {
|
||||
text = R.string.ConversationUpdateItem_return_to_call;
|
||||
} else if (GroupCallUpdateDetailsUtil.parse(conversationMessage.getMessageRecord().getBody()).isCallFull) {
|
||||
} else if (GroupCallUpdateDetailsUtil.parse(conversationMessage.getMessageRecord().getBody()).getIsCallFull()) {
|
||||
text = R.string.ConversationUpdateItem_call_is_full;
|
||||
} else {
|
||||
text = R.string.ConversationUpdateItem_join_call;
|
||||
@@ -712,7 +712,9 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
return;
|
||||
}
|
||||
|
||||
IdentityUtil.getRemoteIdentityKey(getContext(), messageRecord.getToRecipient()).addListener(new ListenableFuture.Listener<>() {
|
||||
final Recipient sender = ConversationUpdateItem.this.senderObserver.getObservedRecipient();
|
||||
|
||||
IdentityUtil.getRemoteIdentityKey(getContext(), sender).addListener(new ListenableFuture.Listener<Optional<IdentityRecord>>() {
|
||||
@Override
|
||||
public void onSuccess(Optional<IdentityRecord> result) {
|
||||
if (result.isPresent()) {
|
||||
|
||||
@@ -63,13 +63,13 @@ object MessageStyler {
|
||||
var bottomButton: BodyRange.Button? = null
|
||||
|
||||
messageRanges
|
||||
.ranges
|
||||
.rangesList
|
||||
.filter { r -> r.start >= 0 && r.start < span.length && r.start + r.length >= 0 }
|
||||
.forEach { range ->
|
||||
val start = range.start
|
||||
val end = (range.start + range.length).coerceAtMost(span.length)
|
||||
|
||||
if (range.style != null) {
|
||||
if (range.hasStyle()) {
|
||||
val styleSpan: Any? = when (range.style) {
|
||||
BodyRange.Style.BOLD -> boldStyle()
|
||||
BodyRange.Style.ITALIC -> italicStyle()
|
||||
@@ -90,10 +90,10 @@ object MessageStyler {
|
||||
span.setSpan(styleSpan, start, end, SPAN_FLAGS)
|
||||
appliedStyle = true
|
||||
}
|
||||
} else if (range.link != null) {
|
||||
} else if (range.hasLink() && range.link != null) {
|
||||
span.setSpan(PlaceholderURLSpan(range.link), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
hasLinks = true
|
||||
} else if (range.button != null) {
|
||||
} else if (range.hasButton() && range.button != null) {
|
||||
bottomButton = range.button
|
||||
}
|
||||
}
|
||||
@@ -245,7 +245,7 @@ object MessageStyler {
|
||||
}
|
||||
|
||||
if (spanLength > 0 && style != null) {
|
||||
BodyRange(start = spanStart, length = spanLength, style = style)
|
||||
BodyRange.newBuilder().setStart(spanStart).setLength(spanLength).setStyle(style).build()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -255,7 +255,7 @@ object MessageStyler {
|
||||
}
|
||||
|
||||
return if (bodyRanges.isNotEmpty()) {
|
||||
BodyRangeList(ranges = bodyRanges)
|
||||
BodyRangeList.newBuilder().addAllRanges(bodyRanges).build()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
@@ -93,7 +93,8 @@ class ScheduledMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment
|
||||
val colorizer = Colorizer()
|
||||
|
||||
messageAdapter = ConversationAdapter(requireContext(), viewLifecycleOwner, GlideApp.with(this), Locale.getDefault(), ConversationAdapterListener(), conversationRecipient.hasWallpaper(), colorizer).apply {
|
||||
setCondensedMode(ConversationItemDisplayMode.Condensed(scheduleMessageMode = true))
|
||||
setCondensedMode(ConversationItemDisplayMode.CONDENSED)
|
||||
setScheduledMessagesMode(true)
|
||||
}
|
||||
|
||||
val list: RecyclerView = view.findViewById<RecyclerView>(R.id.scheduled_list).apply {
|
||||
|
||||
@@ -73,20 +73,20 @@ class ChatColors(
|
||||
}
|
||||
|
||||
fun serialize(): ChatColor {
|
||||
val builder: ChatColor.Builder = ChatColor.Builder()
|
||||
val builder: ChatColor.Builder = ChatColor.newBuilder()
|
||||
|
||||
if (linearGradient != null) {
|
||||
val gradientBuilder = ChatColor.LinearGradient.Builder()
|
||||
val gradientBuilder = ChatColor.LinearGradient.newBuilder()
|
||||
|
||||
gradientBuilder.rotation = linearGradient.degrees
|
||||
gradientBuilder.colors = linearGradient.colors.toList()
|
||||
gradientBuilder.positions = linearGradient.positions.toList()
|
||||
linearGradient.colors.forEach { gradientBuilder.addColors(it) }
|
||||
linearGradient.positions.forEach { gradientBuilder.addPositions(it) }
|
||||
|
||||
builder.linearGradient(gradientBuilder.build())
|
||||
builder.setLinearGradient(gradientBuilder)
|
||||
}
|
||||
|
||||
if (singleColor != null) {
|
||||
builder.singleColor(ChatColor.SingleColor.Builder().color(singleColor).build())
|
||||
builder.setSingleColor(ChatColor.SingleColor.newBuilder().setColor(singleColor))
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
@@ -142,18 +142,18 @@ class ChatColors(
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun forChatColor(id: Id, chatColor: ChatColor): ChatColors {
|
||||
assert((chatColor.singleColor != null) xor (chatColor.linearGradient != null))
|
||||
assert(chatColor.hasSingleColor() xor chatColor.hasLinearGradient())
|
||||
|
||||
return if (chatColor.linearGradient != null) {
|
||||
return if (chatColor.hasLinearGradient()) {
|
||||
val linearGradient = LinearGradient(
|
||||
chatColor.linearGradient.rotation,
|
||||
chatColor.linearGradient.colors.toIntArray(),
|
||||
chatColor.linearGradient.positions.toFloatArray()
|
||||
chatColor.linearGradient.colorsList.toIntArray(),
|
||||
chatColor.linearGradient.positionsList.toFloatArray()
|
||||
)
|
||||
|
||||
forGradient(id, linearGradient)
|
||||
} else {
|
||||
val singleColor = chatColor.singleColor!!.color
|
||||
val singleColor = chatColor.singleColor.color
|
||||
|
||||
forColor(id, singleColor)
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ class DraftRepository(
|
||||
var updatedText: Spannable? = null
|
||||
|
||||
if (textDraft != null && bodyRangesDraft != null) {
|
||||
val bodyRanges: BodyRangeList = BodyRangeList.ADAPTER.decode(Base64.decodeOrThrow(bodyRangesDraft.value))
|
||||
val bodyRanges: BodyRangeList = BodyRangeList.parseFrom(Base64.decodeOrThrow(bodyRangesDraft.value))
|
||||
val mentions: List<Mention> = MentionUtil.bodyRangeListToMentions(bodyRanges)
|
||||
|
||||
val updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, textDraft.value, mentions)
|
||||
|
||||
@@ -62,7 +62,7 @@ class DraftViewModel @JvmOverloads constructor(
|
||||
} else if (mentionRanges == null) {
|
||||
styleBodyRanges
|
||||
} else {
|
||||
styleBodyRanges.newBuilder().apply { ranges += mentionRanges.ranges }.build()
|
||||
styleBodyRanges.toBuilder().addAllRanges(mentionRanges.rangesList).build()
|
||||
}
|
||||
|
||||
saveDrafts(it.copy(textDraft = text.toTextDraft(), bodyRangesDraft = bodyRanges?.toDraft(), messageEditDraft = Draft(Draft.MESSAGE_EDIT, messageId.serialize())))
|
||||
@@ -84,7 +84,7 @@ class DraftViewModel @JvmOverloads constructor(
|
||||
} else if (mentionRanges == null) {
|
||||
styleBodyRanges
|
||||
} else {
|
||||
styleBodyRanges.newBuilder().apply { ranges += mentionRanges.ranges }.build()
|
||||
styleBodyRanges.toBuilder().addAllRanges(mentionRanges.rangesList).build()
|
||||
}
|
||||
|
||||
saveDrafts(it.copy(textDraft = text.toTextDraft(), bodyRangesDraft = bodyRanges?.toDraft()))
|
||||
@@ -148,5 +148,5 @@ private fun String.toTextDraft(): Draft? {
|
||||
}
|
||||
|
||||
private fun BodyRangeList.toDraft(): Draft {
|
||||
return Draft(Draft.BODY_RANGES, Base64.encodeBytes(encode()))
|
||||
return Draft(Draft.BODY_RANGES, Base64.encodeBytes(toByteArray()))
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ class ConversationItemAnimator(
|
||||
|
||||
private fun animateSlide(viewHolder: RecyclerView.ViewHolder, preLayoutInfo: ItemHolderInfo?, postLayoutInfo: ItemHolderInfo): Boolean {
|
||||
if (isInMultiSelectMode() || !shouldPlayMessageAnimations()) {
|
||||
Log.v(TAG, "Dropping slide animation: (${isInMultiSelectMode()}, ${shouldPlayMessageAnimations()}) :: ${viewHolder.absoluteAdapterPosition}")
|
||||
dispatchAnimationFinished(viewHolder)
|
||||
return false
|
||||
}
|
||||
@@ -104,6 +105,7 @@ class ConversationItemAnimator(
|
||||
animateSlide(viewHolder, preLayoutInfo, postLayoutInfo)
|
||||
}
|
||||
} else {
|
||||
Log.v(TAG, "Dropping persistence animation: (${isInMultiSelectMode()}, ${shouldPlayMessageAnimations()}, ${isParentFilled()}) :: ${viewHolder.absoluteAdapterPosition}")
|
||||
dispatchAnimationFinished(viewHolder)
|
||||
false
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ import androidx.core.view.children
|
||||
import androidx.core.view.forEach
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.airbnb.lottie.SimpleColorFilter
|
||||
import com.google.android.material.animation.ArgbEvaluatorCompat
|
||||
@@ -125,15 +124,7 @@ class MultiselectItemDecoration(
|
||||
}
|
||||
|
||||
private fun getCurrentSelection(parent: RecyclerView): Set<MultiselectPart> {
|
||||
return parent.findAdapterBridge().selectedItems
|
||||
}
|
||||
|
||||
private fun RecyclerView.findAdapterBridge(): ConversationAdapterBridge {
|
||||
return when (val parentAdapter = adapter!!) {
|
||||
is ConversationAdapterBridge -> parentAdapter
|
||||
is ConcatAdapter -> (parentAdapter.adapters[1] as ConversationAdapterBridge)
|
||||
else -> error("Unexpected adapter configuration")
|
||||
}
|
||||
return (parent.adapter as ConversationAdapterBridge).selectedItems
|
||||
}
|
||||
|
||||
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||
@@ -165,14 +156,14 @@ class MultiselectItemDecoration(
|
||||
outRect.setEmpty()
|
||||
updateChildOffsets(parent, view)
|
||||
|
||||
consumePulseRequest(parent.findAdapterBridge())
|
||||
consumePulseRequest(parent.adapter as ConversationAdapterBridge)
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the background shade.
|
||||
*/
|
||||
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
val adapter = parent.findAdapterBridge()
|
||||
val adapter = parent.adapter as ConversationAdapterBridge
|
||||
|
||||
if (adapter.selectedItems.isEmpty()) {
|
||||
drawFocusShadeUnderIfNecessary(canvas, parent)
|
||||
@@ -240,7 +231,7 @@ class MultiselectItemDecoration(
|
||||
* Draws the selected check or empty circle.
|
||||
*/
|
||||
override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
val adapter = parent.findAdapterBridge()
|
||||
val adapter = parent.adapter as ConversationAdapterBridge
|
||||
if (adapter.selectedItems.isEmpty()) {
|
||||
drawFocusShadeOverIfNecessary(canvas, parent)
|
||||
}
|
||||
@@ -356,7 +347,7 @@ class MultiselectItemDecoration(
|
||||
* called in getItemOffsets to ensure the gutter goes away when multiselect mode ends.
|
||||
*/
|
||||
private fun updateChildOffsets(parent: RecyclerView, child: View) {
|
||||
val adapter = parent.findAdapterBridge()
|
||||
val adapter = parent.adapter as ConversationAdapterBridge
|
||||
val isLtr = ViewUtil.isLtr(child)
|
||||
val multiselectable: Multiselectable = resolveMultiselectable(parent, child) ?: return
|
||||
|
||||
@@ -575,13 +566,19 @@ class MultiselectItemDecoration(
|
||||
}
|
||||
|
||||
private fun RecyclerView.getInteractableChildren(): Sequence<InteractiveConversationElement> {
|
||||
return children.map { getChildViewHolder(it) }.filterIsInstance<InteractiveConversationElement>() + children.filterIsInstance<InteractiveConversationElement>()
|
||||
return if (FeatureFlags.useTextOnlyConversationItemV2()) {
|
||||
children.map { getChildViewHolder(it) }.filterIsInstance<InteractiveConversationElement>()
|
||||
} else {
|
||||
children.filterIsInstance<InteractiveConversationElement>()
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveMultiselectable(parent: RecyclerView, child: View): Multiselectable? {
|
||||
val multiselectable = parent.getChildViewHolder(child) as? Multiselectable
|
||||
|
||||
return multiselectable ?: child as? Multiselectable
|
||||
return if (FeatureFlags.useTextOnlyConversationItemV2()) {
|
||||
parent.getChildViewHolder(child) as? Multiselectable
|
||||
} else {
|
||||
child as? Multiselectable
|
||||
}
|
||||
}
|
||||
|
||||
private class PulseAnimator(pulseColor: Int) {
|
||||
|
||||
@@ -8,7 +8,6 @@ import android.view.ViewGroup
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
@@ -20,17 +19,11 @@ class MultiselectForwardBottomSheet : FixedRoundedCornerBottomSheetDialogFragmen
|
||||
|
||||
private var callback: Callback? = null
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(MultiselectForwardBottomSheet::class.java)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.multiselect_bottom_sheet, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
Log.d(TAG, "onViewCreated()")
|
||||
|
||||
callback = findListener<Callback>()
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
@@ -60,22 +53,18 @@ class MultiselectForwardBottomSheet : FixedRoundedCornerBottomSheetDialogFragmen
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
Log.d(TAG, "onDismiss()")
|
||||
callback?.onDismissForwardSheet()
|
||||
}
|
||||
|
||||
override fun onFinishForwardAction() {
|
||||
Log.d(TAG, "onFinishForwardAction()")
|
||||
callback?.onFinishForwardAction()
|
||||
}
|
||||
|
||||
override fun exitFlow() {
|
||||
Log.d(TAG, "exitFlow()")
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
|
||||
override fun onSearchInputFocused() {
|
||||
Log.d(TAG, "onSearchInputFocused()")
|
||||
(requireDialog() as BottomSheetDialog).behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
|
||||
|
||||
@@ -117,8 +117,6 @@ class MultiselectForwardFragment :
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
Log.d(TAG, "onViewCreated()")
|
||||
|
||||
view.minimumHeight = resources.displayMetrics.heightPixels
|
||||
|
||||
contactSearchRecycler = view.findViewById(R.id.contact_selection_list)
|
||||
@@ -135,9 +133,7 @@ class MultiselectForwardFragment :
|
||||
this::getConfiguration,
|
||||
object : ContactSearchMediator.SimpleCallbacks() {
|
||||
override fun onBeforeContactsSelected(view: View?, contactSearchKeys: Set<ContactSearchKey>): Set<ContactSearchKey> {
|
||||
val filtered: Set<ContactSearchKey> = filterContacts(view, contactSearchKeys)
|
||||
Log.d(TAG, "onBeforeContactsSelected() Attempting to select: ${contactSearchKeys.map { it.toString() }}, Filtered selection: ${filtered.map { it.toString() } }")
|
||||
return filtered
|
||||
return filterContacts(view, contactSearchKeys)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -228,6 +224,7 @@ class MultiselectForwardFragment :
|
||||
disposables += contactSearchMediator
|
||||
.getErrorEvents()
|
||||
.subscribe {
|
||||
@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
|
||||
val message: Int = when (it) {
|
||||
ContactSearchError.CONTACT_NOT_SELECTABLE -> R.string.MultiselectForwardFragment__only_admins_can_send_messages_to_this_group
|
||||
ContactSearchError.RECOMMENDED_LIMIT_REACHED -> R.string.ContactSelectionListFragment_recommended_member_limit_reached
|
||||
@@ -238,8 +235,6 @@ class MultiselectForwardFragment :
|
||||
}
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) {
|
||||
Log.d(TAG, "State change: ${it.stage.javaClass.simpleName}")
|
||||
|
||||
when (it.stage) {
|
||||
MultiselectForwardState.Stage.Selection -> {}
|
||||
MultiselectForwardState.Stage.FirstConfirmation -> displayFirstSendConfirmation()
|
||||
@@ -277,8 +272,6 @@ class MultiselectForwardFragment :
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
Log.d(TAG, "onViewCreated()")
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
val expiringMessages = args.multiShareArgs.filter { it.expiresAt > 0L }
|
||||
val firstToExpire = expiringMessages.minByOrNull { it.expiresAt }
|
||||
@@ -338,8 +331,6 @@ class MultiselectForwardFragment :
|
||||
}
|
||||
|
||||
private fun dismissWithSuccess(@PluralsRes toastTextResId: Int) {
|
||||
Log.d(TAG, "dismissWithSuccess() Selected: ${contactSearchMediator.getSelectedContacts().map { it.toString() }}")
|
||||
|
||||
requireListener<Callback>().setResult(
|
||||
Bundle().apply {
|
||||
putBoolean(RESULT_SENT, true)
|
||||
@@ -350,7 +341,7 @@ class MultiselectForwardFragment :
|
||||
}
|
||||
|
||||
private fun dismissAndShowToast(@PluralsRes toastTextResId: Int) {
|
||||
Log.d(TAG, "dismissAndShowToast() Selected: ${contactSearchMediator.getSelectedContacts().map { it.toString() }}")
|
||||
Log.d(TAG, "dismissAndShowToast")
|
||||
|
||||
val argCount = getMessageCount()
|
||||
|
||||
@@ -372,7 +363,7 @@ class MultiselectForwardFragment :
|
||||
}
|
||||
|
||||
private fun dismissWithSelection(selectedContacts: Set<ContactSearchKey>) {
|
||||
Log.d(TAG, "dismissWithSelection() Selected: ${selectedContacts.map { it.toString() }}")
|
||||
Log.d(TAG, "dismissWithSelection")
|
||||
|
||||
callback.onFinishForwardAction()
|
||||
dismissibleDialog?.dismiss()
|
||||
|
||||
@@ -74,7 +74,7 @@ class MessageQuotesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment() {
|
||||
val colorizer = Colorizer()
|
||||
|
||||
messageAdapter = ConversationAdapter(requireContext(), viewLifecycleOwner, GlideApp.with(this), Locale.getDefault(), ConversationAdapterListener(), conversationRecipient.hasWallpaper(), colorizer).apply {
|
||||
setCondensedMode(ConversationItemDisplayMode.Condensed(scheduleMessageMode = false))
|
||||
setCondensedMode(ConversationItemDisplayMode.CONDENSED)
|
||||
}
|
||||
|
||||
val list: RecyclerView = view.findViewById<RecyclerView>(R.id.quotes_list).apply {
|
||||
|
||||
@@ -91,7 +91,7 @@ class EditMessageHistoryDialog : FixedRoundedCornerBottomSheetDialogFragment() {
|
||||
conversationRecipient.hasWallpaper(),
|
||||
colorizer
|
||||
).apply {
|
||||
setCondensedMode(ConversationItemDisplayMode.EditHistory)
|
||||
setCondensedMode(ConversationItemDisplayMode.EDIT_HISTORY)
|
||||
}
|
||||
|
||||
binding.editHistoryList.apply {
|
||||
|
||||
@@ -187,7 +187,7 @@ class ConversationAdapterV2(
|
||||
}
|
||||
|
||||
override val displayMode: ConversationItemDisplayMode
|
||||
get() = condensedMode ?: ConversationItemDisplayMode.Standard
|
||||
get() = condensedMode ?: ConversationItemDisplayMode.STANDARD
|
||||
|
||||
override fun onStartExpirationTimeout(messageRecord: MessageRecord) {
|
||||
startExpirationTimeout(messageRecord)
|
||||
@@ -484,7 +484,7 @@ class ConversationAdapterV2(
|
||||
get() = getConversationMessage(bindingAdapterPosition - 1)?.messageRecord.toOptional()
|
||||
|
||||
protected val displayMode: ConversationItemDisplayMode
|
||||
get() = condensedMode ?: ConversationItemDisplayMode.Standard
|
||||
get() = condensedMode ?: ConversationItemDisplayMode.STANDARD
|
||||
|
||||
override val conversationMessage: ConversationMessage
|
||||
get() = bindable.conversationMessage
|
||||
|
||||
@@ -62,7 +62,6 @@ import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.ConversationLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -184,8 +183,6 @@ import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryChanged
|
||||
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryReplacement
|
||||
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryResultsControllerV2
|
||||
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryViewModelV2
|
||||
import org.thoughtcrime.securesms.conversation.v2.computed.ConversationMessageComputeWorkers
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.ConversationMessageElement
|
||||
import org.thoughtcrime.securesms.conversation.v2.groups.ConversationGroupCallViewModel
|
||||
import org.thoughtcrime.securesms.conversation.v2.groups.ConversationGroupViewModel
|
||||
import org.thoughtcrime.securesms.conversation.v2.items.ChatColorsDrawable
|
||||
@@ -469,7 +466,6 @@ class ConversationFragment :
|
||||
private lateinit var conversationActivityResultContracts: ConversationActivityResultContracts
|
||||
private lateinit var scrollToPositionDelegate: ScrollToPositionDelegate
|
||||
private lateinit var adapter: ConversationAdapterV2
|
||||
private lateinit var typingIndicatorAdapter: ConversationTypingIndicatorAdapter
|
||||
private lateinit var recyclerViewColorizer: RecyclerViewColorizer
|
||||
private lateinit var attachmentManager: AttachmentManager
|
||||
private lateinit var multiselectItemDecoration: MultiselectItemDecoration
|
||||
@@ -477,6 +473,7 @@ class ConversationFragment :
|
||||
private lateinit var threadHeaderMarginDecoration: ThreadHeaderMarginDecoration
|
||||
private lateinit var conversationItemDecorations: ConversationItemDecorations
|
||||
private lateinit var optionsMenuCallback: ConversationOptionsMenuCallback
|
||||
private lateinit var typingIndicatorDecoration: TypingIndicatorDecoration
|
||||
private lateinit var backPressedCallback: BackPressedDelegate
|
||||
|
||||
private var animationsAllowed = false
|
||||
@@ -543,8 +540,6 @@ class ConversationFragment :
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.toolbar.isBackInvokedCallbackEnabled = false
|
||||
|
||||
disposables.bindTo(viewLifecycleOwner)
|
||||
FullscreenHelper(requireActivity()).showSystemUI()
|
||||
|
||||
@@ -1043,13 +1038,7 @@ class ConversationFragment :
|
||||
|
||||
getVoiceNoteMediaController().voiceNotePlaybackState.observe(viewLifecycleOwner, inputPanel.playbackStateObserver)
|
||||
|
||||
val conversationUpdateTick = ConversationUpdateTick {
|
||||
disposables += ConversationMessageComputeWorkers.recomputeFormattedDate(
|
||||
requireContext(),
|
||||
adapter.currentList.filterIsInstance<ConversationMessageElement>()
|
||||
).observeOn(AndroidSchedulers.mainThread()).subscribeBy { adapter.updateTimestamps() }
|
||||
}
|
||||
|
||||
val conversationUpdateTick = ConversationUpdateTick { adapter.updateTimestamps() }
|
||||
viewLifecycleOwner.lifecycle.addObserver(conversationUpdateTick)
|
||||
|
||||
if (args.conversationScreenType.isInPopup) {
|
||||
@@ -1087,24 +1076,18 @@ class ConversationFragment :
|
||||
}
|
||||
|
||||
private fun presentTypingIndicator() {
|
||||
typingIndicatorAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
if (positionStart == 0 && itemCount == 1 && layoutManager.findFirstCompletelyVisibleItemPosition() == 0) {
|
||||
scrollToPositionDelegate.resetScrollPosition()
|
||||
}
|
||||
}
|
||||
})
|
||||
typingIndicatorDecoration = TypingIndicatorDecoration(requireContext(), binding.conversationItemRecycler)
|
||||
binding.conversationItemRecycler.addItemDecoration(typingIndicatorDecoration)
|
||||
|
||||
ApplicationDependencies.getTypingStatusRepository().getTypists(args.threadId).observe(viewLifecycleOwner) {
|
||||
val recipient = viewModel.recipientSnapshot ?: return@observe
|
||||
|
||||
typingIndicatorAdapter.setState(
|
||||
ConversationTypingIndicatorAdapter.State(
|
||||
typists = it.typists,
|
||||
isGroupThread = recipient.isGroup,
|
||||
hasWallpaper = recipient.hasWallpaper(),
|
||||
isReplacedByIncomingMessage = it.isReplacedByIncomingMessage
|
||||
)
|
||||
typingIndicatorDecoration.setTypists(
|
||||
GlideApp.with(this),
|
||||
it.typists,
|
||||
recipient.isGroup,
|
||||
recipient.hasWallpaper(),
|
||||
it.isReplacedByIncomingMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1538,8 +1521,6 @@ class ConversationFragment :
|
||||
startExpirationTimeout = viewModel::startExpirationTimeout
|
||||
)
|
||||
|
||||
typingIndicatorAdapter = ConversationTypingIndicatorAdapter(GlideApp.with(this))
|
||||
|
||||
scrollToPositionDelegate = ScrollToPositionDelegate(
|
||||
recyclerView = binding.conversationItemRecycler,
|
||||
canJumpToPosition = adapter::canJumpToPosition
|
||||
@@ -1550,7 +1531,7 @@ class ConversationFragment :
|
||||
recyclerViewColorizer = RecyclerViewColorizer(binding.conversationItemRecycler)
|
||||
recyclerViewColorizer.setChatColors(args.chatColors)
|
||||
|
||||
binding.conversationItemRecycler.adapter = ConcatAdapter(typingIndicatorAdapter, adapter)
|
||||
binding.conversationItemRecycler.adapter = adapter
|
||||
multiselectItemDecoration = MultiselectItemDecoration(
|
||||
requireContext()
|
||||
) { viewModel.wallpaperSnapshot }
|
||||
@@ -1796,7 +1777,7 @@ class ConversationFragment :
|
||||
return
|
||||
}
|
||||
|
||||
if (SignalStore.uiHints().hasNotSeenTextFormattingAlert() && bodyRanges != null && bodyRanges.ranges.isNotEmpty()) {
|
||||
if (SignalStore.uiHints().hasNotSeenTextFormattingAlert() && bodyRanges != null && bodyRanges.rangesCount > 0) {
|
||||
Dialogs.showFormattedTextDialog(requireContext()) {
|
||||
sendMessage(body, mentions, bodyRanges, messageToEdit, quote, scheduledDate, slideDeck, contacts, clearCompose, linkPreviews, preUploadResults, bypassPreSendSafetyNumberCheck, isViewOnce, afterSendComplete)
|
||||
}
|
||||
@@ -3824,9 +3805,7 @@ class ConversationFragment :
|
||||
}
|
||||
|
||||
override fun onRecorderCanceled(byUser: Boolean) {
|
||||
if (lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
|
||||
updateToggleButtonState()
|
||||
}
|
||||
updateToggleButtonState()
|
||||
voiceMessageRecordingDelegate.onRecorderCanceled(byUser)
|
||||
}
|
||||
|
||||
@@ -3992,10 +3971,6 @@ class ConversationFragment :
|
||||
|
||||
override fun onKeyboardHidden() {
|
||||
closeEmojiSearch()
|
||||
|
||||
if (searchMenuItem?.isActionViewExpanded == true && searchMenuItem?.actionView?.hasFocus() == true) {
|
||||
searchMenuItem?.actionView?.clearFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,14 +55,7 @@ class ConversationItemDecorations(hasWallpaper: Boolean = false, private val sch
|
||||
}
|
||||
|
||||
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||
val viewHolder = parent.getChildViewHolder(view)
|
||||
|
||||
if (viewHolder is ConversationTypingIndicatorAdapter.ViewHolder) {
|
||||
outRect.set(0, 0, 0, 0)
|
||||
return
|
||||
}
|
||||
|
||||
val position = viewHolder.bindingAdapterPosition
|
||||
val position = parent.getChildAdapterPosition(view)
|
||||
|
||||
val unreadHeight = if (isFirstUnread(position)) {
|
||||
getUnreadViewHolder(parent).height
|
||||
@@ -83,15 +76,9 @@ class ConversationItemDecorations(hasWallpaper: Boolean = false, private val sch
|
||||
val count = parent.childCount
|
||||
for (layoutPosition in 0 until count) {
|
||||
val child = parent.getChildAt(count - 1 - layoutPosition)
|
||||
val viewHolder = parent.getChildViewHolder(child)
|
||||
val position = parent.getChildAdapterPosition(child)
|
||||
|
||||
if (viewHolder is ConversationTypingIndicatorAdapter.ViewHolder) {
|
||||
continue
|
||||
}
|
||||
|
||||
val bindingAdapterPosition = viewHolder.bindingAdapterPosition
|
||||
|
||||
val unreadOffset = if (isFirstUnread(bindingAdapterPosition)) {
|
||||
val unreadOffset = if (isFirstUnread(position)) {
|
||||
val unread = getUnreadViewHolder(parent)
|
||||
unread.itemView.drawAsTopItemDecoration(c, parent, child)
|
||||
unread.height
|
||||
@@ -99,8 +86,8 @@ class ConversationItemDecorations(hasWallpaper: Boolean = false, private val sch
|
||||
0
|
||||
}
|
||||
|
||||
if (hasHeader(bindingAdapterPosition)) {
|
||||
val headerView = getHeader(parent, currentItems[bindingAdapterPosition] as ConversationMessageElement).itemView
|
||||
if (hasHeader(position)) {
|
||||
val headerView = getHeader(parent, currentItems[position] as ConversationMessageElement).itemView
|
||||
headerView.drawAsTopItemDecoration(c, parent, child, unreadOffset)
|
||||
}
|
||||
}
|
||||
@@ -149,18 +136,18 @@ class ConversationItemDecorations(hasWallpaper: Boolean = false, private val sch
|
||||
}
|
||||
}
|
||||
|
||||
private fun isFirstUnread(bindingAdapterPosition: Int): Boolean {
|
||||
private fun isFirstUnread(position: Int): Boolean {
|
||||
val state = unreadState
|
||||
|
||||
return state is UnreadState.CompleteUnreadState &&
|
||||
state.firstUnreadTimestamp != null &&
|
||||
bindingAdapterPosition in currentItems.indices &&
|
||||
(currentItems[bindingAdapterPosition] as? ConversationMessageElement)?.timestamp() == state.firstUnreadTimestamp
|
||||
position in currentItems.indices &&
|
||||
(currentItems[position] as? ConversationMessageElement)?.timestamp() == state.firstUnreadTimestamp
|
||||
}
|
||||
|
||||
private fun hasHeader(bindingAdapterPosition: Int): Boolean {
|
||||
val model = if (bindingAdapterPosition in currentItems.indices) {
|
||||
currentItems[bindingAdapterPosition]
|
||||
private fun hasHeader(position: Int): Boolean {
|
||||
val model = if (position in currentItems.indices) {
|
||||
currentItems[position]
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -169,7 +156,7 @@ class ConversationItemDecorations(hasWallpaper: Boolean = false, private val sch
|
||||
return false
|
||||
}
|
||||
|
||||
val previousPosition = bindingAdapterPosition + 1
|
||||
val previousPosition = position + 1
|
||||
val previousDay: Long
|
||||
if (previousPosition in currentItems.indices) {
|
||||
val previousModel = currentItems[previousPosition]
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.signal.core.util.toInt
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ConversationTypingView
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
class ConversationTypingIndicatorAdapter(
|
||||
private val glideRequests: GlideRequests
|
||||
) : RecyclerView.Adapter<ConversationTypingIndicatorAdapter.ViewHolder>() {
|
||||
|
||||
private var state: State = State()
|
||||
|
||||
fun setState(state: State) {
|
||||
val isInsert = this.state.typists.isEmpty() && state.typists.isNotEmpty()
|
||||
val isRemoval = state.typists.isEmpty() && this.state.typists.isNotEmpty()
|
||||
val isChange = state.typists.isNotEmpty() && this.state.typists.isNotEmpty()
|
||||
|
||||
this.state = state
|
||||
|
||||
when {
|
||||
isInsert -> notifyItemInserted(0)
|
||||
isRemoval -> notifyItemRemoved(0)
|
||||
isChange -> notifyItemChanged(0)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.conversation_typing_view, parent, false) as ConversationTypingView)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = state.typists.isNotEmpty().toInt()
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(glideRequests, state)
|
||||
}
|
||||
|
||||
class ViewHolder(private val conversationTypingView: ConversationTypingView) : RecyclerView.ViewHolder(conversationTypingView) {
|
||||
fun bind(
|
||||
glideRequests: GlideRequests,
|
||||
state: State
|
||||
) {
|
||||
conversationTypingView.setTypists(
|
||||
glideRequests,
|
||||
state.typists,
|
||||
state.isGroupThread,
|
||||
state.hasWallpaper
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class State(
|
||||
val typists: List<Recipient> = emptyList(),
|
||||
val hasWallpaper: Boolean = false,
|
||||
val isGroupThread: Boolean = false,
|
||||
val isReplacedByIncomingMessage: Boolean = false // TODO
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.View.MeasureSpec
|
||||
import androidx.core.graphics.withTranslation
|
||||
import androidx.core.view.children
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.ItemDecoration
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ConversationTypingView
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
/**
|
||||
* Displays a typing indicator as a part of the very last (first) item in the adapter.
|
||||
*/
|
||||
class TypingIndicatorDecoration(
|
||||
private val context: Context,
|
||||
private val rootView: RecyclerView
|
||||
) : ItemDecoration() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(TypingIndicatorDecoration::class.java)
|
||||
}
|
||||
|
||||
private val typingView: ConversationTypingView by lazy(LazyThreadSafetyMode.NONE) {
|
||||
LayoutInflater.from(context).inflate(R.layout.conversation_typing_view, rootView, false) as ConversationTypingView
|
||||
}
|
||||
|
||||
private var displayIndicator = false
|
||||
private var animationFraction = 0f
|
||||
private var offsetAnimator: ValueAnimator? = null
|
||||
|
||||
init {
|
||||
rootView.addOnLayoutChangeListener { _, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
|
||||
if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) {
|
||||
remeasureTypingView()
|
||||
rootView.invalidateItemDecorations()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||
if (!displayIndicator && animationFraction == 0f) {
|
||||
return outRect.set(0, 0, 0, 0)
|
||||
}
|
||||
|
||||
if (parent.getChildAdapterPosition(view) == 0) {
|
||||
remeasureTypingView()
|
||||
outRect.set(0, 0, 0, (typingView.measuredHeight * animationFraction).toInt())
|
||||
} else {
|
||||
outRect.set(0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
if (!displayIndicator && offsetAnimator?.isRunning != true) {
|
||||
return
|
||||
}
|
||||
|
||||
val firstChild = parent.children.firstOrNull() ?: return
|
||||
|
||||
if (parent.getChildAdapterPosition(firstChild) == 0) {
|
||||
c.withTranslation(
|
||||
x = firstChild.left.toFloat(),
|
||||
y = firstChild.bottom.toFloat()
|
||||
) {
|
||||
typingView.draw(this)
|
||||
}
|
||||
|
||||
if (typingView.isActive) {
|
||||
rootView.post { rootView.invalidateItemDecorations() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setTypists(
|
||||
glideRequests: GlideRequests,
|
||||
typists: List<Recipient>,
|
||||
isGroupThread: Boolean,
|
||||
hasWallpaper: Boolean,
|
||||
isReplacedByIncomingMessage: Boolean
|
||||
) {
|
||||
Log.d(TAG, "setTypists: Updating typists: ${typists.size} $isGroupThread $hasWallpaper $isReplacedByIncomingMessage")
|
||||
|
||||
val isEdge = displayIndicator != typists.isNotEmpty()
|
||||
displayIndicator = typists.isNotEmpty()
|
||||
|
||||
typingView.setTypists(
|
||||
glideRequests,
|
||||
typists,
|
||||
isGroupThread,
|
||||
hasWallpaper
|
||||
)
|
||||
remeasureTypingView()
|
||||
rootView.invalidateItemDecorations()
|
||||
|
||||
if (isReplacedByIncomingMessage) {
|
||||
offsetAnimator?.cancel()
|
||||
animationFraction = 0f
|
||||
} else if (isEdge) {
|
||||
animateOffset()
|
||||
}
|
||||
}
|
||||
|
||||
private fun animateOffset() {
|
||||
offsetAnimator?.cancel()
|
||||
|
||||
val (start, end) = if (displayIndicator) {
|
||||
animationFraction to 1f
|
||||
} else {
|
||||
animationFraction to 0f
|
||||
}
|
||||
|
||||
offsetAnimator = ValueAnimator.ofFloat(start, end).apply {
|
||||
addUpdateListener {
|
||||
animationFraction = it.animatedValue as Float
|
||||
rootView.invalidateItemDecorations()
|
||||
}
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun remeasureTypingView() {
|
||||
with(typingView) {
|
||||
measure(
|
||||
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
|
||||
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
|
||||
)
|
||||
layout(
|
||||
0,
|
||||
0,
|
||||
measuredWidth,
|
||||
measuredHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.thoughtcrime.securesms.conversation.v2.computed
|
||||
|
||||
import android.content.Context
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.ConversationMessageElement
|
||||
|
||||
/**
|
||||
* Collection of workers to recompute computed ConversationMessage fields.
|
||||
*/
|
||||
object ConversationMessageComputeWorkers {
|
||||
|
||||
private val executor = SignalExecutors.newCachedSingleThreadExecutor("conversation-message-compute", ThreadUtil.PRIORITY_IMPORTANT_BACKGROUND_THREAD)
|
||||
|
||||
fun recomputeFormattedDate(
|
||||
context: Context,
|
||||
items: List<ConversationMessageElement>
|
||||
): Single<Boolean> {
|
||||
return Single.fromCallable {
|
||||
var hasUpdatedProperties = false
|
||||
for (item in items) {
|
||||
val oldDate = item.conversationMessage.computedProperties.formattedDate
|
||||
if (oldDate.isRelative) {
|
||||
val newDate = ConversationMessage.getFormattedDate(context, item.conversationMessage.messageRecord)
|
||||
item.conversationMessage.computedProperties.formattedDate = newDate
|
||||
hasUpdatedProperties = true
|
||||
}
|
||||
}
|
||||
|
||||
hasUpdatedProperties
|
||||
}.subscribeOn(Schedulers.from(executor))
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.conversation.v2.computed
|
||||
|
||||
data class FormattedDate(
|
||||
val isRelative: Boolean,
|
||||
val value: String
|
||||
)
|
||||
@@ -35,11 +35,7 @@ class V2ConversationItemLayout @JvmOverloads constructor(
|
||||
onMeasureListeners.forEach { it.onPreMeasure() }
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
|
||||
var remeasure = false
|
||||
onMeasureListeners.forEach {
|
||||
remeasure = it.onPostMeasure() || remeasure
|
||||
}
|
||||
|
||||
val remeasure = onMeasureListeners.map { it.onPostMeasure() }.any { it }
|
||||
if (remeasure) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
}
|
||||
|
||||
@@ -33,16 +33,16 @@ fun V2ConversationItemMediaIncomingBinding.bridge(): V2ConversationItemMediaBind
|
||||
senderName = groupMessageSender,
|
||||
senderPhoto = contactPhoto,
|
||||
senderBadge = badge,
|
||||
body = conversationItemBody,
|
||||
bodyWrapper = conversationItemBodyWrapper,
|
||||
reply = conversationItemReply,
|
||||
reactions = conversationItemReactions,
|
||||
deliveryStatus = null,
|
||||
footerDate = conversationItemFooterDate,
|
||||
footerExpiry = conversationItemExpirationTimer,
|
||||
footerBackground = conversationItemFooterBackground,
|
||||
alert = null,
|
||||
footerSpace = null,
|
||||
conversationItemBody = conversationItemBody,
|
||||
conversationItemBodyWrapper = conversationItemBodyWrapper,
|
||||
conversationItemReply = conversationItemReply,
|
||||
conversationItemReactions = conversationItemReactions,
|
||||
conversationItemDeliveryStatus = null,
|
||||
conversationItemFooterDate = conversationItemFooterDate,
|
||||
conversationItemFooterExpiry = conversationItemExpirationTimer,
|
||||
conversationItemFooterBackground = conversationItemFooterBackground,
|
||||
conversationItemAlert = null,
|
||||
conversationItemFooterSpace = null,
|
||||
isIncoming = true
|
||||
)
|
||||
|
||||
@@ -63,16 +63,16 @@ fun V2ConversationItemMediaOutgoingBinding.bridge(): V2ConversationItemMediaBind
|
||||
senderName = null,
|
||||
senderPhoto = null,
|
||||
senderBadge = null,
|
||||
body = conversationItemBody,
|
||||
bodyWrapper = conversationItemBodyWrapper,
|
||||
reply = conversationItemReply,
|
||||
reactions = conversationItemReactions,
|
||||
deliveryStatus = conversationItemDeliveryStatus,
|
||||
footerDate = conversationItemFooterDate,
|
||||
footerExpiry = conversationItemExpirationTimer,
|
||||
footerBackground = conversationItemFooterBackground,
|
||||
alert = conversationItemAlert,
|
||||
footerSpace = footerEndPad,
|
||||
conversationItemBody = conversationItemBody,
|
||||
conversationItemBodyWrapper = conversationItemBodyWrapper,
|
||||
conversationItemReply = conversationItemReply,
|
||||
conversationItemReactions = conversationItemReactions,
|
||||
conversationItemDeliveryStatus = conversationItemDeliveryStatus,
|
||||
conversationItemFooterDate = conversationItemFooterDate,
|
||||
conversationItemFooterExpiry = conversationItemExpirationTimer,
|
||||
conversationItemFooterBackground = conversationItemFooterBackground,
|
||||
conversationItemAlert = conversationItemAlert,
|
||||
conversationItemFooterSpace = footerEndPad,
|
||||
isIncoming = false
|
||||
)
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class V2ConversationItemMediaViewHolder<Model : MappingModel<Model>>(
|
||||
) {
|
||||
|
||||
init {
|
||||
binding.textBridge.bodyWrapper.clipToOutline = true
|
||||
binding.textBridge.conversationItemBodyWrapper.clipToOutline = true
|
||||
}
|
||||
|
||||
override fun bind(model: Model) {
|
||||
@@ -57,7 +57,7 @@ class V2ConversationItemMediaViewHolder<Model : MappingModel<Model>>(
|
||||
0
|
||||
}
|
||||
|
||||
this.constrainMaxWidth(binding.textBridge.bodyWrapper.id, maxBodyWidth)
|
||||
this.constrainMaxWidth(binding.textBridge.conversationItemBodyWrapper.id, maxBodyWidth)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,12 +18,12 @@ class V2ConversationItemSnapshotStrategy(
|
||||
) : InteractiveConversationElement.SnapshotStrategy {
|
||||
|
||||
private val viewsToRestoreScale = listOfNotNull(
|
||||
binding.bodyWrapper,
|
||||
binding.footerBackground,
|
||||
binding.footerDate,
|
||||
binding.footerExpiry,
|
||||
binding.deliveryStatus,
|
||||
binding.reactions
|
||||
binding.conversationItemBodyWrapper,
|
||||
binding.conversationItemFooterBackground,
|
||||
binding.conversationItemFooterDate,
|
||||
binding.conversationItemFooterExpiry,
|
||||
binding.conversationItemDeliveryStatus,
|
||||
binding.conversationItemReactions
|
||||
)
|
||||
|
||||
private val viewsToHide = listOfNotNull(
|
||||
@@ -33,7 +33,7 @@ class V2ConversationItemSnapshotStrategy(
|
||||
|
||||
override val snapshotMetrics = InteractiveConversationElement.SnapshotMetrics(
|
||||
snapshotOffset = 0f,
|
||||
contextMenuPadding = binding.bodyWrapper.x
|
||||
contextMenuPadding = binding.conversationItemBodyWrapper.x
|
||||
)
|
||||
|
||||
override fun snapshot(canvas: Canvas) {
|
||||
|
||||
@@ -31,16 +31,16 @@ data class V2ConversationItemTextOnlyBindingBridge(
|
||||
val senderName: EmojiTextView?,
|
||||
val senderPhoto: AvatarImageView?,
|
||||
val senderBadge: BadgeImageView?,
|
||||
val bodyWrapper: ViewGroup,
|
||||
val body: EmojiTextView,
|
||||
val reply: ShapeableImageView,
|
||||
val reactions: ReactionsConversationView,
|
||||
val deliveryStatus: DeliveryStatusView?,
|
||||
val footerDate: TextView,
|
||||
val footerExpiry: ExpirationTimerView,
|
||||
val footerBackground: View,
|
||||
val footerSpace: Space?,
|
||||
val alert: AlertView?,
|
||||
val conversationItemBodyWrapper: ViewGroup,
|
||||
val conversationItemBody: EmojiTextView,
|
||||
val conversationItemReply: ShapeableImageView,
|
||||
val conversationItemReactions: ReactionsConversationView,
|
||||
val conversationItemDeliveryStatus: DeliveryStatusView?,
|
||||
val conversationItemFooterDate: TextView,
|
||||
val conversationItemFooterExpiry: ExpirationTimerView,
|
||||
val conversationItemFooterBackground: View,
|
||||
val conversationItemFooterSpace: Space?,
|
||||
val conversationItemAlert: AlertView?,
|
||||
val isIncoming: Boolean
|
||||
)
|
||||
|
||||
@@ -53,16 +53,16 @@ fun V2ConversationItemTextOnlyIncomingBinding.bridge(): V2ConversationItemTextOn
|
||||
senderName = groupMessageSender,
|
||||
senderPhoto = contactPhoto,
|
||||
senderBadge = badge,
|
||||
body = conversationItemBody,
|
||||
bodyWrapper = conversationItemBodyWrapper,
|
||||
reply = conversationItemReply,
|
||||
reactions = conversationItemReactions,
|
||||
deliveryStatus = null,
|
||||
footerDate = conversationItemFooterDate,
|
||||
footerExpiry = conversationItemExpirationTimer,
|
||||
footerBackground = conversationItemFooterBackground,
|
||||
alert = null,
|
||||
footerSpace = footerEndPad,
|
||||
conversationItemBody = conversationItemBody,
|
||||
conversationItemBodyWrapper = conversationItemBodyWrapper,
|
||||
conversationItemReply = conversationItemReply,
|
||||
conversationItemReactions = conversationItemReactions,
|
||||
conversationItemDeliveryStatus = null,
|
||||
conversationItemFooterDate = conversationItemFooterDate,
|
||||
conversationItemFooterExpiry = conversationItemExpirationTimer,
|
||||
conversationItemFooterBackground = conversationItemFooterBackground,
|
||||
conversationItemAlert = null,
|
||||
conversationItemFooterSpace = null,
|
||||
isIncoming = true
|
||||
)
|
||||
}
|
||||
@@ -76,16 +76,16 @@ fun V2ConversationItemTextOnlyOutgoingBinding.bridge(): V2ConversationItemTextOn
|
||||
senderName = null,
|
||||
senderPhoto = null,
|
||||
senderBadge = null,
|
||||
body = conversationItemBody,
|
||||
bodyWrapper = conversationItemBodyWrapper,
|
||||
reply = conversationItemReply,
|
||||
reactions = conversationItemReactions,
|
||||
deliveryStatus = conversationItemDeliveryStatus,
|
||||
footerDate = conversationItemFooterDate,
|
||||
footerExpiry = conversationItemExpirationTimer,
|
||||
footerBackground = conversationItemFooterBackground,
|
||||
alert = conversationItemAlert,
|
||||
footerSpace = footerEndPad,
|
||||
conversationItemBody = conversationItemBody,
|
||||
conversationItemBodyWrapper = conversationItemBodyWrapper,
|
||||
conversationItemReply = conversationItemReply,
|
||||
conversationItemReactions = conversationItemReactions,
|
||||
conversationItemDeliveryStatus = conversationItemDeliveryStatus,
|
||||
conversationItemFooterDate = conversationItemFooterDate,
|
||||
conversationItemFooterExpiry = conversationItemExpirationTimer,
|
||||
conversationItemFooterBackground = conversationItemFooterBackground,
|
||||
conversationItemAlert = conversationItemAlert,
|
||||
conversationItemFooterSpace = footerEndPad,
|
||||
isIncoming = false
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.items
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.Typeface
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableStringBuilder
|
||||
@@ -52,7 +51,6 @@ import org.thoughtcrime.securesms.util.ThemeUtil
|
||||
import org.thoughtcrime.securesms.util.VibrateUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.hasExtraText
|
||||
import org.thoughtcrime.securesms.util.hasNoBubble
|
||||
import org.thoughtcrime.securesms.util.isScheduled
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import java.util.Locale
|
||||
@@ -82,28 +80,26 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
override lateinit var conversationMessage: ConversationMessage
|
||||
|
||||
override val root: ViewGroup = binding.root
|
||||
override val bubbleView: View = binding.bodyWrapper
|
||||
override val bubbleView: View = binding.conversationItemBodyWrapper
|
||||
|
||||
override val bubbleViews: List<View> = listOfNotNull(
|
||||
binding.bodyWrapper,
|
||||
binding.footerDate,
|
||||
binding.footerExpiry,
|
||||
binding.deliveryStatus,
|
||||
binding.footerBackground
|
||||
binding.conversationItemBodyWrapper,
|
||||
binding.conversationItemFooterDate,
|
||||
binding.conversationItemFooterExpiry,
|
||||
binding.conversationItemDeliveryStatus,
|
||||
binding.conversationItemFooterBackground
|
||||
)
|
||||
|
||||
override val reactionsView: View = binding.reactions
|
||||
override val reactionsView: View = binding.conversationItemReactions
|
||||
override val quotedIndicatorView: View? = null
|
||||
override val replyView: View = binding.reply
|
||||
override val replyView: View = binding.conversationItemReply
|
||||
override val contactPhotoHolderView: View? = binding.senderPhoto
|
||||
override val badgeImageView: View? = binding.senderBadge
|
||||
|
||||
private var reactionMeasureListener: ReactionMeasureListener = ReactionMeasureListener()
|
||||
private var dateString: String = ""
|
||||
|
||||
private val bodyBubbleDrawable = ChatColorsDrawable()
|
||||
private val footerDrawable = ChatColorsDrawable()
|
||||
private val senderDrawable = ChatColorsDrawable()
|
||||
private val bodyBubbleLayoutTransition = BodyBubbleLayoutTransition()
|
||||
|
||||
protected lateinit var shape: V2ConversationItemShape.MessageShape
|
||||
@@ -112,21 +108,21 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
override fun onPreMeasure() = Unit
|
||||
|
||||
override fun onPostMeasure(): Boolean {
|
||||
val wrapperHeight = binding.conversationItemBodyWrapper.measuredHeight
|
||||
val yTranslation = (wrapperHeight - 38.dp) / 2f
|
||||
binding.conversationItemReply.translationY = -yTranslation
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
binding.root.addOnMeasureListener(footerDelegate)
|
||||
binding.bodyWrapper.addOnLayoutChangeListener { _, _, top, _, bottom, _, _, _, _ ->
|
||||
val wrapperHeight = bottom - top
|
||||
val yTranslation = (wrapperHeight - 38.dp) / 2f
|
||||
binding.reply.translationY = -yTranslation
|
||||
}
|
||||
binding.root.addOnMeasureListener(replyDelegate)
|
||||
|
||||
binding.root.onDispatchTouchEventListener = dispatchTouchEventListener
|
||||
|
||||
binding.reactions.setOnClickListener {
|
||||
binding.conversationItemReactions.setOnClickListener {
|
||||
conversationContext.clickListener
|
||||
.onReactionClicked(
|
||||
Multiselect.getParts(conversationMessage).asSingle().singlePart,
|
||||
@@ -146,23 +142,23 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
}
|
||||
|
||||
val passthroughClickListener = PassthroughClickListener()
|
||||
binding.body.setOnClickListener(passthroughClickListener)
|
||||
binding.body.setOnLongClickListener(passthroughClickListener)
|
||||
binding.conversationItemBody.setOnClickListener(passthroughClickListener)
|
||||
binding.conversationItemBody.setOnLongClickListener(passthroughClickListener)
|
||||
|
||||
binding.body.isFocusable = false
|
||||
binding.body.setTextSize(TypedValue.COMPLEX_UNIT_SP, SignalStore.settings().messageFontSize.toFloat())
|
||||
binding.body.movementMethod = LongClickMovementMethod.getInstance(context)
|
||||
binding.conversationItemBody.isFocusable = false
|
||||
binding.conversationItemBody.setTextSize(TypedValue.COMPLEX_UNIT_SP, SignalStore.settings().messageFontSize.toFloat())
|
||||
binding.conversationItemBody.movementMethod = LongClickMovementMethod.getInstance(context)
|
||||
|
||||
if (binding.isIncoming) {
|
||||
binding.body.setMentionBackgroundTint(ContextCompat.getColor(context, if (ThemeUtil.isDarkTheme(context)) R.color.core_grey_60 else R.color.core_grey_20))
|
||||
binding.conversationItemBody.setMentionBackgroundTint(ContextCompat.getColor(context, if (ThemeUtil.isDarkTheme(context)) R.color.core_grey_60 else R.color.core_grey_20))
|
||||
} else {
|
||||
binding.body.setMentionBackgroundTint(ContextCompat.getColor(context, R.color.transparent_black_25))
|
||||
binding.conversationItemBody.setMentionBackgroundTint(ContextCompat.getColor(context, R.color.transparent_black_25))
|
||||
}
|
||||
|
||||
binding.bodyWrapper.background = bodyBubbleDrawable
|
||||
binding.bodyWrapper.layoutTransition = bodyBubbleLayoutTransition
|
||||
binding.conversationItemBodyWrapper.background = bodyBubbleDrawable
|
||||
binding.conversationItemBodyWrapper.layoutTransition = bodyBubbleLayoutTransition
|
||||
|
||||
binding.footerBackground.background = footerDrawable
|
||||
binding.conversationItemFooterBackground.background = footerDrawable
|
||||
}
|
||||
|
||||
override fun invalidateChatColorsDrawable(coordinateRoot: ViewGroup) {
|
||||
@@ -173,7 +169,7 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
override fun bind(model: Model) {
|
||||
var hasProcessedSupportedPayload = false
|
||||
|
||||
binding.bodyWrapper.layoutTransition = if (conversationContext.isParentInScroll) {
|
||||
binding.conversationItemBodyWrapper.layoutTransition = if (conversationContext.isParentInScroll) {
|
||||
null
|
||||
} else {
|
||||
bodyBubbleLayoutTransition
|
||||
@@ -198,9 +194,7 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
)
|
||||
|
||||
if (ConversationAdapterBridge.PAYLOAD_TIMESTAMP in payload) {
|
||||
if (conversationMessage.computedProperties.formattedDate.value != dateString) {
|
||||
presentDate()
|
||||
}
|
||||
presentDate()
|
||||
hasProcessedSupportedPayload = true
|
||||
}
|
||||
|
||||
@@ -223,15 +217,13 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
presentDeliveryStatus()
|
||||
presentFooterBackground()
|
||||
presentFooterExpiry()
|
||||
presentFooterEndPadding()
|
||||
presentAlert()
|
||||
presentSender()
|
||||
presentSenderNameColor()
|
||||
presentSenderNameBackground()
|
||||
presentReactions()
|
||||
|
||||
bodyBubbleDrawable.setChatColors(
|
||||
if (binding.body.isJumbomoji) {
|
||||
if (binding.conversationItemBody.isJumbomoji) {
|
||||
transparentChatColors
|
||||
} else if (binding.isIncoming) {
|
||||
ChatColors.forColor(ChatColors.Id.NotSet, themeDelegate.getBodyBubbleColor(conversationMessage))
|
||||
@@ -241,7 +233,7 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
shapeDelegate.corners
|
||||
)
|
||||
|
||||
binding.reply.setBackgroundColor(themeDelegate.getReplyIconBackgroundColor())
|
||||
binding.conversationItemReply.setBackgroundColor(themeDelegate.getReplyIconBackgroundColor())
|
||||
|
||||
itemView.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = shape.topPadding.toInt()
|
||||
@@ -265,9 +257,9 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
projections.add(
|
||||
Projection.relativeToParent(
|
||||
coordinateRoot,
|
||||
binding.bodyWrapper,
|
||||
binding.conversationItemBodyWrapper,
|
||||
shapeDelegate.corners
|
||||
).translateX(binding.bodyWrapper.translationX).translateY(root.translationY)
|
||||
).translateX(binding.conversationItemBodyWrapper.translationX).translateY(root.translationY)
|
||||
)
|
||||
|
||||
return projections
|
||||
@@ -305,7 +297,7 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
} else if (conversationMessage.threadRecipient.isGroup) {
|
||||
binding.senderPhoto
|
||||
} else {
|
||||
binding.bodyWrapper
|
||||
binding.conversationItemBodyWrapper
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,11 +309,11 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
override fun getGiphyMp4PlayableProjection(coordinateRoot: ViewGroup): Projection {
|
||||
return Projection.relativeToParent(
|
||||
coordinateRoot,
|
||||
binding.bodyWrapper,
|
||||
binding.conversationItemBodyWrapper,
|
||||
shapeDelegate.corners
|
||||
)
|
||||
.translateY(root.translationY)
|
||||
.translateX(binding.bodyWrapper.translationX)
|
||||
.translateX(binding.conversationItemBodyWrapper.translationX)
|
||||
.translateX(root.translationX)
|
||||
}
|
||||
|
||||
@@ -336,7 +328,7 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
|
||||
val projection = Projection.relativeToParent(
|
||||
coordinateRoot,
|
||||
binding.footerBackground,
|
||||
binding.conversationItemFooterBackground,
|
||||
shapeDelegate.corners
|
||||
)
|
||||
|
||||
@@ -351,7 +343,7 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
|
||||
val projection = Projection.relativeToParent(
|
||||
coordinateRoot,
|
||||
binding.bodyWrapper,
|
||||
binding.conversationItemBodyWrapper,
|
||||
shapeDelegate.corners
|
||||
)
|
||||
|
||||
@@ -364,8 +356,8 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
}
|
||||
|
||||
private fun presentBody() {
|
||||
binding.body.setTextColor(themeDelegate.getBodyTextColor(conversationMessage))
|
||||
binding.body.setLinkTextColor(themeDelegate.getBodyTextColor(conversationMessage))
|
||||
binding.conversationItemBody.setTextColor(themeDelegate.getBodyTextColor(conversationMessage))
|
||||
binding.conversationItemBody.setLinkTextColor(themeDelegate.getBodyTextColor(conversationMessage))
|
||||
|
||||
val record = conversationMessage.messageRecord
|
||||
var styledText: Spannable = conversationMessage.getDisplayBody(context)
|
||||
@@ -375,21 +367,21 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
|
||||
styledText = SearchUtil.getHighlightedSpan(Locale.getDefault(), STYLE_FACTORY, styledText, conversationContext.searchQuery, SearchUtil.STRICT)
|
||||
if (record.hasExtraText()) {
|
||||
binding.body.setOverflowText(getLongMessageSpan())
|
||||
binding.conversationItemBody.setOverflowText(getLongMessageSpan())
|
||||
} else {
|
||||
binding.body.setOverflowText(null)
|
||||
binding.conversationItemBody.setOverflowText(null)
|
||||
}
|
||||
|
||||
if (isContentCondensed()) {
|
||||
binding.body.maxLines = CONDENSED_MODE_MAX_LINES
|
||||
binding.conversationItemBody.maxLines = CONDENSED_MODE_MAX_LINES
|
||||
} else {
|
||||
binding.body.maxLines = Integer.MAX_VALUE
|
||||
binding.conversationItemBody.maxLines = Integer.MAX_VALUE
|
||||
}
|
||||
|
||||
val bodyText = StringUtil.trim(styledText)
|
||||
|
||||
binding.body.visible = bodyText.isNotEmpty()
|
||||
binding.body.text = bodyText
|
||||
binding.conversationItemBody.visible = bodyText.isNotEmpty()
|
||||
binding.conversationItemBody.text = bodyText
|
||||
}
|
||||
|
||||
private fun linkifyMessageBody(messageBody: Spannable) {
|
||||
@@ -456,17 +448,23 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
}
|
||||
|
||||
private fun isContentCondensed(): Boolean {
|
||||
return conversationContext.displayMode is ConversationItemDisplayMode.Condensed && conversationContext.getPreviousMessage(bindingAdapterPosition) == null
|
||||
return conversationContext.displayMode == ConversationItemDisplayMode.CONDENSED && conversationContext.getPreviousMessage(bindingAdapterPosition) == null
|
||||
}
|
||||
|
||||
private fun presentFooterExpiry() {
|
||||
val timer = binding.footerExpiry
|
||||
if (shape == V2ConversationItemShape.MessageShape.MIDDLE || shape == V2ConversationItemShape.MessageShape.START) {
|
||||
binding.conversationItemFooterExpiry.stopAnimation()
|
||||
binding.conversationItemFooterExpiry.visible = false
|
||||
return
|
||||
}
|
||||
|
||||
binding.conversationItemFooterExpiry.setColorFilter(themeDelegate.getFooterIconColor(conversationMessage))
|
||||
|
||||
val timer = binding.conversationItemFooterExpiry
|
||||
val record = conversationMessage.messageRecord
|
||||
if (record.expiresIn > 0 && !record.isPending) {
|
||||
timer.setColorFilter(themeDelegate.getFooterTextColor(conversationMessage), PorterDuff.Mode.SRC_IN)
|
||||
|
||||
timer.visible = true
|
||||
timer.setPercentComplete(0f)
|
||||
binding.conversationItemFooterExpiry.visible = true
|
||||
binding.conversationItemFooterExpiry.setPercentComplete(0f)
|
||||
|
||||
if (record.expireStarted > 0) {
|
||||
timer.setExpirationTime(record.expireStarted, record.expiresIn)
|
||||
@@ -479,36 +477,10 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
conversationContext.onStartExpirationTimeout(record)
|
||||
}
|
||||
} else {
|
||||
timer.stopAnimation()
|
||||
timer.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentFooterEndPadding() {
|
||||
binding.footerSpace?.visibility = if (isForcedFooter() || shape.isEndingShape) {
|
||||
View.INVISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentSenderNameBackground() {
|
||||
if (binding.senderName == null || !shape.isStartingShape || !conversationMessage.threadRecipient.isGroup || !conversationMessage.messageRecord.hasNoBubble(context)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (conversationContext.hasWallpaper()) {
|
||||
senderDrawable.setChatColors(
|
||||
ChatColors.forColor(ChatColors.Id.BuiltIn, themeDelegate.getFooterBubbleColor(conversationMessage)),
|
||||
footerCorners
|
||||
)
|
||||
|
||||
binding.senderName.background = senderDrawable
|
||||
} else {
|
||||
binding.senderName.background = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentSender() {
|
||||
if (binding.senderName == null || binding.senderPhoto == null || binding.senderBadge == null) {
|
||||
return
|
||||
@@ -553,14 +525,14 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
|
||||
private fun presentAlert() {
|
||||
val record = conversationMessage.messageRecord
|
||||
binding.body.setCompoundDrawablesWithIntrinsicBounds(
|
||||
binding.conversationItemBody.setCompoundDrawablesWithIntrinsicBounds(
|
||||
0,
|
||||
0,
|
||||
if (record.isKeyExchange) R.drawable.ic_menu_login else 0,
|
||||
0
|
||||
)
|
||||
|
||||
val alert = binding.alert ?: return
|
||||
val alert = binding.conversationItemAlert ?: return
|
||||
|
||||
when {
|
||||
record.isFailed -> alert.setFailed()
|
||||
@@ -578,26 +550,25 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
|
||||
private fun presentReactions() {
|
||||
if (conversationMessage.messageRecord.reactions.isEmpty()) {
|
||||
binding.reactions.clear()
|
||||
binding.conversationItemReactions.clear()
|
||||
binding.root.removeOnMeasureListener(reactionMeasureListener)
|
||||
} else {
|
||||
binding.reactions.setReactions(conversationMessage.messageRecord.reactions)
|
||||
binding.conversationItemReactions.setReactions(conversationMessage.messageRecord.reactions)
|
||||
binding.root.addOnMeasureListener(reactionMeasureListener)
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentFooterBackground() {
|
||||
if (!binding.body.isJumbomoji || !conversationContext.hasWallpaper()) {
|
||||
binding.footerBackground.visible = false
|
||||
if (!binding.conversationItemBody.isJumbomoji ||
|
||||
!conversationContext.hasWallpaper() ||
|
||||
shape == V2ConversationItemShape.MessageShape.MIDDLE ||
|
||||
shape == V2ConversationItemShape.MessageShape.START
|
||||
) {
|
||||
binding.conversationItemFooterBackground.visible = false
|
||||
return
|
||||
}
|
||||
|
||||
if (!(isForcedFooter() || shape.isEndingShape)) {
|
||||
binding.footerBackground.visible = false
|
||||
return
|
||||
}
|
||||
|
||||
binding.footerBackground.visible = true
|
||||
binding.conversationItemFooterBackground.visible = true
|
||||
footerDrawable.setChatColors(
|
||||
if (binding.isIncoming) {
|
||||
ChatColors.forColor(ChatColors.Id.NotSet, themeDelegate.getFooterBubbleColor(conversationMessage))
|
||||
@@ -609,16 +580,13 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
}
|
||||
|
||||
private fun presentDate() {
|
||||
if (!shape.isEndingShape && !isForcedFooter()) {
|
||||
binding.footerDate.visible = false
|
||||
if (shape == V2ConversationItemShape.MessageShape.MIDDLE || shape == V2ConversationItemShape.MessageShape.START) {
|
||||
binding.conversationItemFooterDate.visible = false
|
||||
return
|
||||
}
|
||||
|
||||
dateString = conversationMessage.computedProperties.formattedDate.value
|
||||
|
||||
binding.footerDate.setOnClickListener(null)
|
||||
binding.footerDate.visible = true
|
||||
binding.footerDate.setTextColor(themeDelegate.getFooterTextColor(conversationMessage))
|
||||
binding.conversationItemFooterDate.visible = true
|
||||
binding.conversationItemFooterDate.setTextColor(themeDelegate.getFooterTextColor(conversationMessage))
|
||||
|
||||
val record = conversationMessage.messageRecord
|
||||
if (record.isFailed) {
|
||||
@@ -628,31 +596,27 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
else -> R.string.ConversationItem_error_not_sent_tap_for_details
|
||||
}
|
||||
|
||||
binding.footerDate.setText(errorMessage)
|
||||
binding.conversationItemFooterDate.setText(errorMessage)
|
||||
} else if (record.isPendingInsecureSmsFallback) {
|
||||
binding.footerDate.setText(R.string.ConversationItem_click_to_approve_unencrypted)
|
||||
binding.conversationItemFooterDate.setText(R.string.ConversationItem_click_to_approve_unencrypted)
|
||||
} else if (record.isRateLimited) {
|
||||
binding.footerDate.setText(R.string.ConversationItem_send_paused)
|
||||
binding.conversationItemFooterDate.setText(R.string.ConversationItem_send_paused)
|
||||
} else if (record.isScheduled()) {
|
||||
binding.footerDate.text = conversationMessage.computedProperties.formattedDate.value
|
||||
binding.conversationItemFooterDate.text = conversationMessage.formattedDate
|
||||
} else {
|
||||
var date = dateString
|
||||
if (conversationContext.displayMode != ConversationItemDisplayMode.Detailed && record is MediaMmsMessageRecord && record.isEditMessage()) {
|
||||
var date = conversationMessage.formattedDate
|
||||
if (conversationContext.displayMode != ConversationItemDisplayMode.DETAILED && record is MediaMmsMessageRecord && record.isEditMessage()) {
|
||||
date = getContext().getString(R.string.ConversationItem_edited_timestamp_footer, date)
|
||||
|
||||
binding.footerDate.setOnClickListener {
|
||||
conversationContext.clickListener.onEditedIndicatorClicked(record)
|
||||
}
|
||||
}
|
||||
|
||||
binding.footerDate.text = date
|
||||
binding.conversationItemFooterDate.text = date
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentDeliveryStatus() {
|
||||
val deliveryStatus = binding.deliveryStatus ?: return
|
||||
val deliveryStatus = binding.conversationItemDeliveryStatus ?: return
|
||||
|
||||
if (!shape.isEndingShape && !isForcedFooter()) {
|
||||
if (shape == V2ConversationItemShape.MessageShape.MIDDLE || shape == V2ConversationItemShape.MessageShape.START) {
|
||||
deliveryStatus.setNone()
|
||||
return
|
||||
}
|
||||
@@ -703,15 +667,11 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
return false
|
||||
}
|
||||
|
||||
private fun isForcedFooter(): Boolean {
|
||||
return conversationMessage.messageRecord.isEditMessage || conversationMessage.messageRecord.expiresIn > 0L
|
||||
}
|
||||
|
||||
private inner class ReactionMeasureListener : V2ConversationItemLayout.OnMeasureListener {
|
||||
override fun onPreMeasure() = Unit
|
||||
|
||||
override fun onPostMeasure(): Boolean {
|
||||
return binding.reactions.setBubbleWidth(binding.bodyWrapper.measuredWidth)
|
||||
return binding.conversationItemReactions.setBubbleWidth(binding.conversationItemBodyWrapper.measuredWidth)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -721,7 +681,7 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
}
|
||||
|
||||
override fun onLongClick(v: View?): Boolean {
|
||||
if (binding.body.hasSelection()) {
|
||||
if (binding.conversationItemBody.hasSelection()) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user