Skip to content

Commit

Permalink
adds reaction v2 android side
Browse files Browse the repository at this point in the history
cameronvoell committed Jan 30, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent de6c244 commit 010226e
Showing 8 changed files with 271 additions and 27 deletions.
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ import org.xmtp.android.library.codecs.ContentTypeAttachment
import org.xmtp.android.library.codecs.ContentTypeGroupUpdated
import org.xmtp.android.library.codecs.ContentTypeId
import org.xmtp.android.library.codecs.ContentTypeReaction
import org.xmtp.android.library.codecs.ContentTypeReactionV2
import org.xmtp.android.library.codecs.ContentTypeReadReceipt
import org.xmtp.android.library.codecs.ContentTypeRemoteAttachment
import org.xmtp.android.library.codecs.ContentTypeReply
@@ -21,6 +22,7 @@ import org.xmtp.android.library.codecs.GroupUpdated
import org.xmtp.android.library.codecs.GroupUpdatedCodec
import org.xmtp.android.library.codecs.Reaction
import org.xmtp.android.library.codecs.ReactionCodec
import org.xmtp.android.library.codecs.ReactionV2Codec
import org.xmtp.android.library.codecs.ReadReceipt
import org.xmtp.android.library.codecs.ReadReceiptCodec
import org.xmtp.android.library.codecs.RemoteAttachment
@@ -33,6 +35,10 @@ import org.xmtp.android.library.codecs.description
import org.xmtp.android.library.codecs.getReactionAction
import org.xmtp.android.library.codecs.getReactionSchema
import org.xmtp.android.library.codecs.id
import uniffi.xmtpv3.FfiReaction
import uniffi.xmtpv3.FfiReactionAction
import uniffi.xmtpv3.FfiReactionSchema
import uniffi.xmtpv3.decodeReaction
import java.net.URL

class ContentJson(
@@ -55,6 +61,7 @@ class ContentJson(
Client.register(ReplyCodec())
Client.register(ReadReceiptCodec())
Client.register(GroupUpdatedCodec())
Client.register(ReactionV2Codec())
}

fun fromJsonObject(obj: JsonObject): ContentJson {
@@ -95,6 +102,17 @@ class ContentJson(
content = reaction.get("content").asString,
)
)
} else if (obj.has("reactionV2")) {
val reaction = obj.get("reactionV2").asJsonObject
return ContentJson(
ContentTypeReactionV2, FfiReaction(
reference = reaction.get("reference").asString,
action = getReactionV2Action(reaction.get("action").asString.lowercase()),
schema = getReactionV2Schema(reaction.get("schema").asString.lowercase()),
content = reaction.get("content").asString,
referenceInboxId = ""
)
)
} else if (obj.has("reply")) {
val reply = obj.get("reply").asJsonObject
val nested = fromJsonObject(reply.get("content").asJsonObject)
@@ -159,6 +177,18 @@ class ContentJson(
)
)

ContentTypeReactionV2.id -> {
val reaction: FfiReaction = decodeReaction(encodedContent!!.toByteArray())
mapOf(
"reaction" to mapOf(
"reference" to reaction.reference,
"action" to getReactionV2ActionString(reaction.action),
"schema" to getReactionV2SchemaString(reaction.schema),
"content" to reaction.content,
)
)
}

ContentTypeReply.id -> mapOf(
"reply" to mapOf(
"reference" to (content as Reply).reference,
@@ -227,4 +257,38 @@ class ContentJson(
}
}
}
}

fun getReactionV2Schema(schema: String): FfiReactionSchema {
return when (schema) {
"unicode" -> FfiReactionSchema.UNICODE
"shortcode" -> FfiReactionSchema.SHORTCODE
"custom" -> FfiReactionSchema.CUSTOM
else -> FfiReactionSchema.UNKNOWN
}
}

fun getReactionV2Action(action: String): FfiReactionAction {
return when (action) {
"removed" -> FfiReactionAction.REMOVED
"added" -> FfiReactionAction.ADDED
else -> FfiReactionAction.UNKNOWN
}
}

