fix(wallet): replace text-marker hack with proper raw event API (room.sendRaw + MsgLikeKind.Other)

- Add Timeline.sendRaw() to send custom Matrix events
- Add CustomEventContent type for receiving custom events
- Update TimelineEventContentMapper to handle MsgLikeKind.Other
- Update TimelineItemContentFactory to intercept payment events
- Rewrite DefaultPaymentEventSender to use sendRaw instead of text markers
- Update TimelineItemContentPaymentFactory to parse raw JSON
- Remove text-marker detection from TimelineItemContentMessageFactory
- Update tests to use raw event API
- Mark raw event SDK blocker as RESOLVED in BLOCKERS.md

Event type: co.sulkta.payment.request (reverse-domain format)
Status updates: co.sulkta.payment.status

Benefits:
- Proper Matrix protocol compliance
- No JSON embedded in text messages
- Events won't be indexed by search
- Clean separation from regular messages
This commit is contained in:
Kayos 2026-03-27 11:45:12 -07:00
parent adee67cf0d
commit f2b95d6b8a
10 changed files with 264 additions and 189 deletions

View file

@ -35,7 +35,9 @@ import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
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.getDisambiguatedDisplayName
import io.element.android.features.wallet.impl.timeline.TimelineItemContentPaymentFactory
@Inject
class TimelineItemContentFactory(
@ -49,9 +51,25 @@ class TimelineItemContentFactory(
private val stateFactory: TimelineItemContentStateFactory,
private val failedToParseMessageFactory: TimelineItemContentFailedToParseMessageFactory,
private val failedToParseStateFactory: TimelineItemContentFailedToParseStateFactory,
private val paymentFactory: TimelineItemContentPaymentFactory,
private val sessionId: SessionId,
) {
suspend fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent {
val isOutgoing = sessionId == eventTimelineItem.sender
// Check for custom event types that we handle specially
val content = eventTimelineItem.content
if (content is CustomEventContent && paymentFactory.isPaymentEventType(content.eventType)) {
// Try to get raw JSON from debug info for payment events
val rawJson = eventTimelineItem.timelineItemDebugInfoProvider().originalJson
if (rawJson != null) {
val paymentContent = paymentFactory.createFromRaw(rawJson, isOutgoing)
if (paymentContent != null) {
return paymentContent
}
}
}
return create(
itemContent = eventTimelineItem.content,
eventId = eventTimelineItem.eventId,
@ -78,7 +96,6 @@ class TimelineItemContentFactory(
senderProfile = senderProfile,
content = itemContent,
eventId = eventId,
isSentByMe = isOutgoing,
)
}
is ProfileChangeContent -> {
@ -100,6 +117,10 @@ class TimelineItemContentFactory(
is UnableToDecryptContent -> utdFactory.create(itemContent)
is CallNotifyContent -> TimelineItemRtcNotificationContent()
is UnknownContent -> TimelineItemUnknownContent
is CustomEventContent -> {
// Custom events that weren't handled above (e.g., unknown custom event types)
TimelineItemUnknownContent
}
is LiveLocationContent -> {
val lastKnownLocation = itemContent.locations.mapNotNull { beacon ->
Location.fromGeoUri(beacon.geoUri)

View file

@ -26,7 +26,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
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.utils.TextPillificationHelper
import io.element.android.features.wallet.impl.timeline.TimelineItemContentPaymentFactory
import io.element.android.libraries.androidutils.filesize.FileSizeFormatter
import io.element.android.libraries.androidutils.text.safeLinkify
import io.element.android.libraries.core.mimetype.MimeTypes
@ -66,14 +66,12 @@ class TimelineItemContentMessageFactory(
private val htmlConverterProvider: HtmlConverterProvider,
private val permalinkParser: PermalinkParser,
private val textPillificationHelper: TextPillificationHelper,
private val paymentFactory: TimelineItemContentPaymentFactory,
) {
fun create(
content: MessageContent,
senderId: UserId,
senderProfile: ProfileDetails,
eventId: EventId?,
isSentByMe: Boolean = false,
): TimelineItemEventContent {
return when (val messageType = content.type) {
is EmoteMessageType -> {
@ -257,13 +255,7 @@ class TimelineItemContentMessageFactory(
}
is TextMessageType -> {
val body = messageType.body.trimEnd()
// 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)
}
createTextContent(body, messageType, content.isEdited)
}
is OtherMessageType -> {
val body = messageType.body.trimEnd()