diff --git a/app/build.gradle b/app/build.gradle index 8afe9b3d22..fc1a17b668 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -448,6 +448,14 @@ android { variant.setIgnore(true) } } + + android.buildTypes.each { + if (it.name != 'release') { + sourceSets.findByName(it.name).java.srcDirs += "$projectDir/src/debug/java" + } else { + sourceSets.findByName(it.name).java.srcDirs += "$projectDir/src/release/java" + } + } } dependencies { diff --git a/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/ConversationElementGenerator.kt b/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/ConversationElementGenerator.kt new file mode 100644 index 0000000000..09a5dd1266 --- /dev/null +++ b/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/ConversationElementGenerator.kt @@ -0,0 +1,132 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.internal.conversation + +import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory +import org.thoughtcrime.securesms.conversation.v2.data.ConversationElementKey +import org.thoughtcrime.securesms.conversation.v2.data.IncomingTextOnly +import org.thoughtcrime.securesms.conversation.v2.data.OutgoingTextOnly +import org.thoughtcrime.securesms.database.MessageTypes +import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord +import org.thoughtcrime.securesms.database.model.StoryType +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.mms.SlideDeck +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel +import java.security.SecureRandom +import kotlin.time.Duration.Companion.milliseconds + +/** + * Generates random conversation messages via the given set of parameters. + */ +class ConversationElementGenerator { + private val mappingModelCache = mutableMapOf>() + private val random = SecureRandom() + + private val wordBank = listOf( + "A", + "Test", + "Message", + "To", + "Display", + "Content", + "In", + "Bubbles", + "User", + "Signal", + "The" + ) + + fun getMappingModel(key: ConversationElementKey): MappingModel<*> { + val cached = mappingModelCache[key] + if (cached != null) { + return cached + } + + val messageModel = generateMessage(key) + mappingModelCache[key] = messageModel + return messageModel + } + + private fun getIncomingType(): Long { + return MessageTypes.BASE_INBOX_TYPE or MessageTypes.SECURE_MESSAGE_BIT + } + + private fun getSentOutgoingType(): Long { + return MessageTypes.BASE_SENT_TYPE or MessageTypes.SECURE_MESSAGE_BIT + } + + private fun generateMessage(key: ConversationElementKey): MappingModel<*> { + val messageId = key.requireMessageId() + val now = getNow() + + val testMessageWordLength = random.nextInt(40) + 1 + val testMessage = (0 until testMessageWordLength).map { + wordBank.random() + }.joinToString(" ") + + val isIncoming = random.nextBoolean() + + val record = MediaMmsMessageRecord( + messageId, + if (isIncoming) Recipient.UNKNOWN else Recipient.self(), + 0, + if (isIncoming) Recipient.self() else Recipient.UNKNOWN, + now, + now, + now, + 1, + 1, + testMessage, + SlideDeck(), + if (isIncoming) getIncomingType() else getSentOutgoingType(), + emptySet(), + emptySet(), + 0, + 0, + 0, + false, + 1, + null, + emptyList(), + emptyList(), + false, + emptyList(), + false, + false, + now, + 1, + now, + null, + StoryType.NONE, + null, + null, + null, + null, + -1, + null, + null, + 0 + ) + + val conversationMessage = ConversationMessageFactory.createWithUnresolvedData( + ApplicationDependencies.getApplication(), + record, + Recipient.UNKNOWN + ) + + return if (isIncoming) { + IncomingTextOnly(conversationMessage) + } else { + OutgoingTextOnly(conversationMessage) + } + } + + private fun getNow(): Long { + val now = System.currentTimeMillis() + return now - random.nextInt(20.milliseconds.inWholeMilliseconds.toInt()).toLong() + } +} diff --git a/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/InternalConversationTestDataSource.kt b/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/InternalConversationTestDataSource.kt new file mode 100644 index 0000000000..a3153a1509 --- /dev/null +++ b/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/InternalConversationTestDataSource.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.internal.conversation + +import org.signal.paging.PagedDataSource +import org.thoughtcrime.securesms.conversation.v2.data.ConversationElementKey +import org.thoughtcrime.securesms.conversation.v2.data.ConversationMessageElement +import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel +import kotlin.math.min + +class InternalConversationTestDataSource( + private val size: Int, + private val generator: ConversationElementGenerator +) : PagedDataSource> { + override fun size(): Int = size + + override fun load(start: Int, length: Int, totalSize: Int, cancellationSignal: PagedDataSource.CancellationSignal): MutableList> { + val end = min(start + length, totalSize) + return (start until end).map { + load(ConversationElementKey.forMessage(it.toLong()))!! + }.toMutableList() + } + + override fun getKey(data: MappingModel<*>): ConversationElementKey { + check(data is ConversationMessageElement) + + return ConversationElementKey.forMessage(data.conversationMessage.messageRecord.id) + } + + override fun load(key: ConversationElementKey?): MappingModel<*>? { + return key?.let { generator.getMappingModel(it) } + } +} diff --git a/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/InternalConversationTestFragment.kt b/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/InternalConversationTestFragment.kt new file mode 100644 index 0000000000..bdb2354eb2 --- /dev/null +++ b/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/InternalConversationTestFragment.kt @@ -0,0 +1,292 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.internal.conversation + +import android.net.Uri +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.kotlin.subscribeBy +import org.signal.core.util.concurrent.LifecycleDisposable +import org.signal.core.util.logging.Log +import org.signal.ringrtc.CallLinkRootKey +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.ViewBinderDelegate +import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager +import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState +import org.thoughtcrime.securesms.contactshare.Contact +import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener +import org.thoughtcrime.securesms.conversation.ConversationItem +import org.thoughtcrime.securesms.conversation.ConversationMessage +import org.thoughtcrime.securesms.conversation.colors.ChatColors +import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette +import org.thoughtcrime.securesms.conversation.colors.Colorizer +import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer +import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart +import org.thoughtcrime.securesms.conversation.v2.ConversationAdapterV2 +import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord +import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.database.model.MmsMessageRecord +import org.thoughtcrime.securesms.databinding.ConversationTestFragmentBinding +import org.thoughtcrime.securesms.groups.GroupId +import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange +import org.thoughtcrime.securesms.linkpreview.LinkPreview +import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory +import org.thoughtcrime.securesms.mms.GlideApp +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.stickers.StickerLocator +import org.thoughtcrime.securesms.util.doAfterNextLayout + +class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fragment) { + + companion object { + private val TAG = Log.tag(InternalConversationTestFragment::class.java) + } + + private val binding by ViewBinderDelegate(ConversationTestFragmentBinding::bind) + private val viewModel: InternalConversationTestViewModel by viewModels() + private val lifecycleDisposable = LifecycleDisposable() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val adapter = ConversationAdapterV2( + lifecycleOwner = viewLifecycleOwner, + glideRequests = GlideApp.with(this), + clickListener = ClickListener(), + hasWallpaper = false, + colorizer = Colorizer() + ) + + var startTime = 0L + var firstRender = true + lifecycleDisposable.bindTo(viewLifecycleOwner) + adapter.setPagingController(viewModel.controller) + lifecycleDisposable += viewModel.data.observeOn(AndroidSchedulers.mainThread()).subscribeBy { + if (firstRender) { + startTime = System.currentTimeMillis() + } + adapter.submitList(it) { + if (firstRender) { + firstRender = false + binding.root.doAfterNextLayout { + val endTime = System.currentTimeMillis() + Log.d(TAG, "First render in ${endTime - startTime} millis") + } + } + } + } + + binding.root.layoutManager = SmoothScrollingLinearLayoutManager(requireContext(), true) + binding.root.adapter = adapter + + RecyclerViewColorizer(binding.root).apply { + setChatColors(ChatColorsPalette.Bubbles.default.withId(ChatColors.Id.Auto)) + } + } + + private inner class ClickListener : ItemClickListener { + override fun onQuoteClicked(messageRecord: MmsMessageRecord?) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onLinkPreviewClicked(linkPreview: LinkPreview) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onQuotedIndicatorClicked(messageRecord: MessageRecord) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onMoreTextClicked(conversationRecipientId: RecipientId, messageId: Long, isMms: Boolean) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onStickerClicked(stickerLocator: StickerLocator) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onViewOnceMessageClicked(messageRecord: MmsMessageRecord) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onSharedContactDetailsClicked(contact: Contact, avatarTransitionView: View) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onAddToContactsClicked(contact: Contact) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onMessageSharedContactClicked(choices: MutableList) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onInviteSharedContactClicked(choices: MutableList) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onReactionClicked(multiselectPart: MultiselectPart, messageId: Long, isMms: Boolean) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onGroupMemberClicked(recipientId: RecipientId, groupId: GroupId) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onMessageWithErrorClicked(messageRecord: MessageRecord) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onMessageWithRecaptchaNeededClicked(messageRecord: MessageRecord) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onIncomingIdentityMismatchClicked(recipientId: RecipientId) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onRegisterVoiceNoteCallbacks(onPlaybackStartObserver: Observer) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onUnregisterVoiceNoteCallbacks(onPlaybackStartObserver: Observer) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onVoiceNotePause(uri: Uri) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onVoiceNotePlay(uri: Uri, messageId: Long, position: Double) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onVoiceNoteSeekTo(uri: Uri, position: Double) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onVoiceNotePlaybackSpeedChanged(uri: Uri, speed: Float) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onGroupMigrationLearnMoreClicked(membershipChange: GroupMigrationMembershipChange) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onChatSessionRefreshLearnMoreClicked() { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onBadDecryptLearnMoreClicked(author: RecipientId) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onSafetyNumberLearnMoreClicked(recipient: Recipient) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onJoinGroupCallClicked() { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onInviteFriendsToGroupClicked(groupId: GroupId.V2) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onEnableCallNotificationsClicked() { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onPlayInlineContent(conversationMessage: ConversationMessage?) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onInMemoryMessageClicked(messageRecord: InMemoryMessageRecord) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onViewGroupDescriptionChange(groupId: GroupId?, description: String, isMessageRequestAccepted: Boolean) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onChangeNumberUpdateContact(recipient: Recipient) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onCallToAction(action: String) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onDonateClicked() { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onBlockJoinRequest(recipient: Recipient) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onRecipientNameClicked(target: RecipientId) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onInviteToSignalClicked() { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onActivatePaymentsClicked() { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onSendPaymentClicked(recipientId: RecipientId) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onScheduledIndicatorClicked(view: View, conversationMessage: ConversationMessage) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onUrlClicked(url: String): Boolean { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + return true + } + + override fun onViewGiftBadgeClicked(messageRecord: MessageRecord) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onGiftBadgeRevealed(messageRecord: MessageRecord) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun goToMediaPreview(parent: ConversationItem?, sharedElement: View?, args: MediaIntentFactory.MediaPreviewArgs?) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onEditedIndicatorClicked(messageRecord: MessageRecord) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onShowGroupDescriptionClicked(groupName: String, description: String, shouldLinkifyWebLinks: Boolean) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onItemClick(item: MultiselectPart?) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onItemLongClick(itemView: View?, item: MultiselectPart?) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + } +} diff --git a/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/InternalConversationTestViewModel.kt b/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/InternalConversationTestViewModel.kt new file mode 100644 index 0000000000..8ecdc6a654 --- /dev/null +++ b/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/InternalConversationTestViewModel.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.internal.conversation + +import androidx.lifecycle.ViewModel +import org.signal.paging.PagedData +import org.signal.paging.PagingConfig + +class InternalConversationTestViewModel : ViewModel() { + private val generator = ConversationElementGenerator() + private val dataSource = InternalConversationTestDataSource( + 500, + generator + ) + + private val config = PagingConfig.Builder().setPageSize(25) + .setBufferPages(2) + .build() + + private val pagedData = PagedData.createForObservable(dataSource, config) + + val controller = pagedData.controller + val data = pagedData.data +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt index cfad09463d..0107dd3a9d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt @@ -612,6 +612,13 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter } dividerPref() + clickPref( + title = DSLSettingsText.from("Launch ConversationTestFragment"), + onClick = { + findNavController().safeNavigate(InternalSettingsFragmentDirections.actionInternalSettingsFragmentToInternalConversationTestFragment()) + } + ) + switchPref( title = DSLSettingsText.from("Use V2 ConversationFragment"), isChecked = state.useConversationFragmentV2, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/ConversationDataSource.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/ConversationDataSource.kt index b0bc2722f1..42f60dfc15 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/ConversationDataSource.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/ConversationDataSource.kt @@ -32,13 +32,18 @@ import org.whispersystems.signalservice.api.push.ServiceId private typealias ConversationElement = MappingModel<*> sealed interface ConversationElementKey { + + fun requireMessageId(): Long = error("Not implemented for this key") + companion object { fun forMessage(id: Long): ConversationElementKey = MessageBackedKey(id) val threadHeader: ConversationElementKey = ThreadHeaderKey } } -private data class MessageBackedKey(val id: Long) : ConversationElementKey +private data class MessageBackedKey(val id: Long) : ConversationElementKey { + override fun requireMessageId(): Long = id +} private object ThreadHeaderKey : ConversationElementKey /** diff --git a/app/src/main/res/layout/conversation_test_fragment.xml b/app/src/main/res/layout/conversation_test_fragment.xml new file mode 100644 index 0000000000..aad7d8e65f --- /dev/null +++ b/app/src/main/res/layout/conversation_test_fragment.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/app/src/main/res/navigation/app_settings.xml b/app/src/main/res/navigation/app_settings.xml index ba7281a2d2..c16d3f962b 100644 --- a/app/src/main/res/navigation/app_settings.xml +++ b/app/src/main/res/navigation/app_settings.xml @@ -26,9 +26,8 @@ app:popExitAnim="@anim/fragment_close_exit"> + app:argType="boolean" /> + + + @@ -858,7 +865,7 @@ + android:name="org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.UsernameLinkSettingsFragment">