From ac8a2d5ed0b311c0e5c9e92160a632ad807db2c1 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Thu, 25 Jan 2024 20:59:52 -0800 Subject: [PATCH] get the example working with groups --- .../org/xmtp/android/example/ClientManager.kt | 16 ++- .../org/xmtp/android/example/MainActivity.kt | 17 +++- .../org/xmtp/android/example/MainViewModel.kt | 2 +- .../example/connect/ConnectWalletViewModel.kt | 9 +- .../conversation/NewConversationViewModel.kt | 13 +++ .../conversation/NewGroupBottomSheet.kt | 97 +++++++++++++++++++ .../PushNotificationsService.kt | 19 +++- .../src/main/res/drawable/ic_group_add_24.xml | 9 ++ example/src/main/res/layout/activity_main.xml | 12 +++ .../java/org/xmtp/android/library/Client.kt | 85 +++++++++------- .../org/xmtp/android/library/Conversation.kt | 2 +- .../org/xmtp/android/library/Conversations.kt | 2 +- 12 files changed, 239 insertions(+), 44 deletions(-) create mode 100644 example/src/main/java/org/xmtp/android/example/conversation/NewGroupBottomSheet.kt create mode 100644 example/src/main/res/drawable/ic_group_add_24.xml diff --git a/example/src/main/java/org/xmtp/android/example/ClientManager.kt b/example/src/main/java/org/xmtp/android/example/ClientManager.kt index 671a66ea5..fb76d1b26 100644 --- a/example/src/main/java/org/xmtp/android/example/ClientManager.kt +++ b/example/src/main/java/org/xmtp/android/example/ClientManager.kt @@ -1,5 +1,6 @@ package org.xmtp.android.example +import android.content.Context import androidx.annotation.UiThread import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -13,7 +14,16 @@ import org.xmtp.android.library.messages.PrivateKeyBundleV1Builder object ClientManager { - val CLIENT_OPTIONS = ClientOptions(api = ClientOptions.Api(XMTPEnvironment.DEV, appVersion = "XMTPAndroidExample/v1.0.0")) + fun clientOptions(appContext: Context?): ClientOptions { + return ClientOptions( + api = ClientOptions.Api( + XMTPEnvironment.DEV, + appVersion = "XMTPAndroidExample/v1.0.0" + ), + enableLibXmtpV3 = true, + appContext = appContext + ) + } private val _clientState = MutableStateFlow(ClientState.Unknown) val clientState: StateFlow = _clientState @@ -28,13 +38,13 @@ object ClientManager { } @UiThread - fun createClient(encodedPrivateKeyData: String) { + fun createClient(encodedPrivateKeyData: String, appContext: Context) { if (clientState.value is ClientState.Ready) return GlobalScope.launch(Dispatchers.IO) { try { val v1Bundle = PrivateKeyBundleV1Builder.fromEncodedData(data = encodedPrivateKeyData) - _client = Client().buildFrom(v1Bundle, CLIENT_OPTIONS) + _client = Client().buildFrom(v1Bundle, clientOptions(appContext)) _clientState.value = ClientState.Ready } catch (e: Exception) { _clientState.value = ClientState.Error(e.localizedMessage.orEmpty()) diff --git a/example/src/main/java/org/xmtp/android/example/MainActivity.kt b/example/src/main/java/org/xmtp/android/example/MainActivity.kt index 87eb367d8..5c0460dd7 100644 --- a/example/src/main/java/org/xmtp/android/example/MainActivity.kt +++ b/example/src/main/java/org/xmtp/android/example/MainActivity.kt @@ -22,6 +22,7 @@ import org.xmtp.android.example.conversation.ConversationDetailActivity import org.xmtp.android.example.conversation.ConversationsAdapter import org.xmtp.android.example.conversation.ConversationsClickListener import org.xmtp.android.example.conversation.NewConversationBottomSheet +import org.xmtp.android.example.conversation.NewGroupBottomSheet import org.xmtp.android.example.databinding.ActivityMainBinding import org.xmtp.android.example.pushnotifications.PushNotificationTokenManager import org.xmtp.android.example.utils.KeyUtil @@ -35,6 +36,7 @@ class MainActivity : AppCompatActivity(), private lateinit var accountManager: AccountManager private lateinit var adapter: ConversationsAdapter private var bottomSheet: NewConversationBottomSheet? = null + private var groupBottomSheet: NewGroupBottomSheet? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -48,7 +50,7 @@ class MainActivity : AppCompatActivity(), return } - ClientManager.createClient(keys) + ClientManager.createClient(keys, this) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) @@ -67,6 +69,10 @@ class MainActivity : AppCompatActivity(), openConversationDetail() } + binding.groupFab.setOnClickListener { + openGroupDetail() + } + lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { ClientManager.clientState.collect(::ensureClientState) @@ -86,6 +92,7 @@ class MainActivity : AppCompatActivity(), override fun onDestroy() { bottomSheet?.dismiss() + groupBottomSheet?.dismiss() super.onDestroy() } @@ -127,6 +134,7 @@ class MainActivity : AppCompatActivity(), is ClientManager.ClientState.Ready -> { viewModel.fetchConversations() binding.fab.visibility = View.VISIBLE + binding.groupFab.visibility = View.VISIBLE } is ClientManager.ClientState.Error -> showError(clientState.message) is ClientManager.ClientState.Unknown -> Unit @@ -193,4 +201,11 @@ class MainActivity : AppCompatActivity(), NewConversationBottomSheet.TAG ) } + private fun openGroupDetail() { + groupBottomSheet = NewGroupBottomSheet.newInstance() + groupBottomSheet?.show( + supportFragmentManager, + NewGroupBottomSheet.TAG + ) + } } diff --git a/example/src/main/java/org/xmtp/android/example/MainViewModel.kt b/example/src/main/java/org/xmtp/android/example/MainViewModel.kt index 9b12419a4..882e3dbb3 100644 --- a/example/src/main/java/org/xmtp/android/example/MainViewModel.kt +++ b/example/src/main/java/org/xmtp/android/example/MainViewModel.kt @@ -42,7 +42,7 @@ class MainViewModel : ViewModel() { viewModelScope.launch(Dispatchers.IO) { val listItems = mutableListOf() try { - val conversations = ClientManager.client.conversations.list() + val conversations = ClientManager.client.conversations.list(includeGroups = true) PushNotificationTokenManager.xmtpPush.subscribe(conversations.map { it.topic }) listItems.addAll( conversations.map { conversation -> diff --git a/example/src/main/java/org/xmtp/android/example/connect/ConnectWalletViewModel.kt b/example/src/main/java/org/xmtp/android/example/connect/ConnectWalletViewModel.kt index ee8503279..4d37b3eb6 100644 --- a/example/src/main/java/org/xmtp/android/example/connect/ConnectWalletViewModel.kt +++ b/example/src/main/java/org/xmtp/android/example/connect/ConnectWalletViewModel.kt @@ -1,7 +1,10 @@ package org.xmtp.android.example.connect +import android.app.Application +import android.content.Context import android.net.Uri import androidx.annotation.UiThread +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.walletconnect.wcmodal.client.Modal @@ -21,7 +24,7 @@ import org.xmtp.android.library.XMTPException import org.xmtp.android.library.messages.PrivateKeyBuilder import org.xmtp.android.library.messages.PrivateKeyBundleV1Builder -class ConnectWalletViewModel : ViewModel() { +class ConnectWalletViewModel(application: Application) : AndroidViewModel(application) { private val chains: List = Chains.values().map { it.toChainUiState() } @@ -84,7 +87,7 @@ class ConnectWalletViewModel : ViewModel() { _uiState.value = ConnectUiState.Loading try { val wallet = PrivateKeyBuilder() - val client = Client().create(wallet, ClientManager.CLIENT_OPTIONS) + val client = Client().create(wallet, ClientManager.clientOptions(getApplication())) _uiState.value = ConnectUiState.Success( wallet.getAddress(), PrivateKeyBundleV1Builder.encodeData(client.privateKeyBundleV1) @@ -108,7 +111,7 @@ class ConnectWalletViewModel : ViewModel() { it.copy(showWallet = true, uri = uri) } } - val client = Client().create(wallet, ClientManager.CLIENT_OPTIONS) + val client = Client().create(wallet, ClientManager.clientOptions(getApplication())) _uiState.value = ConnectUiState.Success( wallet.getAddress(), PrivateKeyBundleV1Builder.encodeData(client.privateKeyBundleV1) diff --git a/example/src/main/java/org/xmtp/android/example/conversation/NewConversationViewModel.kt b/example/src/main/java/org/xmtp/android/example/conversation/NewConversationViewModel.kt index 7a2cd3341..bb973f619 100644 --- a/example/src/main/java/org/xmtp/android/example/conversation/NewConversationViewModel.kt +++ b/example/src/main/java/org/xmtp/android/example/conversation/NewConversationViewModel.kt @@ -28,6 +28,19 @@ class NewConversationViewModel : ViewModel() { } } + @UiThread + fun createGroup(addresses: List) { + _uiState.value = UiState.Loading + viewModelScope.launch(Dispatchers.IO) { + try { + val group = ClientManager.client.conversations.newGroup(addresses) + _uiState.value = UiState.Success(Conversation.Group(group)) + } catch (e: Exception) { + _uiState.value = UiState.Error(e.localizedMessage.orEmpty()) + } + } + } + sealed class UiState { object Unknown : UiState() object Loading : UiState() diff --git a/example/src/main/java/org/xmtp/android/example/conversation/NewGroupBottomSheet.kt b/example/src/main/java/org/xmtp/android/example/conversation/NewGroupBottomSheet.kt new file mode 100644 index 000000000..fa6a23d9e --- /dev/null +++ b/example/src/main/java/org/xmtp/android/example/conversation/NewGroupBottomSheet.kt @@ -0,0 +1,97 @@ +package org.xmtp.android.example.conversation + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.widget.addTextChangedListener +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import kotlinx.coroutines.launch +import org.xmtp.android.example.R +import org.xmtp.android.example.databinding.BottomSheetNewConversationBinding +import java.util.regex.Pattern + +class NewGroupBottomSheet : BottomSheetDialogFragment() { + + private val viewModel: NewConversationViewModel by viewModels() + private var _binding: BottomSheetNewConversationBinding? = null + private val binding get() = _binding!! + + companion object { + const val TAG = "NewGroupBottomSheet" + + private val ADDRESS_PATTERN = Pattern.compile("^0x[a-fA-F0-9]{40}\$") + + fun newInstance(): NewGroupBottomSheet { + return NewGroupBottomSheet() + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = BottomSheetNewConversationBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.uiState.collect(::ensureUiState) + } + } + + binding.addressInput.addTextChangedListener { + if (viewModel.uiState.value is NewConversationViewModel.UiState.Loading) return@addTextChangedListener + val input = binding.addressInput.text.trim() + val matcher = ADDRESS_PATTERN.matcher(input) + if (matcher.matches()) { + viewModel.createGroup(listOf(input.toString())) + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun ensureUiState(uiState: NewConversationViewModel.UiState) { + when (uiState) { + is NewConversationViewModel.UiState.Error -> { + binding.addressInput.isEnabled = true + binding.progress.visibility = View.GONE + showError(uiState.message) + } + NewConversationViewModel.UiState.Loading -> { + binding.addressInput.isEnabled = false + binding.progress.visibility = View.VISIBLE + } + is NewConversationViewModel.UiState.Success -> { + startActivity( + ConversationDetailActivity.intent( + requireContext(), + topic = uiState.conversation.topic, + peerAddress = uiState.conversation.peerAddress + ) + ) + dismiss() + } + NewConversationViewModel.UiState.Unknown -> Unit + } + } + + private fun showError(message: String) { + val error = message.ifBlank { resources.getString(R.string.error) } + Toast.makeText(requireContext(), error, Toast.LENGTH_SHORT).show() + } +} diff --git a/example/src/main/java/org/xmtp/android/example/pushnotifications/PushNotificationsService.kt b/example/src/main/java/org/xmtp/android/example/pushnotifications/PushNotificationsService.kt index f1e39e381..0ff2839a9 100644 --- a/example/src/main/java/org/xmtp/android/example/pushnotifications/PushNotificationsService.kt +++ b/example/src/main/java/org/xmtp/android/example/pushnotifications/PushNotificationsService.kt @@ -1,8 +1,11 @@ package org.xmtp.android.example.pushnotifications +import android.Manifest import android.app.PendingIntent +import android.content.pm.PackageManager import android.util.Base64 import android.util.Log +import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat @@ -51,7 +54,7 @@ class PushNotificationsService : FirebaseMessagingService() { } GlobalScope.launch(Dispatchers.Main) { - ClientManager.createClient(keysData) + ClientManager.createClient(keysData, applicationContext) } val conversation = ClientManager.client.fetchConversation(topic) if (conversation == null) { @@ -88,6 +91,20 @@ class PushNotificationsService : FirebaseMessagingService() { // Use the URL as the ID for now until one is passed back from the server. NotificationManagerCompat.from(this).apply { + if (ActivityCompat.checkSelfPermission( + applicationContext, + Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) { + // TODO: Consider calling + // ActivityCompat#requestPermissions + // here to request the missing permissions, and then overriding + // public void onRequestPermissionsResult(int requestCode, String[] permissions, + // int[] grantResults) + // to handle the case where the user grants the permission. See the documentation + // for ActivityCompat#requestPermissions for more details. + return + } notify(topic.hashCode(), builder.build()) } } diff --git a/example/src/main/res/drawable/ic_group_add_24.xml b/example/src/main/res/drawable/ic_group_add_24.xml new file mode 100644 index 000000000..8038fe7df --- /dev/null +++ b/example/src/main/res/drawable/ic_group_add_24.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/example/src/main/res/layout/activity_main.xml b/example/src/main/res/layout/activity_main.xml index 3594d690f..a970221d3 100644 --- a/example/src/main/res/layout/activity_main.xml +++ b/example/src/main/res/layout/activity_main.xml @@ -44,6 +44,18 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" /> + + conversationV1.peerAddress is V2 -> conversationV2.peerAddress - is Group -> TODO() + is Group -> group.memberAddresses().toString() } } diff --git a/library/src/main/java/org/xmtp/android/library/Conversations.kt b/library/src/main/java/org/xmtp/android/library/Conversations.kt index 418787224..caeafcfea 100644 --- a/library/src/main/java/org/xmtp/android/library/Conversations.kt +++ b/library/src/main/java/org/xmtp/android/library/Conversations.kt @@ -101,7 +101,7 @@ data class Conversations( libXMTPConversations?.list(opts = FfiListConversationsOptions(null, null, null))?.map { Group(client, it) } - } ?: throw XMTPException("Client does not support Groups") + } ?: emptyList() } /**