feat(wallet): payment card timeline item and raw event handling (Tasks 7+8)

Task 7: Timeline Payment Card
- TimelineItemPaymentView integration with TimelineItemEventContentView
- Payment card rendering for both sender and recipient perspectives
- Unit tests for TimelineItemPaymentContent

Task 8: Raw Event Handling
- Modified TimelineItemContentMessageFactory to intercept payment events
- Added isSentByMe parameter propagation through content factories
- FakePaymentEventSender for testing
- Unit tests for TimelineItemContentPaymentFactory

SDK Limitation Workaround:
Since matrix-rust-sdk doesn't expose raw event sending or UnknownContent
raw JSON, payment events are encoded as text messages with a marker:
[cardano-payment:v1]{...json...}

This falls back gracefully for non-wallet clients while enabling
rich payment card rendering for wallet-enabled clients.
This commit is contained in:
Kayos 2026-03-27 11:08:03 -07:00
parent 39561e1aeb
commit adee67cf0d
16 changed files with 1410 additions and 12 deletions

View file

@ -30,6 +30,8 @@ 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.wallet.api.timeline.TimelineItemPaymentContent
import io.element.android.features.wallet.impl.timeline.TimelineItemPaymentView
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
import io.element.android.wysiwyg.link.Link
@ -134,6 +136,10 @@ fun TimelineItemEventContentView(
modifier = modifier
)
}
is TimelineItemPaymentContent -> TimelineItemPaymentView(
content = content,
modifier = modifier
)
is TimelineItemRtcNotificationContent -> error("This shouldn't be rendered as the content of a bubble")
}
}

View file

@ -78,6 +78,7 @@ class TimelineItemContentFactory(
senderProfile = senderProfile,
content = itemContent,
eventId = eventId,
isSentByMe = isOutgoing,
)
}
is ProfileChangeContent -> {

View file

@ -73,6 +73,7 @@ class TimelineItemContentMessageFactory(
senderId: UserId,
senderProfile: ProfileDetails,
eventId: EventId?,
isSentByMe: Boolean = false,
): TimelineItemEventContent {
return when (val messageType = content.type) {
is EmoteMessageType -> {
@ -256,16 +257,13 @@ class TimelineItemContentMessageFactory(
}
is TextMessageType -> {
val body = messageType.body.trimEnd()
val dom = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser)
val formattedBody = dom?.let(::parseHtml)
?: textPillificationHelper.pillify(body).safeLinkify()
val htmlDocument = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser)
TimelineItemTextContent(
body = body,
htmlDocument = htmlDocument,
formattedBody = formattedBody,
isEdited = content.isEdited,
)
// Check for Cardano payment events embedded in text messages
if (paymentFactory.isPaymentEvent(body)) {
paymentFactory.createFromBody(body, isSentByMe)
?: createTextContent(body, messageType, content.isEdited)
} else {
createTextContent(body, messageType, content.isEdited)
}
}
is OtherMessageType -> {
val body = messageType.body.trimEnd()
@ -279,6 +277,23 @@ class TimelineItemContentMessageFactory(
}
}
private fun createTextContent(
body: String,
messageType: TextMessageType,
isEdited: Boolean,
): TimelineItemTextContent {
val dom = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser)
val formattedBody = dom?.let(::parseHtml)
?: textPillificationHelper.pillify(body).safeLinkify()
val htmlDocument = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser)
return TimelineItemTextContent(
body = body,
htmlDocument = htmlDocument,
formattedBody = formattedBody,
isEdited = isEdited,
)
}
private fun aspectRatioOf(width: Long?, height: Long?): Float? {
val result = if (height != null && width != null) {
width.toFloat() / height.toFloat()