fun getReactionV2SchemaString(schema: FfiReactionSchema): String {
return when (schema) {
FfiReactionSchema.UNICODE -> "unicode"
FfiReactionSchema.SHORTCODE -> "shortcode"
FfiReactionSchema.CUSTOM -> "custom"
FfiReactionSchema.UNKNOWN -> "unknown"
}
}

fun getReactionV2ActionString(action: FfiReactionAction): String {
return when (action) {
FfiReactionAction.REMOVED -> "removed"
FfiReactionAction.ADDED -> "added"
FfiReactionAction.UNKNOWN -> "unknown"
}
}
2 changes: 2 additions & 0 deletions example/src/contentTypes/contentTypes.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import {
GroupUpdatedCodec,
ReactionCodec,
ReactionV2Codec,
ReplyCodec,
RemoteAttachmentCodec,
StaticAttachmentCodec,
} from 'xmtp-react-native-sdk'

export const supportedCodecs = [
new ReactionCodec(),
new ReactionV2Codec(),
new ReplyCodec(),
new RemoteAttachmentCodec(),
new StaticAttachmentCodec(),
183 changes: 160 additions & 23 deletions example/src/tests/contentTypeTests.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,6 @@ import ReactNativeBlobUtil from 'react-native-blob-util'

import { Test, assert, createClients, delayToPropogate } from './test-utils'
import { ReactionContent, RemoteAttachmentContent } from '../../../src/index'
import { ContentTypeId } from '@xmtp/proto/ts/dist/types/message_contents/content.pb'
const { fs } = ReactNativeBlobUtil

export const contentTypeTests: Test[] = []
@@ -16,49 +15,151 @@ function test(name: string, perform: () => Promise<boolean>) {

test('can fetch messages with reactions', async () => {
const [alix, bo] = await createClients(2)

// Create group and sync
const group = await alix.conversations.newGroup([bo.address])
await bo.conversations.sync()
const boGroup = await bo.conversations.findGroup(group.id)

// Send 3 messages from alix
await group.send('message 1')
await group.send('message 2')
await group.send('message 2')
await group.send('message 3')

await delayToPropogate()
await boGroup?.sync()

// Get messages to react to
const messages = await boGroup?.messages()
assert(messages?.length === 3, 'Should have 3 messages')

// Bo sends reactions to first two messages
await boGroup?.send({
reaction: {
action: 'added',
content: '👍',
reference: messages![0].id,
schema: 'unicode',
}
},
})

await boGroup?.send({
reaction: {
action: 'added',
action: 'added',
content: '❤️',
reference: messages![1].id,
schema: 'unicode',
}
},
})

await delayToPropogate()
await group.sync()

// Get regular messages
const regularMessages = await group.messages()
assert(
regularMessages.length === 6,
'Should have 5 total messages including reactions, but got ' +
regularMessages.length
)

// Get messages with reactions
const messagesWithReactions = await group.messagesWithReactions()
assert(messagesWithReactions.length === 4, 'Should have 4 original messages')

// Check reactions are attached to correct messages
const firstMessage = messagesWithReactions[0] // Reverse chronological
const secondMessage = messagesWithReactions[1]
const thirdMessage = messagesWithReactions[2]

assert(
firstMessage.childMessages?.length === 1,
'First message should have 1 reaction'
)
let messageType = firstMessage.childMessages![0].contentTypeId
assert(
messageType === 'xmtp.org/reaction:1.0',
'First message should have reaction type, but got ' + messageType
)
let messageContent: ReactionContent =
firstMessage.childMessages![0].content() as ReactionContent
assert(
messageContent.content === '👍',
'First message should have thumbs up, but got ' + messageContent.content
)

assert(
secondMessage.childMessages?.length === 1,
'Second message should have 1 reaction'
)
messageType = secondMessage.childMessages![0].contentTypeId
assert(
messageType === 'xmtp.org/reaction:1.0',
'Second message should have reaction type, but got ' + messageType
)
messageContent = secondMessage.childMessages![0].content() as ReactionContent
assert(
messageContent.content === '❤️',
'Second message should have heart, but got ' + messageContent.content
)

assert(
!thirdMessage.childMessages?.length,
'Third message should have no reactions'
)

return true
})

