fix(wallet): resolve DI scope mismatch, WalletState constructors, packaging conflict
- 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)
This commit is contained in:
parent
c722ecb3a7
commit
ad89eddfea
18 changed files with 149 additions and 89 deletions
|
|
@ -208,6 +208,7 @@ android {
|
||||||
packaging {
|
packaging {
|
||||||
resources.pickFirsts += setOf(
|
resources.pickFirsts += setOf(
|
||||||
"META-INF/versions/9/OSGI-INF/MANIFEST.MF",
|
"META-INF/versions/9/OSGI-INF/MANIFEST.MF",
|
||||||
|
"META-INF/gradle/incremental.annotation.processors",
|
||||||
)
|
)
|
||||||
|
|
||||||
jniLibs {
|
jniLibs {
|
||||||
|
|
@ -315,6 +316,11 @@ licensee {
|
||||||
allowUrl("https://asm.ow2.io/license.html")
|
allowUrl("https://asm.ow2.io/license.html")
|
||||||
allowUrl("https://www.gnu.org/licenses/agpl-3.0.txt")
|
allowUrl("https://www.gnu.org/licenses/agpl-3.0.txt")
|
||||||
allowUrl("https://github.com/mhssn95/compose-color-picker/blob/main/LICENSE")
|
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")
|
ignoreDependencies("com.github.matrix-org", "matrix-analytics-events")
|
||||||
// Ignore dependency that are not third-party licenses to us.
|
// Ignore dependency that are not third-party licenses to us.
|
||||||
ignoreDependencies(groupId = "io.element.android")
|
ignoreDependencies(groupId = "io.element.android")
|
||||||
|
|
|
||||||
|
|
@ -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.messages.impl.timeline.model.event.duration
|
||||||
import io.element.android.features.poll.api.create.CreatePollEntryPoint
|
import io.element.android.features.poll.api.create.CreatePollEntryPoint
|
||||||
import io.element.android.features.poll.api.create.CreatePollMode
|
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.BackstackWithOverlayBox
|
||||||
import io.element.android.libraries.architecture.BaseFlowNode
|
import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
import io.element.android.libraries.architecture.callback
|
import io.element.android.libraries.architecture.callback
|
||||||
|
|
@ -105,6 +106,7 @@ class MessagesFlowNode(
|
||||||
private val shareLocationEntryPoint: ShareLocationEntryPoint,
|
private val shareLocationEntryPoint: ShareLocationEntryPoint,
|
||||||
private val showLocationEntryPoint: ShowLocationEntryPoint,
|
private val showLocationEntryPoint: ShowLocationEntryPoint,
|
||||||
private val createPollEntryPoint: CreatePollEntryPoint,
|
private val createPollEntryPoint: CreatePollEntryPoint,
|
||||||
|
private val walletEntryPoint: WalletEntryPoint,
|
||||||
private val elementCallEntryPoint: ElementCallEntryPoint,
|
private val elementCallEntryPoint: ElementCallEntryPoint,
|
||||||
private val mediaViewerEntryPoint: MediaViewerEntryPoint,
|
private val mediaViewerEntryPoint: MediaViewerEntryPoint,
|
||||||
private val forwardEntryPoint: ForwardEntryPoint,
|
private val forwardEntryPoint: ForwardEntryPoint,
|
||||||
|
|
@ -179,6 +181,14 @@ class MessagesFlowNode(
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Thread(val threadRootId: ThreadId, val focusedEventId: EventId?) : NavTarget
|
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()
|
private val callback: MessagesEntryPoint.Callback = callback()
|
||||||
|
|
@ -293,6 +303,15 @@ class MessagesFlowNode(
|
||||||
override fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) {
|
override fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) {
|
||||||
backstack.push(NavTarget.Thread(threadRootId, focusedEventId))
|
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)
|
val inputs = MessagesNode.Inputs(focusedEventId = navTarget.focusedEventId)
|
||||||
createNode<MessagesNode>(buildContext, listOf(callback, inputs))
|
createNode<MessagesNode>(buildContext, listOf(callback, inputs))
|
||||||
|
|
@ -502,9 +521,39 @@ class MessagesFlowNode(
|
||||||
override fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) {
|
override fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) {
|
||||||
backstack.push(NavTarget.Thread(threadRootId, focusedEventId))
|
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<ThreadedMessagesNode>(buildContext, listOf(inputs, callback))
|
createNode<ThreadedMessagesNode>(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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,7 @@ class MessagesNode(
|
||||||
fun navigateToRoomDetails()
|
fun navigateToRoomDetails()
|
||||||
fun navigateToPinnedMessagesList()
|
fun navigateToPinnedMessagesList()
|
||||||
fun navigateToKnockRequestsList()
|
fun navigateToKnockRequestsList()
|
||||||
|
fun navigateToPaymentFlow(roomId: RoomId, recipientUserId: UserId?, recipientAddress: String?, amountLovelace: Long?)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
|
|
@ -226,6 +227,15 @@ class MessagesNode(
|
||||||
callback.navigateToThread(threadRootId, focusedEventId)
|
callback.navigateToThread(threadRootId, focusedEventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun navigateToPaymentFlow(
|
||||||
|
roomId: RoomId,
|
||||||
|
recipientUserId: UserId?,
|
||||||
|
recipientAddress: String?,
|
||||||
|
amountLovelace: Long?,
|
||||||
|
) {
|
||||||
|
callback.navigateToPaymentFlow(roomId, recipientUserId, recipientAddress, amountLovelace)
|
||||||
|
}
|
||||||
|
|
||||||
private fun displaySameRoomToast() {
|
private fun displaySameRoomToast() {
|
||||||
context.toast(CommonStrings.screen_room_permalink_same_room_android)
|
context.toast(CommonStrings.screen_room_permalink_same_room_android)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.TimelineItemUnknownContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
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.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.features.messages.impl.utils.messagesummary.DefaultMessageSummaryFormatter
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
|
@ -318,6 +319,9 @@ private fun MessageSummary(
|
||||||
is TimelineItemRtcNotificationContent -> {
|
is TimelineItemRtcNotificationContent -> {
|
||||||
content = { ContentForBody(stringResource(CommonStrings.common_call_started)) }
|
content = { ContentForBody(stringResource(CommonStrings.common_call_started)) }
|
||||||
}
|
}
|
||||||
|
is TimelineItemPaymentContentWrapper -> {
|
||||||
|
content = { ContentForBody(textContent) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Row(modifier = modifier) {
|
Row(modifier = modifier) {
|
||||||
icon()
|
icon()
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,7 @@ import kotlinx.coroutines.flow.merge
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes.Any as AnyMimeTypes
|
import io.element.android.libraries.core.mimetype.MimeTypes.Any as AnyMimeTypes
|
||||||
|
|
||||||
|
|
@ -345,7 +346,7 @@ class MessageComposerPresenter(
|
||||||
}
|
}
|
||||||
is ResolvedSuggestion.Command -> {
|
is ResolvedSuggestion.Command -> {
|
||||||
// Insert the command text with a trailing space
|
// Insert the command text with a trailing space
|
||||||
richTextEditorState.replaceText("${suggestion.command} ")
|
richTextEditorState.setMarkdown("${suggestion.command} ")
|
||||||
suggestionSearchTrigger.value = null
|
suggestionSearchTrigger.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -451,7 +452,7 @@ class MessageComposerPresenter(
|
||||||
when (payCommand) {
|
when (payCommand) {
|
||||||
is io.element.android.features.wallet.impl.slash.ParsedPayCommand.ParseError -> {
|
is io.element.android.features.wallet.impl.slash.ParsedPayCommand.ParseError -> {
|
||||||
// Show error, keep text in composer
|
// Show error, keep text in composer
|
||||||
snackbarDispatcher.post(SnackbarMessage(payCommand.reason))
|
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_error))
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
is io.element.android.features.wallet.impl.slash.ParsedPayCommand.WithAddressRecipient -> {
|
is io.element.android.features.wallet.impl.slash.ParsedPayCommand.WithAddressRecipient -> {
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ fun SuggestionsPickerView(
|
||||||
is ResolvedSuggestion.AtRoom -> "@room"
|
is ResolvedSuggestion.AtRoom -> "@room"
|
||||||
is ResolvedSuggestion.Member -> suggestion.roomMember.userId.value
|
is ResolvedSuggestion.Member -> suggestion.roomMember.userId.value
|
||||||
is ResolvedSuggestion.Alias -> suggestion.roomId.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.AtRoom -> roomAvatar?.copy(size = avatarSize) ?: AvatarData(roomId, roomName, null, avatarSize)
|
||||||
is ResolvedSuggestion.Member -> suggestion.roomMember.getAvatarData(avatarSize)
|
is ResolvedSuggestion.Member -> suggestion.roomMember.getAvatarData(avatarSize)
|
||||||
is ResolvedSuggestion.Alias -> suggestion.getAvatarData(avatarSize)
|
is ResolvedSuggestion.Alias -> suggestion.getAvatarData(avatarSize)
|
||||||
|
is ResolvedSuggestion.Command -> AvatarData(suggestion.command, suggestion.command, null, avatarSize)
|
||||||
}
|
}
|
||||||
val avatarType = when (suggestion) {
|
val avatarType = when (suggestion) {
|
||||||
is ResolvedSuggestion.Alias -> AvatarType.Room()
|
is ResolvedSuggestion.Alias,
|
||||||
|
is ResolvedSuggestion.Command -> AvatarType.Room()
|
||||||
ResolvedSuggestion.AtRoom,
|
ResolvedSuggestion.AtRoom,
|
||||||
is ResolvedSuggestion.Member -> AvatarType.User
|
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.AtRoom -> stringResource(R.string.screen_room_mentions_at_room_title)
|
||||||
is ResolvedSuggestion.Member -> suggestion.roomMember.displayName
|
is ResolvedSuggestion.Member -> suggestion.roomMember.displayName
|
||||||
is ResolvedSuggestion.Alias -> suggestion.roomName
|
is ResolvedSuggestion.Alias -> suggestion.roomName
|
||||||
|
is ResolvedSuggestion.Command -> suggestion.command
|
||||||
}
|
}
|
||||||
val subtitle = when (suggestion) {
|
val subtitle = when (suggestion) {
|
||||||
is ResolvedSuggestion.AtRoom -> "@room"
|
is ResolvedSuggestion.AtRoom -> "@room"
|
||||||
is ResolvedSuggestion.Member -> suggestion.roomMember.userId.value
|
is ResolvedSuggestion.Member -> suggestion.roomMember.userId.value
|
||||||
is ResolvedSuggestion.Alias -> suggestion.roomAlias.value
|
is ResolvedSuggestion.Alias -> suggestion.roomAlias.value
|
||||||
|
is ResolvedSuggestion.Command -> suggestion.description
|
||||||
}
|
}
|
||||||
Avatar(
|
Avatar(
|
||||||
avatarData = avatarData,
|
avatarData = avatarData,
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,7 @@ class ThreadedMessagesNode(
|
||||||
fun navigateToEditPoll(eventId: EventId)
|
fun navigateToEditPoll(eventId: EventId)
|
||||||
fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean)
|
fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean)
|
||||||
fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?)
|
fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?)
|
||||||
|
fun navigateToPaymentFlow(roomId: RoomId, recipientUserId: UserId?, recipientAddress: String?, amountLovelace: Long?)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
|
|
@ -237,6 +238,15 @@ class ThreadedMessagesNode(
|
||||||
callback.navigateToThread(threadRootId, focusedEventId)
|
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()
|
override fun close() = navigateUp()
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -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.TimelineItemUnknownContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
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.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.MatrixTimelineItem
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent
|
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.FailedToParseMessageLikeContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInviteContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInviteContent
|
||||||
|
|
@ -63,6 +65,7 @@ internal fun TimelineItem.Event.canBeGrouped(): Boolean {
|
||||||
TimelineItemUnknownContent,
|
TimelineItemUnknownContent,
|
||||||
is TimelineItemLegacyCallInviteContent,
|
is TimelineItemLegacyCallInviteContent,
|
||||||
is TimelineItemRtcNotificationContent -> false
|
is TimelineItemRtcNotificationContent -> false
|
||||||
|
is TimelineItemPaymentContentWrapper -> false
|
||||||
is TimelineItemProfileChangeContent,
|
is TimelineItemProfileChangeContent,
|
||||||
is TimelineItemRoomMembershipContent,
|
is TimelineItemRoomMembershipContent,
|
||||||
is TimelineItemStateEventContent -> true
|
is TimelineItemStateEventContent -> true
|
||||||
|
|
@ -91,6 +94,7 @@ internal fun MatrixTimelineItem.Event.canBeDisplayedInBubbleBlock(): Boolean {
|
||||||
UnknownContent,
|
UnknownContent,
|
||||||
is LegacyCallInviteContent,
|
is LegacyCallInviteContent,
|
||||||
CallNotifyContent,
|
CallNotifyContent,
|
||||||
is StateContent -> false
|
is StateContent,
|
||||||
|
is CustomEventContent -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,8 @@ fun TimelineItemEventContent.canReact(): Boolean =
|
||||||
is TimelineItemRedactedContent,
|
is TimelineItemRedactedContent,
|
||||||
is TimelineItemLegacyCallInviteContent,
|
is TimelineItemLegacyCallInviteContent,
|
||||||
is TimelineItemRtcNotificationContent,
|
is TimelineItemRtcNotificationContent,
|
||||||
TimelineItemUnknownContent -> false
|
TimelineItemUnknownContent,
|
||||||
|
is TimelineItemPaymentContentWrapper -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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.TimelineItemUnknownContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
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.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.
|
* 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 TimelineItemNoticeContent,
|
||||||
is TimelineItemTextContent,
|
is TimelineItemTextContent,
|
||||||
TimelineItemUnknownContent,
|
TimelineItemUnknownContent,
|
||||||
is TimelineItemVoiceContent -> false
|
is TimelineItemVoiceContent,
|
||||||
|
is TimelineItemPaymentContentWrapper -> false
|
||||||
}
|
}
|
||||||
is TimelineItem.Virtual -> false
|
is TimelineItem.Virtual -> false
|
||||||
is TimelineItem.GroupedEvents -> false
|
is TimelineItem.GroupedEvents -> false
|
||||||
|
|
|
||||||
|
|
@ -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.TimelineItemUnknownContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
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.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.core.extensions.toSafeLength
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||||
|
|
@ -54,6 +55,7 @@ class DefaultMessageSummaryFormatter(
|
||||||
is TimelineItemAudioContent -> context.getString(CommonStrings.common_audio)
|
is TimelineItemAudioContent -> context.getString(CommonStrings.common_audio)
|
||||||
is TimelineItemLegacyCallInviteContent -> context.getString(CommonStrings.common_unsupported_call)
|
is TimelineItemLegacyCallInviteContent -> context.getString(CommonStrings.common_unsupported_call)
|
||||||
is TimelineItemRtcNotificationContent -> context.getString(CommonStrings.common_call_started)
|
is TimelineItemRtcNotificationContent -> context.getString(CommonStrings.common_call_started)
|
||||||
|
is TimelineItemPaymentContentWrapper -> "Payment"
|
||||||
}
|
}
|
||||||
// Truncate the message to a safe length to avoid crashes in Compose
|
// Truncate the message to a safe length to avoid crashes in Compose
|
||||||
.toSafeLength()
|
.toSafeLength()
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
|
|
||||||
package io.element.android.features.wallet.impl.cardano
|
package io.element.android.features.wallet.impl.cardano
|
||||||
|
|
||||||
import com.bloxbean.cardano.client.account.Account
|
|
||||||
import dev.zacsweers.metro.AppScope
|
import dev.zacsweers.metro.AppScope
|
||||||
import dev.zacsweers.metro.ContributesBinding
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
import dev.zacsweers.metro.Inject
|
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 io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
interface CardanoWalletManager {
|
interface CardanoWalletManager {
|
||||||
|
|
@ -24,126 +22,79 @@ interface CardanoWalletManager {
|
||||||
suspend fun initialize(sessionId: SessionId)
|
suspend fun initialize(sessionId: SessionId)
|
||||||
suspend fun getAddress(sessionId: SessionId): Result<String>
|
suspend fun getAddress(sessionId: SessionId): Result<String>
|
||||||
suspend fun getStakeAddress(sessionId: SessionId): Result<String>
|
suspend fun getStakeAddress(sessionId: SessionId): Result<String>
|
||||||
suspend fun getSpendingKey(sessionId: SessionId, addressIndex: Int = 0): Result<ByteArray>
|
/** Called by session-scoped components after fetching balance from chain. */
|
||||||
suspend fun refreshBalance(sessionId: SessionId)
|
suspend fun refreshBalance(sessionId: SessionId, balanceLovelace: Long)
|
||||||
fun clearState()
|
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)
|
@SingleIn(AppScope::class)
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultCardanoWalletManager @Inject constructor(
|
class DefaultCardanoWalletManager @Inject constructor(
|
||||||
private val keyStorage: CardanoKeyStorage,
|
private val keyStorage: CardanoKeyStorage,
|
||||||
private val cardanoClient: io.element.android.features.wallet.api.CardanoClient,
|
|
||||||
) : CardanoWalletManager {
|
) : CardanoWalletManager {
|
||||||
|
|
||||||
private val _walletState = MutableStateFlow(WalletState.Initial)
|
private val _walletState = MutableStateFlow(WalletState.Initial)
|
||||||
override val walletState: StateFlow<WalletState> = _walletState.asStateFlow()
|
override val walletState: StateFlow<WalletState> = _walletState
|
||||||
|
|
||||||
override suspend fun initialize(sessionId: SessionId) {
|
override suspend fun initialize(sessionId: SessionId) {
|
||||||
_walletState.value = WalletState.Initial.copy(isLoading = true)
|
_walletState.value = WalletState.Initial.copy(isLoading = true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val hasWallet = keyStorage.hasWallet(sessionId)
|
val hasWallet = keyStorage.hasWallet(sessionId)
|
||||||
|
|
||||||
if (hasWallet) {
|
if (hasWallet) {
|
||||||
val address = keyStorage.getBaseAddress(sessionId).getOrNull()
|
val address = keyStorage.getBaseAddress(sessionId).getOrNull()
|
||||||
_walletState.value = WalletState(
|
_walletState.value = WalletState(
|
||||||
|
isLoading = false,
|
||||||
hasWallet = true,
|
hasWallet = true,
|
||||||
address = address,
|
address = address,
|
||||||
balanceLovelace = null,
|
balanceLovelace = 0L,
|
||||||
balanceAda = null,
|
balanceAda = "0",
|
||||||
isLoading = false,
|
|
||||||
error = null,
|
error = null,
|
||||||
)
|
)
|
||||||
Timber.d("Initialized wallet for session: ${sessionId.value}, address: $address")
|
|
||||||
} else {
|
} else {
|
||||||
_walletState.value = WalletState(
|
_walletState.value = WalletState(
|
||||||
|
isLoading = false,
|
||||||
hasWallet = false,
|
hasWallet = false,
|
||||||
address = null,
|
address = null,
|
||||||
balanceLovelace = null,
|
balanceLovelace = null,
|
||||||
balanceAda = null,
|
balanceAda = null,
|
||||||
isLoading = false,
|
|
||||||
error = null,
|
error = null,
|
||||||
)
|
)
|
||||||
Timber.d("No wallet found for session: ${sessionId.value}")
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "Failed to initialize wallet for session: ${sessionId.value}")
|
Timber.e(e, "Failed to initialize wallet")
|
||||||
_walletState.value = WalletState(
|
_walletState.value = WalletState(
|
||||||
|
isLoading = false,
|
||||||
hasWallet = false,
|
hasWallet = false,
|
||||||
address = null,
|
address = null,
|
||||||
balanceLovelace = null,
|
balanceLovelace = null,
|
||||||
balanceAda = null,
|
balanceAda = null,
|
||||||
|
error = e.message,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getAddress(sessionId: SessionId): Result<String> =
|
||||||
|
keyStorage.getBaseAddress(sessionId)
|
||||||
|
|
||||||
|
override suspend fun getStakeAddress(sessionId: SessionId): Result<String> =
|
||||||
|
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,
|
isLoading = false,
|
||||||
error = e.message ?: "Failed to load wallet",
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getAddress(sessionId: SessionId): Result<String> {
|
|
||||||
return keyStorage.getBaseAddress(sessionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getStakeAddress(sessionId: SessionId): Result<String> {
|
|
||||||
return keyStorage.getStakeAddress(sessionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getSpendingKey(sessionId: SessionId, addressIndex: Int): Result<ByteArray> {
|
|
||||||
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() {
|
override fun clearState() {
|
||||||
_walletState.value = WalletState.Initial
|
_walletState.value = WalletState.Initial
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,13 @@ class TimelineItemContentPaymentFactory {
|
||||||
/**
|
/**
|
||||||
* Check if a message is a payment message.
|
* 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 {
|
fun isPaymentMessage(body: String): Boolean {
|
||||||
return body.startsWith(DefaultPaymentEventSender.PAYMENT_MESSAGE_PREFIX)
|
return body.startsWith(DefaultPaymentEventSender.PAYMENT_MESSAGE_PREFIX)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ class CardanoWalletManagerTest {
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
fakeKeyStorage = FakeCardanoKeyStorage()
|
fakeKeyStorage = FakeCardanoKeyStorage()
|
||||||
fakeCardanoClient = FakeCardanoClient()
|
fakeCardanoClient = FakeCardanoClient()
|
||||||
walletManager = DefaultCardanoWalletManager(fakeKeyStorage, fakeCardanoClient)
|
walletManager = DefaultCardanoWalletManager(fakeKeyStorage)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -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.roomlist.LatestEventValue
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
|
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.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.EmoteMessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
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 LegacyCallInviteContent -> sp.getString(CommonStrings.common_unsupported_call)
|
||||||
is CallNotifyContent -> sp.getString(CommonStrings.common_call_started)
|
is CallNotifyContent -> sp.getString(CommonStrings.common_call_started)
|
||||||
|
is CustomEventContent -> null
|
||||||
}?.take(DEFAULT_SAFE_LENGTH)
|
}?.take(DEFAULT_SAFE_LENGTH)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.eventformatter.impl.mode.RenderingMode
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
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.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.EventContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
||||||
|
|
@ -71,7 +72,8 @@ class DefaultTimelineEventFormatter(
|
||||||
is FailedToParseMessageLikeContent,
|
is FailedToParseMessageLikeContent,
|
||||||
is FailedToParseStateContent,
|
is FailedToParseStateContent,
|
||||||
is LiveLocationContent,
|
is LiveLocationContent,
|
||||||
is UnknownContent -> {
|
is UnknownContent,
|
||||||
|
is CustomEventContent -> {
|
||||||
if (buildMeta.isDebuggable) {
|
if (buildMeta.isDebuggable) {
|
||||||
error("You should not use this formatter for this event content: $content")
|
error("You should not use this formatter for this event content: $content")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.ui.res.stringResource
|
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.AudioMessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent
|
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.FailedToParseMessageLikeContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
|
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 LegacyCallInviteContent,
|
||||||
is CallNotifyContent,
|
is CallNotifyContent,
|
||||||
is LiveLocationContent,
|
is LiveLocationContent,
|
||||||
|
is CustomEventContent,
|
||||||
null -> null
|
null -> null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.TextMessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
|
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.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.VideoMessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl
|
import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl
|
||||||
|
|
@ -77,7 +78,8 @@ class EventItemFactory(
|
||||||
is StickerContent,
|
is StickerContent,
|
||||||
is UnableToDecryptContent,
|
is UnableToDecryptContent,
|
||||||
is LiveLocationContent,
|
is LiveLocationContent,
|
||||||
UnknownContent -> {
|
UnknownContent,
|
||||||
|
is CustomEventContent -> {
|
||||||
Timber.w("Should not happen: ${content.javaClass.simpleName}")
|
Timber.w("Should not happen: ${content.javaClass.simpleName}")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue