mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-05 01:10:48 +00:00
Convert InternalConversationSettings to compose.
This commit is contained in:
1
.idea/codeStyles/Project.xml
generated
1
.idea/codeStyles/Project.xml
generated
@@ -16,6 +16,7 @@
|
||||
<option name="ALIGN_MULTILINE_TEXT_BLOCKS" value="true" />
|
||||
<option name="IMPORT_LAYOUT_TABLE">
|
||||
<value>
|
||||
<package name="" withSubpackages="true" static="false" module="true" />
|
||||
<package name="android" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="androidx" withSubpackages="true" static="false" />
|
||||
|
||||
@@ -1,38 +1,32 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.text.TextUtils
|
||||
import android.widget.Toast
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.Hex
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.isAbsent
|
||||
import org.signal.core.util.orNull
|
||||
import org.signal.core.util.roundedString
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.MessageType
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.database.model.RecipientRecord
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.IncomingMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
@@ -41,10 +35,7 @@ import org.thoughtcrime.securesms.recipients.RecipientForeverObserver
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import java.util.Objects
|
||||
import kotlin.random.Random
|
||||
import kotlin.time.Duration.Companion.nanoseconds
|
||||
@@ -53,9 +44,8 @@ import kotlin.time.DurationUnit
|
||||
/**
|
||||
* Shows internal details about a recipient that you can view from the conversation settings.
|
||||
*/
|
||||
class InternalConversationSettingsFragment : DSLSettingsFragment(
|
||||
titleId = R.string.ConversationSettingsFragment__internal_details
|
||||
) {
|
||||
@Stable
|
||||
class InternalConversationSettingsFragment : ComposeFragment(), InternalConversationSettingsScreenCallbacks {
|
||||
|
||||
private val viewModel: InternalViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
@@ -64,378 +54,14 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
|
||||
}
|
||||
)
|
||||
|
||||
override fun bindAdapter(adapter: MappingAdapter) {
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state: InternalConversationSettingsState by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
private fun getConfiguration(state: InternalState): DSLConfiguration {
|
||||
val recipient = state.recipient
|
||||
return configure {
|
||||
sectionHeaderPref(DSLSettingsText.from("Data"))
|
||||
|
||||
textPref(
|
||||
title = DSLSettingsText.from("RecipientId"),
|
||||
summary = DSLSettingsText.from(recipient.id.serialize())
|
||||
)
|
||||
|
||||
if (!recipient.isGroup) {
|
||||
val e164: String = recipient.e164.orElse("null")
|
||||
longClickPref(
|
||||
title = DSLSettingsText.from("E164"),
|
||||
summary = DSLSettingsText.from(e164),
|
||||
onLongClick = { copyToClipboard(e164) }
|
||||
)
|
||||
|
||||
val aci: String = recipient.aci.map { it.toString() }.orElse("null")
|
||||
longClickPref(
|
||||
title = DSLSettingsText.from("ACI"),
|
||||
summary = DSLSettingsText.from(aci),
|
||||
onLongClick = { copyToClipboard(aci) }
|
||||
)
|
||||
|
||||
val pni: String = recipient.pni.map { it.toString() }.orElse("null")
|
||||
longClickPref(
|
||||
title = DSLSettingsText.from("PNI"),
|
||||
summary = DSLSettingsText.from(pni),
|
||||
onLongClick = { copyToClipboard(pni) }
|
||||
)
|
||||
}
|
||||
|
||||
if (state.groupId != null) {
|
||||
val groupId: String = state.groupId.toString()
|
||||
longClickPref(
|
||||
title = DSLSettingsText.from("GroupId"),
|
||||
summary = DSLSettingsText.from(groupId),
|
||||
onLongClick = { copyToClipboard(groupId) }
|
||||
)
|
||||
}
|
||||
|
||||
val threadId: String = if (state.threadId != null) state.threadId.toString() else "N/A"
|
||||
longClickPref(
|
||||
title = DSLSettingsText.from("ThreadId"),
|
||||
summary = DSLSettingsText.from(threadId),
|
||||
onLongClick = { copyToClipboard(threadId) }
|
||||
)
|
||||
|
||||
if (!recipient.isGroup) {
|
||||
textPref(
|
||||
title = DSLSettingsText.from("Profile Name"),
|
||||
summary = DSLSettingsText.from("[${recipient.profileName.givenName}] [${state.recipient.profileName.familyName}]")
|
||||
)
|
||||
|
||||
val profileKeyBase64 = recipient.profileKey?.let(Base64::encodeWithPadding) ?: "None"
|
||||
longClickPref(
|
||||
title = DSLSettingsText.from("Profile Key (Base64)"),
|
||||
summary = DSLSettingsText.from(profileKeyBase64),
|
||||
onLongClick = { copyToClipboard(profileKeyBase64) }
|
||||
)
|
||||
|
||||
val profileKeyHex = recipient.profileKey?.let(Hex::toStringCondensed) ?: ""
|
||||
longClickPref(
|
||||
title = DSLSettingsText.from("Profile Key (Hex)"),
|
||||
summary = DSLSettingsText.from(profileKeyHex),
|
||||
onLongClick = { copyToClipboard(profileKeyHex) }
|
||||
)
|
||||
|
||||
textPref(
|
||||
title = DSLSettingsText.from("Sealed Sender Mode"),
|
||||
summary = DSLSettingsText.from(recipient.sealedSenderAccessMode.toString())
|
||||
)
|
||||
|
||||
textPref(
|
||||
title = DSLSettingsText.from("Phone Number Sharing"),
|
||||
summary = DSLSettingsText.from(recipient.phoneNumberSharing.name)
|
||||
)
|
||||
|
||||
textPref(
|
||||
title = DSLSettingsText.from("Phone Number Discoverability"),
|
||||
summary = DSLSettingsText.from(SignalDatabase.recipients.getPhoneNumberDiscoverability(recipient.id)?.name ?: "null")
|
||||
)
|
||||
}
|
||||
|
||||
textPref(
|
||||
title = DSLSettingsText.from("Profile Sharing (AKA \"Whitelisted\")"),
|
||||
summary = DSLSettingsText.from(recipient.isProfileSharing.toString())
|
||||
)
|
||||
|
||||
if (!recipient.isGroup) {
|
||||
textPref(
|
||||
title = DSLSettingsText.from("Capabilities"),
|
||||
summary = DSLSettingsText.from(buildCapabilitySpan(recipient))
|
||||
)
|
||||
}
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from("Trigger Thread Update"),
|
||||
summary = DSLSettingsText.from("Triggers a thread update. Useful for testing perf."),
|
||||
onClick = {
|
||||
val startTimeNanos = System.nanoTime()
|
||||
SignalDatabase.threads.update(state.threadId ?: -1, true)
|
||||
val endTimeNanos = System.nanoTime()
|
||||
Toast.makeText(context, "Thread update took ${(endTimeNanos - startTimeNanos).nanoseconds.toDouble(DurationUnit.MILLISECONDS).roundedString(2)} ms", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
)
|
||||
|
||||
if (!recipient.isGroup) {
|
||||
sectionHeaderPref(DSLSettingsText.from("Actions"))
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from("Disable Profile Sharing"),
|
||||
summary = DSLSettingsText.from("Clears profile sharing/whitelisted status, which should cause the Message Request UI to show."),
|
||||
onClick = {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle("Are you sure?")
|
||||
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> SignalDatabase.recipients.setProfileSharing(recipient.id, false) }
|
||||
.show()
|
||||
}
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from("Delete Sessions"),
|
||||
summary = DSLSettingsText.from("Deletes all sessions with this recipient, essentially guaranteeing an encryption error if they send you a message."),
|
||||
onClick = {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle("Are you sure?")
|
||||
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
if (recipient.hasAci) {
|
||||
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account.requireAci(), addressName = recipient.requireAci().toString())
|
||||
}
|
||||
if (recipient.hasPni) {
|
||||
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account.requireAci(), addressName = recipient.requirePni().toString())
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from("Archive Sessions"),
|
||||
summary = DSLSettingsText.from("Archives all sessions associated with this recipient, causing you to create a new session the next time you send a message (while not causing decryption errors)."),
|
||||
onClick = {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle("Are you sure?")
|
||||
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
AppDependencies.protocolStore.aci().sessions().archiveSessions(recipient.id)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from("Delete Avatar"),
|
||||
summary = DSLSettingsText.from("Deletes the avatar file and clears manually showing the avatar, resulting in a blurred gradient (assuming no profile sharing, no group in common, etc.)"),
|
||||
onClick = {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle("Are you sure?")
|
||||
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
SignalDatabase.recipients.manuallyUpdateShowAvatar(recipient.id, false)
|
||||
AvatarHelper.delete(requireContext(), recipient.id)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from("Clear recipient data"),
|
||||
summary = DSLSettingsText.from("Clears service id, profile data, sessions, identities, and thread."),
|
||||
onClick = {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle("Are you sure?")
|
||||
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
SignalDatabase.threads.deleteConversation(SignalDatabase.threads.getThreadIdIfExistsFor(recipient.id))
|
||||
|
||||
if (recipient.hasServiceId) {
|
||||
SignalDatabase.recipients.debugClearServiceIds(recipient.id)
|
||||
SignalDatabase.recipients.debugClearProfileData(recipient.id)
|
||||
}
|
||||
|
||||
if (recipient.hasAci) {
|
||||
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account.requireAci(), addressName = recipient.requireAci().toString())
|
||||
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account.requirePni(), addressName = recipient.requireAci().toString())
|
||||
AppDependencies.protocolStore.aci().identities().delete(recipient.requireAci().toString())
|
||||
}
|
||||
|
||||
if (recipient.hasPni) {
|
||||
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account.requireAci(), addressName = recipient.requirePni().toString())
|
||||
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account.requirePni(), addressName = recipient.requirePni().toString())
|
||||
AppDependencies.protocolStore.aci().identities().delete(recipient.requirePni().toString())
|
||||
}
|
||||
|
||||
startActivity(MainActivity.clearTop(requireContext()))
|
||||
}
|
||||
.show()
|
||||
}
|
||||
)
|
||||
|
||||
if (!recipient.isGroup) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from("Add 1,000 dummy messages"),
|
||||
summary = DSLSettingsText.from("Just adds 1,000 random messages to the chat. Text-only, nothing complicated."),
|
||||
onClick = {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle("Are you sure?")
|
||||
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val messageCount = 1000
|
||||
val startTime = System.currentTimeMillis() - messageCount
|
||||
SignalDatabase.rawDatabase.withinTransaction {
|
||||
val targetThread = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
for (i in 1..messageCount) {
|
||||
val time = startTime + i
|
||||
if (Math.random() > 0.5) {
|
||||
val id = SignalDatabase.messages.insertMessageOutbox(
|
||||
message = OutgoingMessage(threadRecipient = recipient, sentTimeMillis = time, body = "Outgoing: $i"),
|
||||
threadId = targetThread
|
||||
)
|
||||
SignalDatabase.messages.markAsSent(id, true)
|
||||
} else {
|
||||
SignalDatabase.messages.insertMessageInbox(
|
||||
retrieved = IncomingMessage(type = MessageType.NORMAL, from = recipient.id, sentTimeMillis = time, serverTimeMillis = time, receivedTimeMillis = System.currentTimeMillis(), body = "Incoming: $i"),
|
||||
candidateThreadId = targetThread
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Toast.makeText(context, "Done!", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from("Add 10 dummy messages with attachments"),
|
||||
summary = DSLSettingsText.from("Adds 10 random messages to the chat with attachments of a random image. Attachments are not uploaded."),
|
||||
onClick = {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle("Are you sure?")
|
||||
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val messageCount = 10
|
||||
val startTime = System.currentTimeMillis() - messageCount
|
||||
SignalDatabase.rawDatabase.withinTransaction {
|
||||
val targetThread = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
for (i in 1..messageCount) {
|
||||
val time = startTime + i
|
||||
val attachment = makeDummyAttachment()
|
||||
val id = SignalDatabase.messages.insertMessageOutbox(
|
||||
message = OutgoingMessage(threadRecipient = recipient, sentTimeMillis = time, body = "Outgoing: $i", attachments = listOf(attachment)),
|
||||
threadId = targetThread
|
||||
)
|
||||
SignalDatabase.messages.markAsSent(id, true)
|
||||
}
|
||||
}
|
||||
|
||||
Toast.makeText(context, "Done!", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (recipient.isSelf) {
|
||||
sectionHeaderPref(DSLSettingsText.from("Donations"))
|
||||
|
||||
// TODO [alex] - DB on main thread!
|
||||
val subscriber: InAppPaymentSubscriberRecord? = InAppPaymentsRepository.getSubscriber(InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
val summary = if (subscriber != null) {
|
||||
"""currency code: ${subscriber.currency!!.currencyCode}
|
||||
|subscriber id: ${subscriber.subscriberId.serialize()}
|
||||
""".trimMargin()
|
||||
} else {
|
||||
"None"
|
||||
}
|
||||
|
||||
longClickPref(
|
||||
title = DSLSettingsText.from("Subscriber ID"),
|
||||
summary = DSLSettingsText.from(summary),
|
||||
onLongClick = {
|
||||
if (subscriber != null) {
|
||||
copyToClipboard(subscriber.subscriberId.serialize())
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
sectionHeaderPref(DSLSettingsText.from("PNP"))
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from("Split and create threads"),
|
||||
summary = DSLSettingsText.from("Splits this contact into two recipients and two threads so that you can test merging them together. This will remain the 'primary' recipient."),
|
||||
onClick = {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle("Are you sure?")
|
||||
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
if (!recipient.hasE164) {
|
||||
Toast.makeText(context, "Recipient doesn't have an E164! Can't split.", Toast.LENGTH_SHORT).show()
|
||||
return@setPositiveButton
|
||||
}
|
||||
|
||||
SignalDatabase.recipients.debugClearE164AndPni(recipient.id)
|
||||
|
||||
val splitRecipientId: RecipientId = SignalDatabase.recipients.getAndPossiblyMergePnpVerified(null, recipient.pni.orElse(null), recipient.requireE164())
|
||||
val splitRecipient: Recipient = Recipient.resolved(splitRecipientId)
|
||||
val splitThreadId: Long = SignalDatabase.threads.getOrCreateThreadIdFor(splitRecipient)
|
||||
|
||||
val messageId: Long = SignalDatabase.messages.insertMessageOutbox(
|
||||
OutgoingMessage.text(splitRecipient, "Test Message ${System.currentTimeMillis()}", 0),
|
||||
splitThreadId,
|
||||
false,
|
||||
null
|
||||
)
|
||||
SignalDatabase.messages.markAsSent(messageId, true)
|
||||
|
||||
SignalDatabase.threads.update(splitThreadId, true)
|
||||
|
||||
Toast.makeText(context, "Done! We split the E164/PNI from this contact into $splitRecipientId", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from("Split without creating threads"),
|
||||
summary = DSLSettingsText.from("Splits this contact into two recipients so you can test merging them together. This will become the PNI-based recipient. Another recipient will be made with this ACI and profile key. Doing a CDS refresh should allow you to see a Session Switchover Event, as long as you had a session with this PNI."),
|
||||
onClick = {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle("Are you sure?")
|
||||
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
if (recipient.pni.isAbsent()) {
|
||||
Toast.makeText(context, "Recipient doesn't have a PNI! Can't split.", Toast.LENGTH_SHORT).show()
|
||||
return@setPositiveButton
|
||||
}
|
||||
|
||||
if (recipient.serviceId.isAbsent()) {
|
||||
Toast.makeText(context, "Recipient doesn't have a serviceId! Can't split.", Toast.LENGTH_SHORT).show()
|
||||
return@setPositiveButton
|
||||
}
|
||||
|
||||
SignalDatabase.recipients.debugRemoveAci(recipient.id)
|
||||
|
||||
val aciRecipientId: RecipientId = SignalDatabase.recipients.getAndPossiblyMergePnpVerified(recipient.requireAci(), null, null)
|
||||
|
||||
recipient.profileKey?.let { profileKey ->
|
||||
SignalDatabase.recipients.setProfileKey(aciRecipientId, ProfileKey(profileKey))
|
||||
}
|
||||
|
||||
SignalDatabase.recipients.debugClearProfileData(recipient.id)
|
||||
|
||||
Toast.makeText(context, "Done! Split the ACI and profile key off into $aciRecipientId", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
)
|
||||
}
|
||||
InternalConversationSettingsScreen(
|
||||
state = state,
|
||||
callbacks = this
|
||||
)
|
||||
}
|
||||
|
||||
private fun makeDummyAttachment(): Attachment {
|
||||
@@ -469,44 +95,174 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
|
||||
)
|
||||
}
|
||||
|
||||
private fun copyToClipboard(text: String) {
|
||||
Util.copyToClipboard(requireContext(), text)
|
||||
override fun onNavigationClick() {
|
||||
requireActivity().onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
|
||||
override fun copyToClipboard(data: String) {
|
||||
Util.copyToClipboard(requireContext(), data)
|
||||
Toast.makeText(requireContext(), "Copied to clipboard", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun buildCapabilitySpan(recipient: Recipient): CharSequence {
|
||||
val capabilities: RecipientRecord.Capabilities? = SignalDatabase.recipients.getCapabilities(recipient.id)
|
||||
override fun triggerThreadUpdate(threadId: Long?) {
|
||||
val startTimeNanos = System.nanoTime()
|
||||
SignalDatabase.threads.update(threadId ?: -1L, true)
|
||||
val endTimeNanos = System.nanoTime()
|
||||
Toast.makeText(context, "Thread update took ${(endTimeNanos - startTimeNanos).nanoseconds.toDouble(DurationUnit.MILLISECONDS).roundedString(2)} ms", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
return if (capabilities != null) {
|
||||
TextUtils.concat(
|
||||
colorize("SSREv2", capabilities.storageServiceEncryptionV2)
|
||||
)
|
||||
} else {
|
||||
"Recipient not found!"
|
||||
override fun disableProfileSharing(recipientId: RecipientId) {
|
||||
SignalDatabase.recipients.setProfileSharing(recipientId, false)
|
||||
}
|
||||
|
||||
override fun deleteSessions(recipientId: RecipientId) {
|
||||
val recipient = Recipient.live(recipientId).get()
|
||||
val aci = recipient.aci.orNull()
|
||||
val pni = recipient.pni.orNull()
|
||||
|
||||
if (aci != null) {
|
||||
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account.requireAci(), addressName = aci.toString())
|
||||
}
|
||||
if (pni != null) {
|
||||
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account.requireAci(), addressName = pni.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun colorize(name: String, support: Recipient.Capability): CharSequence {
|
||||
return when (support) {
|
||||
Recipient.Capability.SUPPORTED -> SpanUtil.color(Color.rgb(0, 150, 0), name)
|
||||
Recipient.Capability.NOT_SUPPORTED -> SpanUtil.color(Color.RED, name)
|
||||
Recipient.Capability.UNKNOWN -> SpanUtil.italic(name)
|
||||
override fun archiveSessions(recipientId: RecipientId) {
|
||||
AppDependencies.protocolStore.aci().sessions().archiveSessions(recipientId)
|
||||
}
|
||||
|
||||
override fun deleteAvatar(recipientId: RecipientId) {
|
||||
SignalDatabase.recipients.manuallyUpdateShowAvatar(recipientId, false)
|
||||
AvatarHelper.delete(requireContext(), recipientId)
|
||||
}
|
||||
|
||||
override fun clearRecipientData(recipientId: RecipientId) {
|
||||
val recipient = Recipient.live(recipientId).get()
|
||||
SignalDatabase.threads.deleteConversation(SignalDatabase.threads.getThreadIdIfExistsFor(recipientId))
|
||||
|
||||
if (recipient.hasServiceId) {
|
||||
SignalDatabase.recipients.debugClearServiceIds(recipientId)
|
||||
SignalDatabase.recipients.debugClearProfileData(recipientId)
|
||||
}
|
||||
|
||||
if (recipient.hasAci) {
|
||||
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account.requireAci(), addressName = recipient.requireAci().toString())
|
||||
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account.requirePni(), addressName = recipient.requireAci().toString())
|
||||
AppDependencies.protocolStore.aci().identities().delete(recipient.requireAci().toString())
|
||||
}
|
||||
|
||||
if (recipient.hasPni) {
|
||||
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account.requireAci(), addressName = recipient.requirePni().toString())
|
||||
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account.requirePni(), addressName = recipient.requirePni().toString())
|
||||
AppDependencies.protocolStore.aci().identities().delete(recipient.requirePni().toString())
|
||||
}
|
||||
|
||||
startActivity(MainActivity.clearTop(requireContext()))
|
||||
}
|
||||
|
||||
override fun add1000Messages(recipientId: RecipientId) {
|
||||
val recipient = Recipient.live(recipientId).get()
|
||||
val messageCount = 10
|
||||
val startTime = System.currentTimeMillis() - messageCount
|
||||
SignalDatabase.rawDatabase.withinTransaction {
|
||||
val targetThread = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
for (i in 1..messageCount) {
|
||||
val time = startTime + i
|
||||
val attachment = makeDummyAttachment()
|
||||
val id = SignalDatabase.messages.insertMessageOutbox(
|
||||
message = OutgoingMessage(threadRecipient = recipient, sentTimeMillis = time, body = "Outgoing: $i", attachments = listOf(attachment)),
|
||||
threadId = targetThread
|
||||
)
|
||||
SignalDatabase.messages.markAsSent(id, true)
|
||||
}
|
||||
}
|
||||
|
||||
Toast.makeText(context, "Done!", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun add10Messages(recipientId: RecipientId) {
|
||||
val recipient = Recipient.live(recipientId).get()
|
||||
val messageCount = 10
|
||||
val startTime = System.currentTimeMillis() - messageCount
|
||||
SignalDatabase.rawDatabase.withinTransaction {
|
||||
val targetThread = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
for (i in 1..messageCount) {
|
||||
val time = startTime + i
|
||||
val attachment = makeDummyAttachment()
|
||||
val id = SignalDatabase.messages.insertMessageOutbox(
|
||||
message = OutgoingMessage(threadRecipient = recipient, sentTimeMillis = time, body = "Outgoing: $i", attachments = listOf(attachment)),
|
||||
threadId = targetThread
|
||||
)
|
||||
SignalDatabase.messages.markAsSent(id, true)
|
||||
}
|
||||
}
|
||||
|
||||
Toast.makeText(context, "Done!", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun splitAndCreateThreads(recipientId: RecipientId) {
|
||||
val recipient = Recipient.live(recipientId).get()
|
||||
if (!recipient.hasE164) {
|
||||
Toast.makeText(context, "Recipient doesn't have an E164! Can't split.", Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
SignalDatabase.recipients.debugClearE164AndPni(recipient.id)
|
||||
|
||||
val splitRecipientId: RecipientId = SignalDatabase.recipients.getAndPossiblyMergePnpVerified(null, recipient.pni.orElse(null), recipient.requireE164())
|
||||
val splitRecipient: Recipient = Recipient.resolved(splitRecipientId)
|
||||
val splitThreadId: Long = SignalDatabase.threads.getOrCreateThreadIdFor(splitRecipient)
|
||||
|
||||
val messageId: Long = SignalDatabase.messages.insertMessageOutbox(
|
||||
OutgoingMessage.text(splitRecipient, "Test Message ${System.currentTimeMillis()}", 0),
|
||||
splitThreadId,
|
||||
false,
|
||||
null
|
||||
)
|
||||
SignalDatabase.messages.markAsSent(messageId, true)
|
||||
|
||||
SignalDatabase.threads.update(splitThreadId, true)
|
||||
|
||||
Toast.makeText(context, "Done! We split the E164/PNI from this contact into $splitRecipientId", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun splitWithoutCreatingThreads(recipientId: RecipientId) {
|
||||
val recipient = Recipient.live(recipientId).get()
|
||||
if (recipient.pni.isAbsent()) {
|
||||
Toast.makeText(context, "Recipient doesn't have a PNI! Can't split.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
if (recipient.serviceId.isAbsent()) {
|
||||
Toast.makeText(context, "Recipient doesn't have a serviceId! Can't split.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
SignalDatabase.recipients.debugRemoveAci(recipient.id)
|
||||
|
||||
val aciRecipientId: RecipientId = SignalDatabase.recipients.getAndPossiblyMergePnpVerified(recipient.requireAci(), null, null)
|
||||
|
||||
recipient.profileKey?.let { profileKey ->
|
||||
SignalDatabase.recipients.setProfileKey(aciRecipientId, ProfileKey(profileKey))
|
||||
}
|
||||
|
||||
SignalDatabase.recipients.debugClearProfileData(recipient.id)
|
||||
|
||||
Toast.makeText(context, "Done! Split the ACI and profile key off into $aciRecipientId", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
class InternalViewModel(
|
||||
val recipientId: RecipientId
|
||||
) : ViewModel(), RecipientForeverObserver {
|
||||
|
||||
private val store = Store(
|
||||
InternalState(
|
||||
private val store = MutableStateFlow(
|
||||
InternalConversationSettingsState.create(
|
||||
recipient = Recipient.resolved(recipientId),
|
||||
threadId = null,
|
||||
groupId = null
|
||||
)
|
||||
)
|
||||
|
||||
val state = store.stateLiveData
|
||||
val state: StateFlow<InternalConversationSettingsState> = store
|
||||
val liveRecipient = Recipient.live(recipientId)
|
||||
|
||||
init {
|
||||
@@ -520,7 +276,7 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
|
||||
}
|
||||
|
||||
override fun onRecipientChanged(recipient: Recipient) {
|
||||
store.update { state -> state.copy(recipient = recipient) }
|
||||
store.update { state -> InternalConversationSettingsState.create(recipient, state.threadId, state.groupId) }
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
@@ -533,10 +289,4 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
|
||||
return Objects.requireNonNull(modelClass.cast(InternalViewModel(recipientId)))
|
||||
}
|
||||
}
|
||||
|
||||
data class InternalState(
|
||||
val recipient: Recipient,
|
||||
val threadId: Long?,
|
||||
val groupId: GroupId?
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,463 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.conversation
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import org.signal.core.ui.compose.Dialogs
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Rows
|
||||
import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.ui.compose.Texts
|
||||
import org.signal.core.util.Hex.fromStringCondensed
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
private enum class Dialog {
|
||||
NONE,
|
||||
DISABLE_PROFILE_SHARING,
|
||||
DELETE_SESSIONS,
|
||||
ARCHIVE_SESSIONS,
|
||||
DELETE_AVATAR,
|
||||
CLEAR_RECIPIENT_DATA,
|
||||
ADD_1000_MESSAGES,
|
||||
ADD_10_MESSAGES,
|
||||
SPLIT_AND_CREATE_THREADS,
|
||||
SPLIT_WITHOUT_CREATING_THREADS
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows internal details about a recipient that you can view from the conversation settings.
|
||||
*/
|
||||
@Composable
|
||||
fun InternalConversationSettingsScreen(
|
||||
state: InternalConversationSettingsState,
|
||||
callbacks: InternalConversationSettingsScreenCallbacks
|
||||
) {
|
||||
var dialog by remember { mutableStateOf(Dialog.NONE) }
|
||||
|
||||
Scaffolds.Settings(
|
||||
title = stringResource(R.string.ConversationSettingsFragment__internal_details),
|
||||
onNavigationClick = callbacks::onNavigationClick,
|
||||
navigationIconPainter = painterResource(R.drawable.symbol_arrow_start_24),
|
||||
navigationContentDescription = stringResource(R.string.CallScreenTopBar__go_back)
|
||||
) { paddingValues ->
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(paddingValues)
|
||||
) {
|
||||
item {
|
||||
Texts.SectionHeader(
|
||||
text = "Data"
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = "RecipientId",
|
||||
label = state.recipientId.toString()
|
||||
)
|
||||
}
|
||||
|
||||
if (!state.isGroup) {
|
||||
item {
|
||||
LongClickToCopy(
|
||||
text = "E164",
|
||||
label = state.e164,
|
||||
callback = callbacks
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
LongClickToCopy(
|
||||
text = "ACI",
|
||||
label = state.aci,
|
||||
callback = callbacks
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
LongClickToCopy(
|
||||
text = "PNI",
|
||||
label = state.pni,
|
||||
callback = callbacks
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.groupIdString != null) {
|
||||
item {
|
||||
LongClickToCopy(
|
||||
text = "GroupId",
|
||||
label = state.groupIdString,
|
||||
callback = callbacks
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
LongClickToCopy(
|
||||
text = "ThreadId",
|
||||
label = state.threadIdString,
|
||||
callback = callbacks
|
||||
)
|
||||
}
|
||||
|
||||
if (!state.isGroup) {
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = "Profile Name",
|
||||
label = state.profileName
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
LongClickToCopy(
|
||||
text = "Profile Key (Base64)",
|
||||
label = state.profileKeyBase64,
|
||||
callback = callbacks
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
LongClickToCopy(
|
||||
text = "Profile Key (Hex)",
|
||||
label = state.profileKeyHex,
|
||||
callback = callbacks
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = "Sealed Sender Mode",
|
||||
label = state.sealedSenderAccessMode
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = "Phone Number Sharing",
|
||||
label = state.phoneNumberSharing
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = "Phone Number Discoverability",
|
||||
label = state.phoneNumberDiscoverability
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = "Profile Sharing (AKA \"Whitelisted\")",
|
||||
label = state.profileSharing
|
||||
)
|
||||
}
|
||||
|
||||
if (!state.isGroup) {
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = remember { AnnotatedString("Capabilities") },
|
||||
label = state.capabilities
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
val onClick = remember(state.threadId) {
|
||||
{ callbacks.triggerThreadUpdate(state.threadId) }
|
||||
}
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Trigger Thread Update",
|
||||
label = "Triggers a thread update. Useful for testing perf.",
|
||||
onClick = onClick
|
||||
)
|
||||
}
|
||||
|
||||
if (!state.isGroup) {
|
||||
item {
|
||||
Texts.SectionHeader(text = "Actions")
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = "Disable Profile Sharing",
|
||||
label = "Clears profile sharing/whitelisted status, which should cause the Message Request UI to show.",
|
||||
onClick = {
|
||||
dialog = Dialog.DISABLE_PROFILE_SHARING
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = "Delete Sessions",
|
||||
label = "Deletes all sessions with this recipient, essentially guaranteeing an encryption error if they send you a message.",
|
||||
onClick = {
|
||||
dialog = Dialog.DELETE_SESSIONS
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = "Archive Sessions",
|
||||
label = "Archives all sessions associated with this recipient, causing you to create a new session the next time you send a message (while not causing decryption errors).",
|
||||
onClick = {
|
||||
dialog = Dialog.ARCHIVE_SESSIONS
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = "Delete Avatar",
|
||||
label = "Deletes the avatar file and clears manually showing the avatar, resulting in a blurred gradient (assuming no profile sharing, no group in common, etc.)",
|
||||
onClick = {
|
||||
dialog = Dialog.DELETE_AVATAR
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = "Clear recipient data",
|
||||
label = "Clears service id, profile data, sessions, identities, and thread.",
|
||||
onClick = {
|
||||
dialog = Dialog.CLEAR_RECIPIENT_DATA
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (!state.isGroup) {
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = "Add 1,000 dummy messages",
|
||||
label = "Just adds 1,000 random messages to the chat. Text-only, nothing complicated.",
|
||||
onClick = {
|
||||
dialog = Dialog.ADD_1000_MESSAGES
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = "Add 10 dummy messages with attachments",
|
||||
label = "Adds 10 random messages to the chat with attachments of a random image. Attachments are not uploaded.",
|
||||
onClick = {
|
||||
dialog = Dialog.ADD_10_MESSAGES
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.isSelf) {
|
||||
item {
|
||||
Texts.SectionHeader(text = "Donations")
|
||||
}
|
||||
|
||||
item {
|
||||
val onLongClick = remember(state.subscriberId) {
|
||||
{ callbacks.copyToClipboard(state.subscriberId) }
|
||||
}
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Subscriber ID",
|
||||
label = state.subscriberId,
|
||||
onLongClick = onLongClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Texts.SectionHeader(
|
||||
text = "PNP"
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = "Split and create threads",
|
||||
label = "Splits this contact into two recipients and two threads so that you can test merging them together. This will remain the 'primary' recipient.",
|
||||
onClick = {
|
||||
dialog = Dialog.SPLIT_AND_CREATE_THREADS
|
||||
}
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Split without creating threads",
|
||||
label = "Splits this contact into two recipients so you can test merging them together. This will become the PNI-based recipient. Another recipient will be made with this ACI and profile key. Doing a CDS refresh should allow you to see a Session Switchover Event, as long as you had a session with this PNI.",
|
||||
onClick = {
|
||||
dialog = Dialog.SPLIT_WITHOUT_CREATING_THREADS
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dialog != Dialog.NONE) {
|
||||
val onConfirm = rememberOnConfirm(state, callbacks, dialog)
|
||||
|
||||
AreYouSureDialog(onConfirm) {
|
||||
dialog = Dialog.NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberOnConfirm(
|
||||
state: InternalConversationSettingsState,
|
||||
callbacks: InternalConversationSettingsScreenCallbacks,
|
||||
dialog: Dialog
|
||||
): () -> Unit {
|
||||
return remember(dialog, callbacks, state.threadId, state.recipientId) {
|
||||
when (dialog) {
|
||||
Dialog.NONE -> {
|
||||
{}
|
||||
}
|
||||
Dialog.DISABLE_PROFILE_SHARING -> {
|
||||
{ callbacks.disableProfileSharing(state.recipientId) }
|
||||
}
|
||||
Dialog.DELETE_SESSIONS -> {
|
||||
{ callbacks.deleteSessions(state.recipientId) }
|
||||
}
|
||||
Dialog.ARCHIVE_SESSIONS -> {
|
||||
{ callbacks.archiveSessions(state.recipientId) }
|
||||
}
|
||||
Dialog.DELETE_AVATAR -> {
|
||||
{ callbacks.deleteAvatar(state.recipientId) }
|
||||
}
|
||||
Dialog.CLEAR_RECIPIENT_DATA -> {
|
||||
{ callbacks.clearRecipientData(state.recipientId) }
|
||||
}
|
||||
Dialog.ADD_1000_MESSAGES -> {
|
||||
{ callbacks.add1000Messages(state.recipientId) }
|
||||
}
|
||||
Dialog.ADD_10_MESSAGES -> {
|
||||
{ callbacks.add10Messages(state.recipientId) }
|
||||
}
|
||||
Dialog.SPLIT_AND_CREATE_THREADS -> {
|
||||
{ callbacks.splitAndCreateThreads(state.recipientId) }
|
||||
}
|
||||
Dialog.SPLIT_WITHOUT_CREATING_THREADS -> {
|
||||
{ callbacks.splitWithoutCreatingThreads(state.recipientId) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LongClickToCopy(
|
||||
text: String,
|
||||
label: String,
|
||||
callback: InternalConversationSettingsScreenCallbacks
|
||||
) {
|
||||
val onLongClick = remember(label, callback) {
|
||||
{ callback.copyToClipboard(label) }
|
||||
}
|
||||
|
||||
Rows.TextRow(
|
||||
text = text,
|
||||
label = label,
|
||||
onLongClick = onLongClick
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AreYouSureDialog(
|
||||
onConfirm: () -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = "",
|
||||
body = "Are you sure?",
|
||||
confirm = stringResource(android.R.string.ok),
|
||||
dismiss = stringResource(android.R.string.cancel),
|
||||
onConfirm = onConfirm,
|
||||
onDismiss = onDismiss
|
||||
)
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
fun InternalConversationSettingsScreenPreview() {
|
||||
Previews.Preview {
|
||||
InternalConversationSettingsScreen(
|
||||
state = createState(),
|
||||
callbacks = InternalConversationSettingsScreenCallbacks.Empty
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
fun InternalConversationSettingsScreenGroupPreview() {
|
||||
Previews.Preview {
|
||||
InternalConversationSettingsScreen(
|
||||
state = createState(GroupId.v2(GroupMasterKey(fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")))),
|
||||
callbacks = InternalConversationSettingsScreenCallbacks.Empty
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createState(
|
||||
groupId: GroupId? = null
|
||||
): InternalConversationSettingsState {
|
||||
return InternalConversationSettingsState(
|
||||
recipientId = RecipientId.from(1),
|
||||
isGroup = groupId != null,
|
||||
e164 = "+11111111111",
|
||||
aci = "TEST-ACI",
|
||||
pni = "TEST-PNI",
|
||||
groupId = groupId,
|
||||
threadId = 12,
|
||||
profileName = "Miles Morales",
|
||||
profileKeyBase64 = "profile64",
|
||||
profileKeyHex = "profileHex",
|
||||
sealedSenderAccessMode = "SealedSenderAccessMode",
|
||||
phoneNumberSharing = "PhoneNumberSharing",
|
||||
phoneNumberDiscoverability = "PhoneNumberDiscoverability",
|
||||
profileSharing = "ProfileSharing",
|
||||
capabilities = AnnotatedString("CapabilitiesString"),
|
||||
hasServiceId = true,
|
||||
isSelf = groupId == null,
|
||||
subscriberId = "SubscriberId"
|
||||
)
|
||||
}
|
||||
|
||||
@Stable
|
||||
interface InternalConversationSettingsScreenCallbacks {
|
||||
fun onNavigationClick() = Unit
|
||||
fun copyToClipboard(data: String) = Unit
|
||||
fun triggerThreadUpdate(threadId: Long?) = Unit
|
||||
fun disableProfileSharing(recipientId: RecipientId) = Unit
|
||||
fun deleteSessions(recipientId: RecipientId) = Unit
|
||||
fun archiveSessions(recipientId: RecipientId) = Unit
|
||||
fun deleteAvatar(recipientId: RecipientId) = Unit
|
||||
fun clearRecipientData(recipientId: RecipientId) = Unit
|
||||
fun add1000Messages(recipientId: RecipientId) = Unit
|
||||
fun add10Messages(recipientId: RecipientId) = Unit
|
||||
fun splitAndCreateThreads(recipientId: RecipientId) = Unit
|
||||
fun splitWithoutCreatingThreads(recipientId: RecipientId) = Unit
|
||||
|
||||
object Empty : InternalConversationSettingsScreenCallbacks
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.conversation
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.Hex
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.database.model.RecipientRecord
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
@Immutable
|
||||
data class InternalConversationSettingsState(
|
||||
val recipientId: RecipientId,
|
||||
val isGroup: Boolean,
|
||||
val e164: String,
|
||||
val aci: String,
|
||||
val pni: String,
|
||||
val groupId: GroupId?,
|
||||
val threadId: Long?,
|
||||
val profileName: String,
|
||||
val profileKeyBase64: String,
|
||||
val profileKeyHex: String,
|
||||
val sealedSenderAccessMode: String,
|
||||
val phoneNumberSharing: String,
|
||||
val phoneNumberDiscoverability: String,
|
||||
val profileSharing: String,
|
||||
val capabilities: AnnotatedString,
|
||||
val hasServiceId: Boolean,
|
||||
val isSelf: Boolean,
|
||||
val subscriberId: String
|
||||
) {
|
||||
|
||||
val groupIdString = groupId?.toString()
|
||||
val threadIdString = threadId?.toString() ?: "N/A"
|
||||
|
||||
companion object {
|
||||
@WorkerThread
|
||||
fun create(recipient: Recipient, threadId: Long?, groupId: GroupId?): InternalConversationSettingsState {
|
||||
return InternalConversationSettingsState(
|
||||
recipientId = recipient.id,
|
||||
isGroup = recipient.isGroup,
|
||||
e164 = recipient.e164.orElse("null"),
|
||||
aci = recipient.aci.map { it.toString() }.orElse("null"),
|
||||
pni = recipient.pni.map { it.toString() }.orElse("null"),
|
||||
groupId = groupId,
|
||||
threadId = threadId,
|
||||
profileName = with(recipient) {
|
||||
if (isGroup) "" else "[${profileName.givenName}] [${profileName.familyName}]"
|
||||
},
|
||||
profileKeyBase64 = with(recipient) {
|
||||
if (isGroup) "" else profileKey?.let(Base64::encodeWithPadding) ?: "None"
|
||||
},
|
||||
profileKeyHex = with(recipient) {
|
||||
if (isGroup) "" else profileKey?.let(Hex::toStringCondensed) ?: "None"
|
||||
},
|
||||
sealedSenderAccessMode = recipient.sealedSenderAccessMode.toString(),
|
||||
phoneNumberSharing = recipient.phoneNumberSharing.name,
|
||||
phoneNumberDiscoverability = SignalDatabase.recipients.getPhoneNumberDiscoverability(recipient.id)?.name ?: "null",
|
||||
profileSharing = recipient.isProfileSharing.toString(),
|
||||
capabilities = buildCapabilities(recipient),
|
||||
hasServiceId = recipient.hasServiceId,
|
||||
isSelf = recipient.isSelf,
|
||||
subscriberId = buildSubscriberId(recipient)
|
||||
)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun buildSubscriberId(recipient: Recipient): String {
|
||||
return if (recipient.isSelf) {
|
||||
val subscriber: InAppPaymentSubscriberRecord? = InAppPaymentsRepository.getSubscriber(InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
if (subscriber != null) {
|
||||
"""currency code: ${subscriber.currency!!.currencyCode}
|
||||
|subscriber id: ${subscriber.subscriberId.serialize()}
|
||||
""".trimMargin()
|
||||
} else {
|
||||
"None"
|
||||
}
|
||||
} else {
|
||||
"None"
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun buildCapabilities(recipient: Recipient): AnnotatedString {
|
||||
return if (recipient.isGroup) {
|
||||
AnnotatedString("null")
|
||||
} else {
|
||||
val capabilities: RecipientRecord.Capabilities? = SignalDatabase.recipients.getCapabilities(recipient.id)
|
||||
if (capabilities != null) {
|
||||
val style: SpanStyle = when (capabilities.storageServiceEncryptionV2) {
|
||||
Recipient.Capability.SUPPORTED -> SpanStyle(color = Color(0, 150, 0))
|
||||
Recipient.Capability.NOT_SUPPORTED -> SpanStyle(color = Color.Red)
|
||||
Recipient.Capability.UNKNOWN -> SpanStyle(fontStyle = FontStyle.Italic)
|
||||
}
|
||||
|
||||
buildAnnotatedString {
|
||||
withStyle(style = style) {
|
||||
append("SSREv2")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
AnnotatedString("Recipient not found!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.R
|
||||
@@ -224,6 +225,34 @@ object Rows {
|
||||
onClick: (() -> Unit)? = null,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
TextRow(
|
||||
text = remember(text) { AnnotatedString(text) },
|
||||
label = remember(label) { label?.let { AnnotatedString(label) } },
|
||||
icon = icon,
|
||||
modifier = modifier,
|
||||
iconModifier = iconModifier,
|
||||
foregroundTint = foregroundTint,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
enabled = enabled
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Text row that positions [text] and optional [label] in a [TextAndLabel] to the side of an optional [icon].
|
||||
*/
|
||||
@Composable
|
||||
fun TextRow(
|
||||
text: AnnotatedString,
|
||||
modifier: Modifier = Modifier,
|
||||
iconModifier: Modifier = Modifier,
|
||||
label: AnnotatedString? = null,
|
||||
icon: Painter? = null,
|
||||
foregroundTint: Color = MaterialTheme.colorScheme.onSurface,
|
||||
onClick: (() -> Unit)? = null,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
TextRow(
|
||||
text = {
|
||||
@@ -353,6 +382,28 @@ object Rows {
|
||||
enabled: Boolean = true,
|
||||
textColor: Color = MaterialTheme.colorScheme.onSurface,
|
||||
textStyle: TextStyle = MaterialTheme.typography.bodyLarge
|
||||
) {
|
||||
TextAndLabel(
|
||||
text = remember(text) { text?.let { AnnotatedString(it) } },
|
||||
label = remember(label) { label?.let { AnnotatedString(it) } },
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
textColor = textColor,
|
||||
textStyle = textStyle
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Row component to position text above an optional label.
|
||||
*/
|
||||
@Composable
|
||||
fun RowScope.TextAndLabel(
|
||||
text: AnnotatedString? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
label: AnnotatedString? = null,
|
||||
enabled: Boolean = true,
|
||||
textColor: Color = MaterialTheme.colorScheme.onSurface,
|
||||
textStyle: TextStyle = MaterialTheme.typography.bodyLarge
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
|
||||
Reference in New Issue
Block a user