test('can use reaction v2 from rust/proto', async () => {
const [alix, bo] = await createClients(2)

// Create group and sync
const group = await alix.conversations.newGroup([bo.address])
await bo.conversations.sync()
const boGroup = await bo.conversations.findGroup(group.id)

// Send 3 messages from alix
await group.send('message 1')
await group.send('message 2')
await group.send('message 3')

await delayToPropogate()
await boGroup?.sync()

// Get messages to react to
const messages = await boGroup?.messages()
assert(messages?.length === 3, 'Should have 3 messages')

// Bo sends reaction V2 to first two messages
await boGroup?.send({
reactionV2: {
action: 'added',
content: '👍',
reference: messages![0].id,
schema: 'unicode',
},
})

await boGroup?.send({
reactionV2: {
action: 'added',
content: '❤️',
reference: messages![1].id,
schema: 'unicode',
},
})

await delayToPropogate()
await group.sync()

// Get regular messages
const regularMessages = await group.messages()
assert(regularMessages.length === 6, 'Should have 5 total messages including reactions, but got ' + regularMessages.length)
assert(
regularMessages.length === 6,
'Should have 5 total messages including reactions, but got ' +
regularMessages.length
)

// Get messages with reactions
const messagesWithReactions = await group.messagesWithReactions()
@@ -69,19 +170,55 @@ test('can fetch messages with reactions', async () => {
const secondMessage = messagesWithReactions[1]
const thirdMessage = messagesWithReactions[2]

assert(firstMessage.childMessages?.length === 1, 'First message should have 1 reaction')
assert(
firstMessage.childMessages?.length === 1,
'First message should have 1 reaction'
)
let messageType = firstMessage.childMessages![0].contentTypeId
assert(messageType === 'xmtp.org/reaction:1.0', 'First message should have reaction type, but got ' + messageType)
let messageContent: ReactionContent = (firstMessage.childMessages![0].content()) as ReactionContent
assert(messageContent.content === '👍', 'First message should have thumbs up, but got ' + messageContent.content)

assert(secondMessage.childMessages?.length === 1, 'Second message should have 1 reaction')
assert(
messageType === 'xmtp.org/reaction:2.0',
'First message should have reaction V2 type, but got ' + messageType
)
let messageContent: ReactionContent =
firstMessage.childMessages![0].content() as ReactionContent
assert(
messageContent.content === '👍',
'First message should have thumbs up, but got ' + messageContent.content
)

assert(
secondMessage.childMessages?.length === 1,
'Second message should have 1 reaction'
)
messageType = secondMessage.childMessages![0].contentTypeId
assert(messageType === 'xmtp.org/reaction:1.0', 'Second message should have reaction type, but got ' + messageType)
messageContent = (secondMessage.childMessages![0].content()) as ReactionContent
assert(messageContent.content === '❤️', 'Second message should have heart, but got ' + messageContent.content)

assert(!thirdMessage.childMessages?.length, 'Third message should have no reactions')
assert(
messageType === 'xmtp.org/reaction:2.0',
'Second message should have reaction V2 type, but got ' + messageType
)
messageContent = secondMessage.childMessages![0].content() as ReactionContent
assert(
messageContent.content === '❤️',
'Second message should have heart, but got ' + messageContent.content
)
assert(
messageContent.reference === messages![1].id,
'Second message should have reference to second message, but got ' +
messageContent.reference
)
assert(
messageContent.action === 'added',
'Second message should have added action, but got ' + messageContent.action
)
assert(
messageContent.schema === 'unicode',
'Second message should have unicode schema, but got ' +
messageContent.schema
)

assert(
!thirdMessage.childMessages?.length,
'Third message should have no reactions'
)

return true
})
2 changes: 2 additions & 0 deletions example/src/tests/test-utils.ts
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ import {
XMTPEnvironment,
Signer,
ReactionCodec,
ReactionV2Codec,
} from 'xmtp-react-native-sdk'

export type Test = {
@@ -48,6 +49,7 @@ export async function createClients(
Client.register(new GroupUpdatedCodec())
Client.register(new RemoteAttachmentCodec())
Client.register(new ReactionCodec())
Client.register(new ReactionV2Codec())
clients.push(client)
}
return clients
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -32,6 +32,7 @@ export * from './context'
export * from './hooks'
export { GroupUpdatedCodec } from './lib/NativeCodecs/GroupUpdatedCodec'
export { ReactionCodec } from './lib/NativeCodecs/ReactionCodec'
export { ReactionV2Codec } from './lib/NativeCodecs/ReactionV2Codec'
export { ReadReceiptCodec } from './lib/NativeCodecs/ReadReceiptCodec'
export { RemoteAttachmentCodec } from './lib/NativeCodecs/RemoteAttachmentCodec'
export { ReplyCodec } from './lib/NativeCodecs/ReplyCodec'
7 changes: 3 additions & 4 deletions src/lib/DecodedMessage.ts
Original file line number Diff line number Diff line change
@@ -34,7 +34,6 @@ export class DecodedMessage<
deliveryStatus: MessageDeliveryStatus = MessageDeliveryStatus.PUBLISHED
childMessages?: DecodedMessage<ContentType>[]


static from<
ContentType extends
DefaultContentTypes[number] = DefaultContentTypes[number],
@@ -45,7 +44,7 @@ export class DecodedMessage<
const childMessages = decoded.childMessages?.map((childJson: any) =>
DecodedMessage.fromObject<ContentType>({
...childJson,
deliveryStatus: childJson.deliveryStatus
deliveryStatus: childJson.deliveryStatus,
})
)
return new DecodedMessage<ContentType>(
@@ -57,7 +56,7 @@ export class DecodedMessage<
decoded.content,
decoded.fallback,
decoded.deliveryStatus,
childMessages,
childMessages
) as DecodedMessageUnion<ContentTypes>
}

@@ -95,7 +94,7 @@ export class DecodedMessage<
content: any,
fallback: string | undefined,
deliveryStatus: MessageDeliveryStatus = MessageDeliveryStatus.PUBLISHED,
childMessages?: DecodedMessage<ContentType>[],
childMessages?: DecodedMessage<ContentType>[]
) {
this.id = id
this.topic = topic
38 changes: 38 additions & 0 deletions src/lib/NativeCodecs/ReactionV2Codec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {
ContentTypeId,
NativeContentCodec,
NativeMessageContent,
ReactionContent,
} from '../ContentCodec'

export class ReactionV2Codec implements NativeContentCodec<ReactionContent> {
contentKey: 'reactionV2' = 'reactionV2'

contentType: ContentTypeId = {
authorityId: 'xmtp.org',
typeId: 'reaction',
versionMajor: 2,
versionMinor: 0,
}

encode(content: ReactionContent): NativeMessageContent {
return {
reactionV2: content,
}
}

decode(nativeContent: NativeMessageContent): ReactionContent {
return nativeContent.reactionV2!
}

fallback(content: ReactionContent): string | undefined {
switch (content.action) {
case 'added':
return `Reacted “${content.content}” to an earlier message`
case 'removed':
return `Removed “${content.content}” from an earlier message`
default:
return undefined
}
}
}
1 change: 1 addition & 0 deletions src/lib/types/ContentCodec.ts
Original file line number Diff line number Diff line change
@@ -76,6 +76,7 @@ export type NativeMessageContent = {
unknown?: UnknownContent
reply?: ReplyContent
reaction?: ReactionContent
reactionV2?: ReactionContent
attachment?: StaticAttachmentContent
remoteAttachment?: RemoteAttachmentContent
readReceipt?: ReadReceiptContent

0 comments on commit 010226e

Please sign in to comment.