diff --git a/app/src/main/java/org/session/libsession/database/StorageProtocol.kt b/app/src/main/java/org/session/libsession/database/StorageProtocol.kt index a5b45a1580..1d99e66c52 100644 --- a/app/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/app/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -146,6 +146,8 @@ interface StorageProtocol { fun trimThreadBefore(threadID: Long, timestamp: Long) fun getMessageCount(threadID: Long): Long fun getTotalPinned(): Int + suspend fun getTotalSentProBadges(): Int + suspend fun getTotalSentLongMessages(): Int fun setPinned(address: Address, isPinned: Boolean) fun isRead(threadId: Long) : Boolean fun setThreadCreationDate(threadId: Long, newDate: Long) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 30661eb9cc..fa27f388e5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -58,7 +58,6 @@ import androidx.recyclerview.widget.RecyclerView import com.annimon.stream.Stream import com.bumptech.glide.Glide import com.squareup.phrase.Phrase -import dagger.Lazy import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.lifecycle.withCreationCallback import kotlinx.coroutines.CancellationException @@ -119,7 +118,6 @@ import org.session.libsignal.crypto.MnemonicCodec import org.session.libsignal.utilities.ListenableFuture import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.toHexString -import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.FullComposeActivity.Companion.applyCommonPropertiesForCompose import org.thoughtcrime.securesms.ScreenLockActionBarActivity import org.thoughtcrime.securesms.audio.AudioRecorderHandle @@ -135,7 +133,6 @@ import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companio import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_REPLY import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_RESEND import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_SAVE -import org.thoughtcrime.securesms.conversation.v2.dialogs.BlockedDialog import org.thoughtcrime.securesms.conversation.v2.dialogs.LinkPreviewDialog import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarButton import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarDelegate @@ -158,7 +155,6 @@ import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities import org.thoughtcrime.securesms.conversation.v2.utilities.ResendMessageUtilities import org.thoughtcrime.securesms.crypto.MnemonicUtilities -import org.thoughtcrime.securesms.database.AttachmentDatabase import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.database.LokiMessageDatabase import org.thoughtcrime.securesms.database.MmsDatabase @@ -2076,9 +2072,11 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, override fun sendMessage() { val recipient = viewModel.recipient + // It shouldn't be possible to send a message to a blocked user anymore. + // But as a safety net: // show the unblock dialog when trying to send a message to a blocked contact if (recipient.isStandardRecipient && recipient.blocked) { - BlockedDialog(recipient.address, recipient.displayName()).show(supportFragmentManager, "Blocked Dialog") + unblock() return } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt deleted file mode 100644 index 451dcc3031..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt +++ /dev/null @@ -1,36 +0,0 @@ -package org.thoughtcrime.securesms.conversation.v2.dialogs - -import android.app.Dialog -import android.graphics.Typeface -import android.os.Bundle -import android.text.Spannable -import android.text.SpannableStringBuilder -import android.text.style.StyleSpan -import androidx.fragment.app.DialogFragment -import network.loki.messenger.R -import org.session.libsession.messaging.MessagingModuleConfiguration -import org.session.libsession.utilities.Address -import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY -import org.thoughtcrime.securesms.createSessionDialog -import org.thoughtcrime.securesms.ui.getSubbedCharSequence - -/** Shown upon sending a message to a user that's blocked. */ -class BlockedDialog(private val recipient: Address, private val contactName: String) : DialogFragment() { - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog { - val explanationCS = context.getSubbedCharSequence(R.string.blockUnblockName, NAME_KEY to contactName) - val spannable = SpannableStringBuilder(explanationCS) - val startIndex = explanationCS.indexOf(contactName) - spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + contactName.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - - title(resources.getString(R.string.blockUnblock)) - text(spannable) - dangerButton(R.string.blockUnblock, R.string.AccessibilityId_unblockConfirm) { unblock() } - cancelButton { dismiss() } - } - - private fun unblock() { - MessagingModuleConfiguration.shared.storage.setBlocked(listOf(recipient), false) - dismiss() - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt index 81a3ce84d4..ac74fb1352 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt @@ -365,7 +365,7 @@ class InputBar @JvmOverloads constructor( fun setCharLimitState(state: InputbarViewModel.InputBarCharLimitState?) { // handle char limit if(state != null){ - binding.characterLimitText.text = state.count.toString() + binding.characterLimitText.text = state.countFormatted binding.characterLimitText.setTextColor(if(state.danger) dangerColor else textColor) binding.characterLimitContainer.setOnClickListener { delegate?.onCharLimitTapped() diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsDialogs.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsDialogs.kt index ccb1e3f534..d058fb29f9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsDialogs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsDialogs.kt @@ -263,6 +263,7 @@ fun ConversationSettingsDialogs( iconRes = R.drawable.ic_pro_badge, iconSize = 40.sp to 18.sp, style = LocalType.current.large, + textQaTag = stringResource(R.string.qa_cta_body) ) }, content = { CTAImage(heroImage = R.drawable.cta_hero_group) }, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsViewModel.kt index 6ed520d3be..b54cba5abc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsViewModel.kt @@ -11,6 +11,7 @@ import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity.CLIPBOARD_SERVICE import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.viewModel import com.squareup.phrase.Phrase import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -713,14 +714,17 @@ class ConversationSettingsViewModel @AssistedInject constructor( private fun pinConversation(){ // check the pin limit before continuing val totalPins = storage.getTotalPinned() - val maxPins = proStatusManager.getPinnedConversationLimit(recipientRepository.getSelf().isPro) - if(totalPins >= maxPins){ + val maxPins = + proStatusManager.getPinnedConversationLimit(recipientRepository.getSelf().isPro) + if (totalPins >= maxPins) { // the user has reached the pin limit, show the CTA _dialogState.update { - it.copy(pinCTA = PinProCTA( - overTheLimit = totalPins > maxPins, - proSubscription = proStatusManager.proDataState.value.type - )) + it.copy( + pinCTA = PinProCTA( + overTheLimit = totalPins > maxPins, + proSubscription = proStatusManager.proDataState.value.type + ) + ) } } else { viewModelScope.launch { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt index 730c922c66..70a4e50947 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt @@ -117,6 +117,32 @@ class MmsDatabase @Inject constructor( .any { MmsSmsColumns.Types.isOutgoingMessageType(it) } } + fun getOutgoingMessageProFeatureCount(featureMask: Long): Int { + return getOutgoingProFeatureCountInternal(PRO_MESSAGE_FEATURES, featureMask) + } + + fun getOutgoingProfileProFeatureCount(featureMask: Long): Int { + return getOutgoingProFeatureCountInternal(PRO_PROFILE_FEATURES, featureMask) + } + + private fun getOutgoingProFeatureCountInternal(column: String, featureMask: Long): Int { + val db = readableDatabase + val outgoingTypes = MmsSmsColumns.Types.OUTGOING_MESSAGE_TYPES.joinToString(",") + + // outgoing clause + val outgoingSelection = + "($MESSAGE_BOX & ${MmsSmsColumns.Types.BASE_TYPE_MASK}) IN ($outgoingTypes)" + + val where = "($column & $featureMask) != 0 AND $outgoingSelection" + + db.query(TABLE_NAME, arrayOf("COUNT(*)"), where, null, null, null, null).use { cursor -> + if (cursor.moveToFirst()) { + return cursor.getInt(0) + } + } + return 0 + } + fun isDeletedMessage(id: Long): Boolean = writableDatabase.query( TABLE_NAME, @@ -703,7 +729,7 @@ class MmsDatabase @Inject constructor( val deletedMessageIDs: MutableList val deletedMessagesThreadIDs = hashSetOf() - writableDatabase.rawQuery( + writableDatabase.rawQuery( "DELETE FROM $TABLE_NAME WHERE $where RETURNING $ID, $THREAD_ID", *whereArgs ).use { cursor -> @@ -1055,12 +1081,12 @@ class MmsDatabase @Inject constructor( val quoteText = retrievedQuote?.body val quoteMissing = retrievedQuote == null val quoteDeck = ( - (retrievedQuote as? MmsMessageRecord)?.slideDeck ?: - Stream.of(attachmentDatabase.getAttachment(cursor)) - .filter { obj: DatabaseAttachment? -> obj!!.isQuote } - .toList() - .let { SlideDeck(context, it) } - ) + (retrievedQuote as? MmsMessageRecord)?.slideDeck ?: + Stream.of(attachmentDatabase.getAttachment(cursor)) + .filter { obj: DatabaseAttachment? -> obj!!.isQuote } + .toList() + .let { SlideDeck(context, it) } + ) return Quote( quoteId, recipientRepository.getRecipientSync(quoteAuthor.toAddress()), diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index a881f936ae..1aa7363678 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -55,6 +55,8 @@ import dagger.hilt.android.qualifiers.ApplicationContext; import kotlin.Pair; import kotlin.Triple; +import network.loki.messenger.libsession_util.protocol.ProFeature; +import network.loki.messenger.libsession_util.protocol.ProMessageFeature; @Singleton public class MmsSmsDatabase extends Database { @@ -427,6 +429,17 @@ public long getConversationCount(long threadId) { return count; } + public int getOutgoingMessageProFeatureCount(long featureMask) { + return smsDatabase.get().getOutgoingMessageProFeatureCount(featureMask) + + mmsDatabase.get().getOutgoingMessageProFeatureCount(featureMask); + } + + public int getOutgoingProfileProFeatureCount(long featureMask) { + return smsDatabase.get().getOutgoingProfileProFeatureCount(featureMask) + + mmsDatabase.get().getOutgoingProfileProFeatureCount(featureMask); + } + + public void incrementReadReceiptCount(SyncMessageId syncMessageId, long timestamp) { smsDatabase.get().incrementReceiptCount(syncMessageId, false, true); mmsDatabase.get().incrementReceiptCount(syncMessageId, timestamp, false, true); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index f579417396..7a6204dfe6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -294,6 +294,39 @@ public boolean isOutgoingMessage(long id) { return isOutgoing; } + public int getOutgoingMessageProFeatureCount(long featureMask) { + return getOutgoingProFeatureCountInternal(PRO_MESSAGE_FEATURES, featureMask); + } + + public int getOutgoingProfileProFeatureCount(long featureMask) { + return getOutgoingProFeatureCountInternal(PRO_PROFILE_FEATURES, featureMask); + } + + private int getOutgoingProFeatureCountInternal(@NonNull String columnName, long featureMask) { + SQLiteDatabase db = getReadableDatabase(); + + // outgoing clause + StringBuilder outgoingTypes = new StringBuilder(); + long[] types = MmsSmsColumns.Types.OUTGOING_MESSAGE_TYPES; + for (int i = 0; i < types.length; i++) { + if (i > 0) outgoingTypes.append(","); + outgoingTypes.append(types[i]); + } + + String outgoingSelection = + "(" + TYPE + " & " + MmsSmsColumns.Types.BASE_TYPE_MASK + ") IN (" + outgoingTypes + ")"; + + String where = "(" + columnName + " & " + featureMask + ") != 0 AND " + outgoingSelection; + + try (Cursor cursor = db.query(TABLE_NAME, new String[]{"COUNT(*)"}, where, null, null, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + return cursor.getInt(0); + } + } + + return 0; + } + public boolean isDeletedMessage(long id) { SQLiteDatabase database = getWritableDatabase(); Cursor cursor = null; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index fdbaa3c84a..c0c8576408 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -4,10 +4,15 @@ import android.content.Context import android.net.Uri import dagger.Lazy import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import network.loki.messenger.libsession_util.MutableConversationVolatileConfig import network.loki.messenger.libsession_util.PRIORITY_PINNED import network.loki.messenger.libsession_util.PRIORITY_VISIBLE import network.loki.messenger.libsession_util.ReadableUserGroupsConfig +import network.loki.messenger.libsession_util.protocol.ProFeature +import network.loki.messenger.libsession_util.protocol.ProMessageFeature +import network.loki.messenger.libsession_util.protocol.ProProfileFeature import network.loki.messenger.libsession_util.util.BlindKeyAPI import network.loki.messenger.libsession_util.util.Bytes import network.loki.messenger.libsession_util.util.Conversation @@ -945,6 +950,24 @@ open class Storage @Inject constructor( } } + override suspend fun getTotalSentProBadges(): Int = + getTotalSentForFeature(ProProfileFeature.PRO_BADGE) + + override suspend fun getTotalSentLongMessages(): Int = + getTotalSentForFeature(ProMessageFeature.HIGHER_CHARACTER_LIMIT) + + suspend fun getTotalSentForFeature(feature: ProFeature): Int = withContext(Dispatchers.IO) { + val mask = 1L shl feature.bitIndex + + when (feature) { + is ProMessageFeature -> + mmsSmsDatabase.getOutgoingMessageProFeatureCount(mask) + + is ProProfileFeature -> + mmsSmsDatabase.getOutgoingProfileProFeatureCount(mask) + } + } + override fun setPinned(address: Address, isPinned: Boolean) { val isLocalNumber = address.address == getUserPublicKey() configFactory.withMutableUserConfigs { configs -> diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt index bf4849a7ec..3333e6ad61 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt @@ -270,7 +270,8 @@ class HomeViewModel @Inject constructor( fun setPinned(address: Address, pinned: Boolean) { // check the pin limit before continuing val totalPins = storage.getTotalPinned() - val maxPins = proStatusManager.getPinnedConversationLimit(recipientRepository.getSelf().isPro) + val maxPins = + proStatusManager.getPinnedConversationLimit(recipientRepository.getSelf().isPro) if (pinned && totalPins >= maxPins) { // the user has reached the pin limit, show the CTA _dialogsState.update { diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsScreen.kt index c6c96c25e2..06a27e8455 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsScreen.kt @@ -1010,7 +1010,9 @@ fun AnimatedProCTA( // main message Text( - modifier = Modifier.align(Alignment.CenterHorizontally), + modifier = Modifier + .qaTag(R.string.qa_cta_body) + .align(Alignment.CenterHorizontally), text = stringResource(R.string.proAnimatedDisplayPicture), textAlign = TextAlign.Center, style = LocalType.current.base.copy( diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsViewModel.kt index 0afe1c3fdc..4a6451be95 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsViewModel.kt @@ -15,13 +15,18 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.async +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import network.loki.messenger.R +import org.session.libsession.database.StorageProtocol import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.NonTranslatableStringConstants import org.session.libsession.utilities.StringSubstitutionConstants.ACTION_TYPE_KEY @@ -69,6 +74,7 @@ class ProSettingsViewModel @AssistedInject constructor( private val prefs: TextSecurePreferences, private val proDetailsRepository: ProDetailsRepository, private val configFactory: Lazy, + private val storage: StorageProtocol, ) : ViewModel() { @AssistedFactory @@ -184,8 +190,11 @@ class ProSettingsViewModel @AssistedInject constructor( private fun generateState(proDataState: ProDataState){ val subType = proDataState.type + // calculate stats for pro users + if(subType is ProStatus.Active) refreshProStats() + _proSettingsUIState.update { - ProSettingsState( + it.copy( proDataState = proDataState, subscriptionExpiryLabel = when(subType){ is ProStatus.Active.AutoRenewing -> @@ -206,14 +215,6 @@ class ProSettingsViewModel @AssistedInject constructor( is ProStatus.Active -> subType.duration.expiryFromNow() else -> "" }, - proStats = State.Success( //todo PRO calculate properly - ProStats( - groupsUpdated = 0, - pinnedConversations = 12, - proBadges = 6400, - longMessages = 215, - ) - ) ) } } @@ -811,6 +812,53 @@ class ProSettingsViewModel @AssistedInject constructor( } } + private fun refreshProStats(){ + viewModelScope.launch { + // show a loader for the stats + _proSettingsUIState.update { + it.copy( + proStats = State.Loading + ) + } + + // calculate pro stats values + try { + val stats = withContext(Dispatchers.IO) { + val pinsDeferred = async { + storage.getTotalPinned() + } + + val badgesDeferred = async { + storage.getTotalSentProBadges() + } + + val longMsgDeferred = async { + storage.getTotalSentLongMessages() + } + + ProStats( + groupsUpdated = 0, + pinnedConversations = pinsDeferred.await(), + proBadges = badgesDeferred.await(), + longMessages = longMsgDeferred.await(), + ) + } + + // update ui with results + _proSettingsUIState.update { + it.copy(proStats = State.Success(stats)) + } + } catch (e: Exception) { + // currently the UI doesn't have an error display + // it will look like it's still loading + // but the logic is there in case we have a look for stats errors + _proSettingsUIState.update { + it.copy(proStats = State.Error(e)) + } + } + } + } + sealed interface Commands { data class ShowOpenUrlDialog(val url: String?) : Commands data object ShowTCPolicyDialog: Commands diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/ProComponents.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/ProComponents.kt index a5a48edf5f..527a5e6f23 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/ProComponents.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/ProComponents.kt @@ -310,7 +310,10 @@ fun SessionProCTA( // features if (features.isNotEmpty()) { features.forEachIndexed { index, feature -> - ProCTAFeature(data = feature) + ProCTAFeature( + modifier = Modifier.qaTag(stringResource(R.string.qa_cta_feature) + index.toString()), + data = feature + ) if (index < features.size - 1) { Spacer(Modifier.height(LocalDimensions.current.xsSpacing)) } @@ -330,7 +333,9 @@ fun SessionProCTA( ) { positiveButtonText?.let { AccentFillButtonRect( - modifier = Modifier.then( + modifier = Modifier + .qaTag(R.string.qa_cta_button_positive) + .then( if (negativeButtonText != null) Modifier.weight(1f) else Modifier @@ -342,7 +347,9 @@ fun SessionProCTA( negativeButtonText?.let { TertiaryFillButtonRect( - modifier = Modifier.then( + modifier = Modifier + .qaTag(R.string.qa_cta_button_negative) + .then( if (positiveButtonText != null) Modifier.weight(1f) else Modifier @@ -443,7 +450,9 @@ fun SimpleSessionProCTA( badgeAtStart = badgeAtStart, textContent = { Text( - modifier = Modifier.align(Alignment.CenterHorizontally), + modifier = Modifier + .qaTag(R.string.qa_cta_body) + .align(Alignment.CenterHorizontally), text = text, textAlign = TextAlign.Center, style = LocalType.current.base.copy( @@ -495,7 +504,9 @@ fun AnimatedSessionProCTA( modifier = modifier, textContent = { Text( - modifier = Modifier.align(Alignment.CenterHorizontally), + modifier = Modifier + .qaTag(R.string.qa_cta_body) + .align(Alignment.CenterHorizontally), text = text, textAlign = TextAlign.Center, style = LocalType.current.base.copy( diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/Text.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/Text.kt index 25cd0a4bd2..39f2a57182 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/Text.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/Text.kt @@ -258,6 +258,7 @@ fun AnnotatedTextWithIcon( modifier: Modifier = Modifier, style: TextStyle = LocalType.current.base, color: Color = Color.Unspecified, + textQaTag: String? = null, iconSize: Pair = 12.sp to 12.sp, iconPaddingValues: PaddingValues = PaddingValues(start = style.lineHeight.value.dp * 0.2f), onIconClick: (() -> Unit)? = null @@ -296,6 +297,7 @@ fun AnnotatedTextWithIcon( modifier: Modifier = Modifier, style: TextStyle = LocalType.current.base, color: Color = Color.Unspecified, + textQaTag: String? = null, iconSize: Pair = 12.sp to 12.sp, ) { var inlineContent: Map = mapOf() @@ -330,7 +332,11 @@ fun AnnotatedTextWithIcon( Text( text = annotated, - modifier = modifier.fillMaxWidth(), + modifier = modifier.fillMaxWidth() + .then( + if (textQaTag != null) Modifier.qaTag(textQaTag) + else Modifier + ), style = style, color = color, textAlign = TextAlign.Center, diff --git a/content-descriptions/src/main/res/values/strings.xml b/content-descriptions/src/main/res/values/strings.xml index a727f999e5..d0982ac176 100644 --- a/content-descriptions/src/main/res/values/strings.xml +++ b/content-descriptions/src/main/res/values/strings.xml @@ -291,6 +291,12 @@ action-item-icon qa-blocked-contacts-settings-item + + cta-body + cta-feature- + cta-button-positive + cta-button-negative + qa-collapsing-footer-action \ No newline at end of file