Skip to content

Commit

Permalink
Expose timestamp on installations and make members async (#300)
Browse files Browse the repository at this point in the history
* members is now async and takes a client param

* update tests, update peerInboxIds to be suspend

* remove param

* jni libs and malformatted bindings

* convert getters to suspend

* lint

* pull in updates from #299

* Move peerAddress back to a sync getter

* lint

* Undo readme change

* format

* format

* update the formatting

* update the library

* update the test and return a list of installations

* fix up and write tests

* optimize the imports

* revert some stuff

* bump the binaries

---------

Co-authored-by: Naomi Plasterer <[email protected]>
  • Loading branch information
codabrink and nplasterer authored Sep 26, 2024
1 parent 51d4748 commit 1bc5bdc
Show file tree
Hide file tree
Showing 17 changed files with 183 additions and 69 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,6 @@ lint/tmp/
# lint/reports/

*google-services.json

# OSX
.DS_Store
36 changes: 18 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ To learn about example app push notifications, see [Enable the quickstart app to

## Reference docs

> **View the reference**
> **View the reference**
> Access the [Kotlin client SDK reference documentation](https://xmtp.github.io/xmtp-android/).
## Install from Maven Central
Expand Down Expand Up @@ -51,15 +51,15 @@ val messages = conversation.messages()
// Send a message
conversation.send(text = "gm")
// Listen for new messages in the conversation
conversation.streamMessages().collect {
conversation.streamMessages().collect {
print("${message.senderAddress}: ${message.body}")
}
```

## Use local storage

> **Important**
> If you are building a production-grade app, be sure to use an architecture that includes a local cache backed by an XMTP SDK.
> **Important**
> If you are building a production-grade app, be sure to use an architecture that includes a local cache backed by an XMTP SDK.
To learn more, see [Use a local cache](https://xmtp.org/docs/build/local-first).

Expand All @@ -70,7 +70,7 @@ A client is created with `Client().create(account: SigningKey): Client` that req
1. To sign the newly generated key bundle. This happens only the very first time when a key bundle is not found in storage.
2. To sign a random salt used to encrypt the key bundle in storage. This happens every time the client is started, including the very first time.

> **Note**
> **Note**
> The client connects to the XMTP `dev` environment by default. [Use `ClientOptions`](#configure-the-client) to change this and other parameters of the network connection.
```kotlin
Expand Down Expand Up @@ -106,10 +106,10 @@ val client = Client().buildFrom(bundle = keys, options = options)

You can configure the client with these parameters of `Client.create`:

| Parameter | Default | Description |
| --------- | ------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| env | `DEV` | Connect to the specified XMTP network environment. Valid values include `DEV`, `.PRODUCTION`, or `LOCAL`. For important details about working with these environments, see [XMTP `production` and `dev` network environments](#xmtp-production-and-dev-network-environments). |
| appVersion | `undefined` | Add a client app version identifier that's included with API requests.<br/>For example, you can use the following format: `appVersion: APP_NAME + '/' + APP_VERSION`.<br/>Setting this value provides telemetry that shows which apps are using the XMTP client SDK. This information can help XMTP developers provide app support, especially around communicating important SDK updates, including deprecations and required upgrades. |
| Parameter | Default | Description |
| ---------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| env | `DEV` | Connect to the specified XMTP network environment. Valid values include `DEV`, `.PRODUCTION`, or `LOCAL`. For important details about working with these environments, see [XMTP `production` and `dev` network environments](#xmtp-production-and-dev-network-environments). |
| appVersion | `undefined` | Add a client app version identifier that's included with API requests.<br/>For example, you can use the following format: `appVersion: APP_NAME + '/' + APP_VERSION`.<br/>Setting this value provides telemetry that shows which apps are using the XMTP client SDK. This information can help XMTP developers provide app support, especially around communicating important SDK updates, including deprecations and required upgrades. |

**Configure `env`**

Expand All @@ -119,7 +119,7 @@ val options = ClientOptions(api = ClientOptions.Api(env = XMTPEnvironment.PRODUC
val client = Client().create(account = account, options = options)
```

> **Note**
> **Note**
> The `apiUrl`, `keyStoreType`, `codecs`, and `maxContentSize` parameters from the XMTP client SDK for JavaScript (xmtp-js) are not yet supported.
## Handle conversations
Expand Down Expand Up @@ -214,7 +214,7 @@ conversation.streamMessages().collect {
if (it.senderAddress == client.address) {
// This message was sent from me
}

print("New message from ${it.senderAddress}: ${it.body}")
}
```
Expand Down Expand Up @@ -285,11 +285,11 @@ To learn more, see [Request and respect user consent](https://xmtp.org/docs/buil

## Handle different types of content

All the send functions support `SendOptions` as an optional parameter. The `contentType` option allows specifying different types of content than the default simple string, which is identified with content type identifier `ContentTypeText`.
All the send functions support `SendOptions` as an optional parameter. The `contentType` option allows specifying different types of content than the default simple string, which is identified with content type identifier `ContentTypeText`.

To learn more about content types, see [Content types with XMTP](https://xmtp.org/docs/concepts/content-types).

Support for other types of content can be added by registering additional `ContentCodec`s with the Client. Every codec is associated with a content type identifier, `ContentTypeId`, which is used to signal to the Client which codec should be used to process the content that is being sent or received.
Support for other types of content can be added by registering additional `ContentCodec`s with the Client. Every codec is associated with a content type identifier, `ContentTypeId`, which is used to signal to the Client which codec should be used to process the content that is being sent or received.

```kotlin
// Assuming we've loaded a fictional NumberCodec that can be used to encode numbers,
Expand All @@ -302,7 +302,7 @@ aliceConversation.send(content = 3.14, options = options)

As shown in the example above, you must provide a `contentFallback` value. Use it to provide an alt text-like description of the original content. Providing a `contentFallback` value enables clients that don't support the content type to still display something meaningful.

> **Caution**
> **Caution**
> If you don't provide a `contentFallback` value, clients that don't support the content type will display an empty message. This results in a poor user experience and breaks interoperability.
### Handle custom content types
Expand Down Expand Up @@ -334,9 +334,9 @@ Older versions of the SDK will eventually be deprecated, which means:

The following table provides the deprecation schedule.

| Announced | Effective | Minimum Version | Rationale |
| ---------- | ---------- | --------------- | ----------------------------------------------------------------------------------------------------------------- |
| There are no deprecations scheduled for `xmtp-android` at this time. | | | |
| Announced | Effective | Minimum Version | Rationale |
| -------------------------------------------------------------------- | --------- | --------------- | --------- |
| There are no deprecations scheduled for `xmtp-android` at this time. | | | |

Bug reports, feature requests, and PRs are welcome in accordance with these [contribution guidelines](https://github.com/xmtp/xmtp-android/blob/main/CONTRIBUTING.md).

Expand All @@ -347,7 +347,7 @@ XMTP provides both `production` and `dev` network environments to support the de
The `production` and `dev` networks are completely separate and not interchangeable.
For example, for a given blockchain account, its XMTP identity on `dev` network is completely distinct from its XMTP identity on the `production` network, as are the messages associated with these identities. In addition, XMTP identities and messages created on the `dev` network can't be accessed from or moved to the `production` network, and vice versa.

> **Note**
> **Note**
> When you [create a client](#create-a-client), it connects to the XMTP `dev` environment by default. To learn how to use the `env` parameter to set your client's network environment, see [Configure the client](#configure-the-client).
The `env` parameter accepts one of three valid values: `dev`, `production`, or `local`. Here are some best practices for when to use each environment:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import org.xmtp.android.example.R
import org.xmtp.android.example.databinding.ListItemConversationBinding
import org.xmtp.android.example.extension.truncatedAddress
import org.xmtp.android.library.Conversation
import org.xmtp.android.library.codecs.GroupUpdatedCodec
import org.xmtp.proto.mls.message.contents.TranscriptMessages
import org.xmtp.proto.mls.message.contents.TranscriptMessages.GroupUpdated

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -509,13 +509,14 @@ class ClientTest {
}

var state = runBlocking { alixClient3.inboxState(true) }
assertEquals(state.installationIds.size, 3)
assertEquals(state.installations.size, 3)
assert(state.installations.first().createdAt != null)

runBlocking {
alixClient3.revokeAllOtherInstallations(alixWallet)
}

state = runBlocking { alixClient3.inboxState(true) }
assertEquals(state.installationIds.size, 1)
assertEquals(state.installations.size, 1)
}
}
38 changes: 19 additions & 19 deletions library/src/androidTest/java/org/xmtp/android/library/GroupTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ class GroupTest {
alixGroup.addMembers(listOf(caro.walletAddress))
boGroup.sync()
}
assertEquals(alixGroup.members().size, 3)
assertEquals(boGroup.members().size, 3)
assertEquals(runBlocking { alixGroup.members().size }, 3)
assertEquals(runBlocking { boGroup.members().size }, 3)

// All members also defaults remove to admin only now.
assertThrows(XMTPException::class.java) {
Expand All @@ -95,8 +95,8 @@ class GroupTest {
}
}

assertEquals(alixGroup.members().size, 3)
assertEquals(boGroup.members().size, 3)
assertEquals(runBlocking { alixGroup.members().size }, 3)
assertEquals(runBlocking { boGroup.members().size }, 3)

assertEquals(boGroup.permissionPolicySet().addMemberPolicy, PermissionOption.Allow)
assertEquals(alixGroup.permissionPolicySet().addMemberPolicy, PermissionOption.Allow)
Expand Down Expand Up @@ -135,31 +135,31 @@ class GroupTest {
alixGroup.sync()
}

assertEquals(alixGroup.members().size, 3)
assertEquals(boGroup.members().size, 3)
assertEquals(runBlocking { alixGroup.members().size }, 3)
assertEquals(runBlocking { boGroup.members().size }, 3)

assertThrows(XMTPException::class.java) {
runBlocking { alixGroup.removeMembers(listOf(caro.walletAddress)) }
}
runBlocking { boGroup.sync() }

assertEquals(alixGroup.members().size, 3)
assertEquals(boGroup.members().size, 3)
assertEquals(runBlocking { alixGroup.members().size }, 3)
assertEquals(runBlocking { boGroup.members().size }, 3)
runBlocking {
boGroup.removeMembers(listOf(caro.walletAddress))
alixGroup.sync()
}

assertEquals(alixGroup.members().size, 2)
assertEquals(boGroup.members().size, 2)
assertEquals(runBlocking { alixGroup.members().size }, 2)
assertEquals(runBlocking { boGroup.members().size }, 2)

assertThrows(XMTPException::class.java) {
runBlocking { alixGroup.addMembers(listOf(caro.walletAddress)) }
}
runBlocking { boGroup.sync() }

assertEquals(alixGroup.members().size, 2)
assertEquals(boGroup.members().size, 2)
assertEquals(runBlocking { alixGroup.members().size }, 2)
assertEquals(runBlocking { boGroup.members().size }, 2)

assertEquals(boGroup.permissionPolicySet().addMemberPolicy, PermissionOption.Admin)
assertEquals(alixGroup.permissionPolicySet().addMemberPolicy, PermissionOption.Admin)
Expand All @@ -183,7 +183,7 @@ class GroupTest {
)
}
assertEquals(
group.members().map { it.inboxId }.sorted(),
runBlocking { group.members().map { it.inboxId }.sorted() },
listOf(
caroClient.inboxId,
alixClient.inboxId,
Expand All @@ -200,7 +200,7 @@ class GroupTest {
)

assertEquals(
group.peerInboxIds().sorted(),
runBlocking { group.peerInboxIds().sorted() },
listOf(
caroClient.inboxId,
alixClient.inboxId,
Expand Down Expand Up @@ -238,7 +238,7 @@ class GroupTest {
val group = runBlocking { boClient.conversations.newGroup(listOf(alix.walletAddress)) }
runBlocking { group.addMembers(listOf(caro.walletAddress)) }
assertEquals(
group.members().map { it.inboxId }.sorted(),
runBlocking { group.members().map { it.inboxId }.sorted() },
listOf(
caroClient.inboxId,
alixClient.inboxId,
Expand All @@ -259,7 +259,7 @@ class GroupTest {
}
runBlocking { group.removeMembers(listOf(caro.walletAddress)) }
assertEquals(
group.members().map { it.inboxId }.sorted(),
runBlocking { group.members().map { it.inboxId }.sorted() },
listOf(
alixClient.inboxId,
boClient.inboxId
Expand Down Expand Up @@ -291,7 +291,7 @@ class GroupTest {
boGroup.sync()
}
assertEquals(
boGroup.members().map { it.inboxId }.sorted(),
runBlocking { boGroup.members().map { it.inboxId }.sorted() },
listOf(
alixClient.inboxId,
boClient.inboxId
Expand All @@ -303,7 +303,7 @@ class GroupTest {
val group = runBlocking { boClient.conversations.newGroup(listOf(alix.walletAddress)) }
runBlocking { group.addMembersByInboxId(listOf(caroClient.inboxId)) }
assertEquals(
group.members().map { it.inboxId }.sorted(),
runBlocking { group.members().map { it.inboxId }.sorted() },
listOf(
caroClient.inboxId,
alixClient.inboxId,
Expand All @@ -324,7 +324,7 @@ class GroupTest {
}
runBlocking { group.removeMembersByInboxId(listOf(caroClient.inboxId)) }
assertEquals(
group.members().map { it.inboxId }.sorted(),
runBlocking { group.members().map { it.inboxId }.sorted() },
listOf(
alixClient.inboxId,
boClient.inboxId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,11 @@ class GroupUpdatedTest {
}
val messages = group.messages()
assertEquals(messages.size, 1)
assertEquals(group.members().size, 3)
assertEquals(runBlocking { group.members().size }, 3)
runBlocking { group.removeMembers(listOf(caro.walletAddress)) }
val updatedMessages = group.messages()
assertEquals(updatedMessages.size, 2)
assertEquals(group.members().size, 2)
assertEquals(runBlocking { group.members().size }, 2)
val content: GroupUpdated? = updatedMessages.first().content()

assertEquals(
Expand All @@ -119,7 +119,7 @@ class GroupUpdatedTest {
}
val messages = group.messages()
assertEquals(messages.size, 1)
assertEquals(group.members().size, 3)
assertEquals(runBlocking { group.members().size }, 3)
runBlocking {
group.send(
content = membershipChange,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ class V3ClientTest {
fun testsCanCreateGroup() {
val group = runBlocking { boV3Client.conversations.newGroup(listOf(caroV2V3.walletAddress)) }
assertEquals(
group.members().map { it.inboxId }.sorted(),
runBlocking { group.members().map { it.inboxId }.sorted() },
listOf(caroV2V3Client.inboxId, boV3Client.inboxId).sorted()
)

Expand Down
4 changes: 2 additions & 2 deletions library/src/main/java/libxmtp-version.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Version: 34d32499
Version: 4f529aeb
Branch: main
Date: 2024-09-19 21:05:16 +0000
Date: 2024-09-26 04:05:26 +0000
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.xmtp.android.library
import android.util.Log
import com.google.protobuf.kotlin.toByteString
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.runBlocking
import org.xmtp.android.library.codecs.EncodedContent
import org.xmtp.android.library.libxmtp.MessageV3
import org.xmtp.android.library.messages.DecryptedMessage
Expand Down Expand Up @@ -56,7 +57,7 @@ sealed class Conversation {
return when (this) {
is V1 -> conversationV1.peerAddress
is V2 -> conversationV2.peerAddress
is Group -> group.peerInboxIds().joinToString(",")
is Group -> runBlocking { group.peerInboxIds().joinToString(",") }
}
}

Expand All @@ -65,7 +66,7 @@ sealed class Conversation {
return when (this) {
is V1 -> listOf(conversationV1.peerAddress)
is V2 -> listOf(conversationV2.peerAddress)
is Group -> group.peerInboxIds()
is Group -> runBlocking { group.peerInboxIds() }
}
}

Expand Down
4 changes: 2 additions & 2 deletions library/src/main/java/org/xmtp/android/library/Group.kt
Original file line number Diff line number Diff line change
Expand Up @@ -247,11 +247,11 @@ class Group(val client: Client, private val libXMTPGroup: FfiGroup) {
}
}

fun members(): List<Member> {
suspend fun members(): List<Member> {
return libXMTPGroup.listMembers().map { Member(it) }
}

fun peerInboxIds(): List<String> {
suspend fun peerInboxIds(): List<String> {
val ids = members().map { it.inboxId }.toMutableList()
ids.remove(client.inboxId)
return ids
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package uniffi.xmtpv3.org.xmtp.android.library.libxmtp

import org.xmtp.android.library.toHex
import uniffi.xmtpv3.FfiInboxState

class InboxState(private val ffiInboxState: FfiInboxState) {
Expand All @@ -9,8 +8,8 @@ class InboxState(private val ffiInboxState: FfiInboxState) {
val addresses: List<String>
get() = ffiInboxState.accountAddresses

val installationIds: List<String>
get() = ffiInboxState.installationIds.map { it.toHex() }
val installations: List<Installation>
get() = ffiInboxState.installations.map { Installation(it) }

val recoveryAddress: String
get() = ffiInboxState.recoveryAddress
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package uniffi.xmtpv3.org.xmtp.android.library.libxmtp

import org.xmtp.android.library.toHex
import uniffi.xmtpv3.FfiInstallation
import java.util.Date

class Installation(private val ffiInstallation: FfiInstallation) {
val installationId: String
get() = ffiInstallation.id.toHex()
val createdAt: Date?
get() = ffiInstallation.clientTimestampNs?.let {
Date(it.toLong())
}
}
Loading

0 comments on commit 1bc5bdc

Please sign in to comment.