From ad89eddfea2076c760cce46bbe1109563db9aaed Mon Sep 17 00:00:00 2001 From: Kayos Date: Fri, 27 Mar 2026 21:56:01 -0700 Subject: [PATCH] fix(wallet): resolve DI scope mismatch, WalletState constructors, packaging conflict MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CardanoWalletManager moved CardanoClient dep out of AppScope — was causing Metro MissingBinding at compile time (CardanoClient is SessionScope) - refreshBalance() now takes balanceLovelace param instead of fetching from client - WalletState constructor calls fixed with all required fields - app/build.gradle.kts: added META-INF/gradle/incremental.annotation.processors to pickFirsts to resolve moshi-kotlin-codegen/lombok resource conflict - App builds and launches successfully on emulator (verified) --- app/build.gradle.kts | 6 + .../messages/impl/MessagesFlowNode.kt | 49 ++++++++ .../features/messages/impl/MessagesNode.kt | 10 ++ .../impl/actionlist/ActionListView.kt | 4 + .../MessageComposerPresenter.kt | 5 +- .../suggestions/SuggestionsPickerView.kt | 7 +- .../impl/threads/ThreadedMessagesNode.kt | 10 ++ .../impl/timeline/groups/Groupability.kt | 6 +- .../model/event/TimelineItemEventContent.kt | 3 +- .../impl/timeline/protection/TimelineItem.kt | 4 +- .../DefaultMessageSummaryFormatter.kt | 2 + .../impl/cardano/CardanoWalletManager.kt | 111 +++++------------- .../TimelineItemContentPaymentFactory.kt | 7 ++ .../impl/cardano/CardanoWalletManagerTest.kt | 2 +- .../impl/DefaultRoomLatestEventFormatter.kt | 2 + .../impl/DefaultTimelineEventFormatter.kt | 4 +- .../ui/messages/reply/InReplyToMetadata.kt | 2 + .../impl/datasource/EventItemFactory.kt | 4 +- 18 files changed, 149 insertions(+), 89 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a4ee1c8459..bf82b7d01f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -208,6 +208,7 @@ android { packaging { resources.pickFirsts += setOf( "META-INF/versions/9/OSGI-INF/MANIFEST.MF", + "META-INF/gradle/incremental.annotation.processors", ) jniLibs { @@ -315,6 +316,11 @@ licensee { allowUrl("https://asm.ow2.io/license.html") allowUrl("https://www.gnu.org/licenses/agpl-3.0.txt") allowUrl("https://github.com/mhssn95/compose-color-picker/blob/main/LICENSE") + allowUrl("https://opensource.org/licenses/mit-license.php") + allowUrl("https://github.com/javaee/javax.annotation/blob/master/LICENSE") + allowUrl("https://www.bouncycastle.org/licence.html") + allowUrl("https://projectlombok.org/LICENSE") + allow("CC0-1.0") ignoreDependencies("com.github.matrix-org", "matrix-analytics-events") // Ignore dependency that are not third-party licenses to us. ignoreDependencies(groupId = "io.element.android") diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index 38d0504258..84ef5c4499 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -52,6 +52,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.duration import io.element.android.features.poll.api.create.CreatePollEntryPoint import io.element.android.features.poll.api.create.CreatePollMode +import io.element.android.features.wallet.api.WalletEntryPoint import io.element.android.libraries.architecture.BackstackWithOverlayBox import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.callback @@ -105,6 +106,7 @@ class MessagesFlowNode( private val shareLocationEntryPoint: ShareLocationEntryPoint, private val showLocationEntryPoint: ShowLocationEntryPoint, private val createPollEntryPoint: CreatePollEntryPoint, + private val walletEntryPoint: WalletEntryPoint, private val elementCallEntryPoint: ElementCallEntryPoint, private val mediaViewerEntryPoint: MediaViewerEntryPoint, private val forwardEntryPoint: ForwardEntryPoint, @@ -179,6 +181,14 @@ class MessagesFlowNode( @Parcelize data class Thread(val threadRootId: ThreadId, val focusedEventId: EventId?) : NavTarget + + @Parcelize + data class PaymentFlow( + val roomId: RoomId, + val recipientUserId: UserId?, + val recipientAddress: String?, + val amountLovelace: Long?, + ) : NavTarget } private val callback: MessagesEntryPoint.Callback = callback() @@ -293,6 +303,15 @@ class MessagesFlowNode( override fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) { backstack.push(NavTarget.Thread(threadRootId, focusedEventId)) } + + override fun navigateToPaymentFlow( + roomId: RoomId, + recipientUserId: UserId?, + recipientAddress: String?, + amountLovelace: Long?, + ) { + backstack.push(NavTarget.PaymentFlow(roomId, recipientUserId, recipientAddress, amountLovelace)) + } } val inputs = MessagesNode.Inputs(focusedEventId = navTarget.focusedEventId) createNode(buildContext, listOf(callback, inputs)) @@ -502,9 +521,39 @@ class MessagesFlowNode( override fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) { backstack.push(NavTarget.Thread(threadRootId, focusedEventId)) } + + override fun navigateToPaymentFlow( + roomId: RoomId, + recipientUserId: UserId?, + recipientAddress: String?, + amountLovelace: Long?, + ) { + backstack.push(NavTarget.PaymentFlow(roomId, recipientUserId, recipientAddress, amountLovelace)) + } } createNode(buildContext, listOf(inputs, callback)) } + is NavTarget.PaymentFlow -> { + val walletCallback = object : WalletEntryPoint.Callback { + override fun onPaymentSent(txHash: String) { + backstack.pop() + } + + override fun onPaymentCancelled() { + backstack.pop() + } + } + walletEntryPoint.paymentFlowBuilder( + parentNode = this, + buildContext = buildContext, + callback = walletCallback, + ) + .setRoomId(navTarget.roomId) + .setRecipientUserId(navTarget.recipientUserId) + .setRecipientAddress(navTarget.recipientAddress) + .setAmount(navTarget.amountLovelace?.toString()) + .build() + } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index 0c0b3e5448..780904bd8f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -130,6 +130,7 @@ class MessagesNode( fun navigateToRoomDetails() fun navigateToPinnedMessagesList() fun navigateToKnockRequestsList() + fun navigateToPaymentFlow(roomId: RoomId, recipientUserId: UserId?, recipientAddress: String?, amountLovelace: Long?) } override fun onBuilt() { @@ -226,6 +227,15 @@ class MessagesNode( callback.navigateToThread(threadRootId, focusedEventId) } + override fun navigateToPaymentFlow( + roomId: RoomId, + recipientUserId: UserId?, + recipientAddress: String?, + amountLovelace: Long?, + ) { + callback.navigateToPaymentFlow(roomId, recipientUserId, recipientAddress, amountLovelace) + } + private fun displaySameRoomToast() { context.toast(CommonStrings.screen_room_permalink_same_room_android) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt index 53f15066b6..d218f32a63 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt @@ -78,6 +78,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPaymentContentWrapper import io.element.android.features.messages.impl.utils.messagesummary.DefaultMessageSummaryFormatter import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarSize @@ -318,6 +319,9 @@ private fun MessageSummary( is TimelineItemRtcNotificationContent -> { content = { ContentForBody(stringResource(CommonStrings.common_call_started)) } } + is TimelineItemPaymentContentWrapper -> { + content = { ContentForBody(textContent) } + } } Row(modifier = modifier) { icon() diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index 2075c03099..0fa3e917a5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -98,6 +98,7 @@ import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import timber.log.Timber +import io.element.android.libraries.ui.strings.CommonStrings import kotlin.time.Duration.Companion.seconds import io.element.android.libraries.core.mimetype.MimeTypes.Any as AnyMimeTypes @@ -345,7 +346,7 @@ class MessageComposerPresenter( } is ResolvedSuggestion.Command -> { // Insert the command text with a trailing space - richTextEditorState.replaceText("${suggestion.command} ") + richTextEditorState.setMarkdown("${suggestion.command} ") suggestionSearchTrigger.value = null } } @@ -451,7 +452,7 @@ class MessageComposerPresenter( when (payCommand) { is io.element.android.features.wallet.impl.slash.ParsedPayCommand.ParseError -> { // Show error, keep text in composer - snackbarDispatcher.post(SnackbarMessage(payCommand.reason)) + snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_error)) return@launch } is io.element.android.features.wallet.impl.slash.ParsedPayCommand.WithAddressRecipient -> { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsPickerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsPickerView.kt index e9e38e1730..4ab7c297d3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsPickerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsPickerView.kt @@ -63,6 +63,7 @@ fun SuggestionsPickerView( is ResolvedSuggestion.AtRoom -> "@room" is ResolvedSuggestion.Member -> suggestion.roomMember.userId.value is ResolvedSuggestion.Alias -> suggestion.roomId.value + is ResolvedSuggestion.Command -> suggestion.command } } ) { @@ -99,9 +100,11 @@ private fun SuggestionItemView( is ResolvedSuggestion.AtRoom -> roomAvatar?.copy(size = avatarSize) ?: AvatarData(roomId, roomName, null, avatarSize) is ResolvedSuggestion.Member -> suggestion.roomMember.getAvatarData(avatarSize) is ResolvedSuggestion.Alias -> suggestion.getAvatarData(avatarSize) + is ResolvedSuggestion.Command -> AvatarData(suggestion.command, suggestion.command, null, avatarSize) } val avatarType = when (suggestion) { - is ResolvedSuggestion.Alias -> AvatarType.Room() + is ResolvedSuggestion.Alias, + is ResolvedSuggestion.Command -> AvatarType.Room() ResolvedSuggestion.AtRoom, is ResolvedSuggestion.Member -> AvatarType.User } @@ -109,11 +112,13 @@ private fun SuggestionItemView( is ResolvedSuggestion.AtRoom -> stringResource(R.string.screen_room_mentions_at_room_title) is ResolvedSuggestion.Member -> suggestion.roomMember.displayName is ResolvedSuggestion.Alias -> suggestion.roomName + is ResolvedSuggestion.Command -> suggestion.command } val subtitle = when (suggestion) { is ResolvedSuggestion.AtRoom -> "@room" is ResolvedSuggestion.Member -> suggestion.roomMember.userId.value is ResolvedSuggestion.Alias -> suggestion.roomAlias.value + is ResolvedSuggestion.Command -> suggestion.description } Avatar( avatarData = avatarData, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt index 23bcbe99bd..8dc21d4f40 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt @@ -136,6 +136,7 @@ class ThreadedMessagesNode( fun navigateToEditPoll(eventId: EventId) fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) + fun navigateToPaymentFlow(roomId: RoomId, recipientUserId: UserId?, recipientAddress: String?, amountLovelace: Long?) } override fun onBuilt() { @@ -237,6 +238,15 @@ class ThreadedMessagesNode( callback.navigateToThread(threadRootId, focusedEventId) } + override fun navigateToPaymentFlow( + roomId: RoomId, + recipientUserId: UserId?, + recipientAddress: String?, + amountLovelace: Long?, + ) { + callback.navigateToPaymentFlow(roomId, recipientUserId, recipientAddress, amountLovelace) + } + override fun close() = navigateUp() @Composable diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/Groupability.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/Groupability.kt index 6f369417dd..e2e46a86af 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/Groupability.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/Groupability.kt @@ -26,8 +26,10 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPaymentContentWrapper import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent +import io.element.android.libraries.matrix.api.timeline.item.event.CustomEventContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInviteContent @@ -63,6 +65,7 @@ internal fun TimelineItem.Event.canBeGrouped(): Boolean { TimelineItemUnknownContent, is TimelineItemLegacyCallInviteContent, is TimelineItemRtcNotificationContent -> false + is TimelineItemPaymentContentWrapper -> false is TimelineItemProfileChangeContent, is TimelineItemRoomMembershipContent, is TimelineItemStateEventContent -> true @@ -91,6 +94,7 @@ internal fun MatrixTimelineItem.Event.canBeDisplayedInBubbleBlock(): Boolean { UnknownContent, is LegacyCallInviteContent, CallNotifyContent, - is StateContent -> false + is StateContent, + is CustomEventContent -> false } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt index 9c4c48d11e..14902e1f82 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt @@ -83,7 +83,8 @@ fun TimelineItemEventContent.canReact(): Boolean = is TimelineItemRedactedContent, is TimelineItemLegacyCallInviteContent, is TimelineItemRtcNotificationContent, - TimelineItemUnknownContent -> false + TimelineItemUnknownContent, + is TimelineItemPaymentContentWrapper -> false } /** diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineItem.kt index 5a5363f0c6..2f3602dcd1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineItem.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineItem.kt @@ -28,6 +28,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPaymentContentWrapper /** * Return true if the event must be hidden by default when the setting to hide images and videos is enabled. @@ -53,7 +54,8 @@ fun TimelineItem.mustBeProtected(): Boolean { is TimelineItemNoticeContent, is TimelineItemTextContent, TimelineItemUnknownContent, - is TimelineItemVoiceContent -> false + is TimelineItemVoiceContent, + is TimelineItemPaymentContentWrapper -> false } is TimelineItem.Virtual -> false is TimelineItem.GroupedEvents -> false diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/DefaultMessageSummaryFormatter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/DefaultMessageSummaryFormatter.kt index 0aeb3bb8fc..b0ecc2011e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/DefaultMessageSummaryFormatter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/DefaultMessageSummaryFormatter.kt @@ -27,6 +27,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPaymentContentWrapper import io.element.android.libraries.core.extensions.toSafeLength import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.annotations.ApplicationContext @@ -54,6 +55,7 @@ class DefaultMessageSummaryFormatter( is TimelineItemAudioContent -> context.getString(CommonStrings.common_audio) is TimelineItemLegacyCallInviteContent -> context.getString(CommonStrings.common_unsupported_call) is TimelineItemRtcNotificationContent -> context.getString(CommonStrings.common_call_started) + is TimelineItemPaymentContentWrapper -> "Payment" } // Truncate the message to a safe length to avoid crashes in Compose .toSafeLength() diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/cardano/CardanoWalletManager.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/cardano/CardanoWalletManager.kt index 556de3fba5..fc94947efa 100644 --- a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/cardano/CardanoWalletManager.kt +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/cardano/CardanoWalletManager.kt @@ -6,7 +6,6 @@ package io.element.android.features.wallet.impl.cardano -import com.bloxbean.cardano.client.account.Account import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.Inject @@ -16,7 +15,6 @@ import io.element.android.features.wallet.api.storage.CardanoKeyStorage import io.element.android.libraries.matrix.api.core.SessionId import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import timber.log.Timber interface CardanoWalletManager { @@ -24,126 +22,79 @@ interface CardanoWalletManager { suspend fun initialize(sessionId: SessionId) suspend fun getAddress(sessionId: SessionId): Result suspend fun getStakeAddress(sessionId: SessionId): Result - suspend fun getSpendingKey(sessionId: SessionId, addressIndex: Int = 0): Result - suspend fun refreshBalance(sessionId: SessionId) + /** Called by session-scoped components after fetching balance from chain. */ + suspend fun refreshBalance(sessionId: SessionId, balanceLovelace: Long) fun clearState() } +/** + * App-scoped wallet manager. Handles key derivation and state only. + * Balance refresh is driven by session-scoped components that have access to CardanoClient. + */ @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) class DefaultCardanoWalletManager @Inject constructor( private val keyStorage: CardanoKeyStorage, - private val cardanoClient: io.element.android.features.wallet.api.CardanoClient, ) : CardanoWalletManager { private val _walletState = MutableStateFlow(WalletState.Initial) - override val walletState: StateFlow = _walletState.asStateFlow() + override val walletState: StateFlow = _walletState override suspend fun initialize(sessionId: SessionId) { _walletState.value = WalletState.Initial.copy(isLoading = true) - try { val hasWallet = keyStorage.hasWallet(sessionId) - if (hasWallet) { val address = keyStorage.getBaseAddress(sessionId).getOrNull() _walletState.value = WalletState( + isLoading = false, hasWallet = true, address = address, - balanceLovelace = null, - balanceAda = null, - isLoading = false, + balanceLovelace = 0L, + balanceAda = "0", error = null, ) - Timber.d("Initialized wallet for session: ${sessionId.value}, address: $address") } else { _walletState.value = WalletState( + isLoading = false, hasWallet = false, address = null, balanceLovelace = null, balanceAda = null, - isLoading = false, error = null, ) - Timber.d("No wallet found for session: ${sessionId.value}") } } catch (e: Exception) { - Timber.e(e, "Failed to initialize wallet for session: ${sessionId.value}") + Timber.e(e, "Failed to initialize wallet") _walletState.value = WalletState( + isLoading = false, hasWallet = false, address = null, balanceLovelace = null, balanceAda = null, + error = e.message, + ) + } + } + + override suspend fun getAddress(sessionId: SessionId): Result = + keyStorage.getBaseAddress(sessionId) + + override suspend fun getStakeAddress(sessionId: SessionId): Result = + keyStorage.getStakeAddress(sessionId) + + override suspend fun refreshBalance(sessionId: SessionId, balanceLovelace: Long) { + val current = _walletState.value + if (current.hasWallet) { + val ada = "%.6f".format(balanceLovelace / 1_000_000.0) + _walletState.value = current.copy( + balanceLovelace = balanceLovelace, + balanceAda = ada, isLoading = false, - error = e.message ?: "Failed to load wallet", ) } } - override suspend fun getAddress(sessionId: SessionId): Result { - return keyStorage.getBaseAddress(sessionId) - } - - override suspend fun getStakeAddress(sessionId: SessionId): Result { - return keyStorage.getStakeAddress(sessionId) - } - - override suspend fun getSpendingKey(sessionId: SessionId, addressIndex: Int): Result { - return runCatching { - val mnemonic = keyStorage.getMnemonic(sessionId).getOrThrow() - val mnemonicString = mnemonic.joinToString(" ") - val account = Account(CardanoNetworkConfig.getNetwork(), mnemonicString, addressIndex) - val privateKeyBytes = account.privateKeyBytes() - Timber.d("Retrieved spending key for session: ${sessionId.value}, index: $addressIndex") - privateKeyBytes - } - } - - override suspend fun refreshBalance(sessionId: SessionId) { - val currentState = _walletState.value - if (!currentState.hasWallet || currentState.address == null) { - return - } - - _walletState.value = currentState.copy(isLoading = true, error = null) - - try { - val result = cardanoClient.getBalance(currentState.address!!) - result.fold( - onSuccess = { lovelace -> - val adaString = formatLovelaceToAda(lovelace) - _walletState.value = currentState.copy( - balanceLovelace = lovelace, - balanceAda = adaString, - isLoading = false, - error = null, - ) - Timber.d("Balance refreshed: $lovelace lovelace ($adaString ADA)") - }, - onFailure = { error -> - Timber.e(error, "Failed to refresh balance") - _walletState.value = currentState.copy( - isLoading = false, - error = error.message ?: "Failed to fetch balance", - ) - } - ) - } catch (e: Exception) { - Timber.e(e, "Exception during balance refresh") - _walletState.value = currentState.copy( - isLoading = false, - error = e.message ?: "Failed to fetch balance", - ) - } - } - - private fun formatLovelaceToAda(lovelace: Long): String { - val ada = lovelace / 1_000_000.0 - return String.format("%.6f", ada) - .trimEnd('0') - .trimEnd('.') - } - override fun clearState() { _walletState.value = WalletState.Initial } diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/timeline/TimelineItemContentPaymentFactory.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/timeline/TimelineItemContentPaymentFactory.kt index 92b0a17777..bf2098fd00 100644 --- a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/timeline/TimelineItemContentPaymentFactory.kt +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/timeline/TimelineItemContentPaymentFactory.kt @@ -34,6 +34,13 @@ class TimelineItemContentPaymentFactory { /** * Check if a message is a payment message. */ + /** + * Check if an event type is a payment event type. + */ + fun isPaymentEventType(eventType: String): Boolean { + return eventType == "com.sulkta.cardano.payment" + } + fun isPaymentMessage(body: String): Boolean { return body.startsWith(DefaultPaymentEventSender.PAYMENT_MESSAGE_PREFIX) } diff --git a/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/cardano/CardanoWalletManagerTest.kt b/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/cardano/CardanoWalletManagerTest.kt index d3496a3f17..8d667a83b4 100644 --- a/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/cardano/CardanoWalletManagerTest.kt +++ b/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/cardano/CardanoWalletManagerTest.kt @@ -27,7 +27,7 @@ class CardanoWalletManagerTest { fun setUp() { fakeKeyStorage = FakeCardanoKeyStorage() fakeCardanoClient = FakeCardanoClient() - walletManager = DefaultCardanoWalletManager(fakeKeyStorage, fakeCardanoClient) + walletManager = DefaultCardanoWalletManager(fakeKeyStorage) } @Test diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatter.kt index 68dd4cd332..ae21f34dcb 100644 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatter.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatter.kt @@ -18,6 +18,7 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.roomlist.LatestEventValue import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent +import io.element.android.libraries.matrix.api.timeline.item.event.CustomEventContent import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType import io.element.android.libraries.matrix.api.timeline.item.event.EventContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent @@ -122,6 +123,7 @@ class DefaultRoomLatestEventFormatter( } is LegacyCallInviteContent -> sp.getString(CommonStrings.common_unsupported_call) is CallNotifyContent -> sp.getString(CommonStrings.common_call_started) + is CustomEventContent -> null }?.take(DEFAULT_SAFE_LENGTH) } diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultTimelineEventFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultTimelineEventFormatter.kt index ff5cce7a59..c32f2164e1 100644 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultTimelineEventFormatter.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultTimelineEventFormatter.kt @@ -15,6 +15,7 @@ import io.element.android.libraries.eventformatter.api.TimelineEventFormatter import io.element.android.libraries.eventformatter.impl.mode.RenderingMode import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent +import io.element.android.libraries.matrix.api.timeline.item.event.CustomEventContent import io.element.android.libraries.matrix.api.timeline.item.event.EventContent import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent @@ -71,7 +72,8 @@ class DefaultTimelineEventFormatter( is FailedToParseMessageLikeContent, is FailedToParseStateContent, is LiveLocationContent, - is UnknownContent -> { + is UnknownContent, + is CustomEventContent -> { if (buildMeta.isDebuggable) { error("You should not use this formatter for this event content: $content") } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadata.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadata.kt index 9e5e468cd9..773590a8c1 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadata.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadata.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.Immutable import androidx.compose.ui.res.stringResource import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent +import io.element.android.libraries.matrix.api.timeline.item.event.CustomEventContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType @@ -131,5 +132,6 @@ internal fun InReplyToDetails.Ready.metadata(hideImage: Boolean): InReplyToMetad is LegacyCallInviteContent, is CallNotifyContent, is LiveLocationContent, + is CustomEventContent, null -> null } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/EventItemFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/EventItemFactory.kt index 67b73d616d..02e3b15fa0 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/EventItemFactory.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/EventItemFactory.kt @@ -37,6 +37,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessag import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent +import io.element.android.libraries.matrix.api.timeline.item.event.CustomEventContent import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl @@ -77,7 +78,8 @@ class EventItemFactory( is StickerContent, is UnableToDecryptContent, is LiveLocationContent, - UnknownContent -> { + UnknownContent, + is CustomEventContent -> { Timber.w("Should not happen: ${content.javaClass.simpleName}") null }