From 7c0ec737ad6dffd486234aedae3af9484eaa97f9 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 3 Sep 2024 12:01:19 +0100 Subject: [PATCH] feat: add basic chat app, that sends and receives messages using `ChatApi` --- .editorconfig | 2 +- .idea/codeStyles/Project.xml | 51 +----- example/build.gradle.kts | 17 ++ example/src/main/AndroidManifest.xml | 3 +- .../com/ably/chat/example/MainActivity.kt | 150 ++++++++++++++++-- gradle.properties | 4 +- 6 files changed, 166 insertions(+), 61 deletions(-) diff --git a/.editorconfig b/.editorconfig index b908e46..f048600 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,4 +7,4 @@ trim_trailing_whitespace = true charset = utf-8 [*.{kt,kts}] -ij_kotlin_imports_layout = android.**,androidx.**,*,java.**,javax.**,kotlin.**,kotlinx.**,io.ably.**,com.ably.**,^ \ No newline at end of file +ij_kotlin_imports_layout = *,^ \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index d69494f..6c59a1f 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -2,8 +2,8 @@ - \ No newline at end of file + diff --git a/example/build.gradle.kts b/example/build.gradle.kts index 0f13d33..2307a66 100644 --- a/example/build.gradle.kts +++ b/example/build.gradle.kts @@ -1,3 +1,7 @@ +import java.io.FileInputStream +import java.io.InputStreamReader +import java.util.Properties + plugins { alias(libs.plugins.android.application) alias(libs.plugins.android.kotlin) @@ -19,6 +23,8 @@ android { vectorDrawables { useSupportLibrary = true } + + buildConfigField("String", "ABLY_KEY", "\"${getLocalProperty("ABLY_KEY") ?: ""}\"") } buildTypes { @@ -38,6 +44,7 @@ android { jvmTarget = "1.8" } buildFeatures { + buildConfig = true compose = true } composeOptions { @@ -68,3 +75,13 @@ dependencies { debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) } + +fun getLocalProperty(key: String, file: String = "local.properties"): String? { + val properties = Properties() + val localProperties = File(file) + if (!localProperties.isFile) return null + InputStreamReader(FileInputStream(localProperties), Charsets.UTF_8).use { reader -> + properties.load(reader) + } + return properties.getProperty(key) +} diff --git a/example/src/main/AndroidManifest.xml b/example/src/main/AndroidManifest.xml index adb9701..a503e35 100644 --- a/example/src/main/AndroidManifest.xml +++ b/example/src/main/AndroidManifest.xml @@ -13,6 +13,7 @@ android:theme="@style/Theme.AblyChatExample" tools:targetApi="31"> - \ No newline at end of file + diff --git a/example/src/main/java/com/ably/chat/example/MainActivity.kt b/example/src/main/java/com/ably/chat/example/MainActivity.kt index 792d3c6..8fc363b 100644 --- a/example/src/main/java/com/ably/chat/example/MainActivity.kt +++ b/example/src/main/java/com/ably/chat/example/MainActivity.kt @@ -4,24 +4,62 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TextField import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import com.ably.chat.ChatApi +import com.ably.chat.Message +import com.ably.chat.QueryOptions +import com.ably.chat.QueryOptions.MessageOrder.OldestFirst +import com.ably.chat.RealtimeClient +import com.ably.chat.SendMessageParams import com.ably.chat.example.ui.theme.AblyChatExampleTheme +import io.ably.lib.types.ClientOptions +import java.util.UUID +import kotlinx.coroutines.launch + +val randomClientId = UUID.randomUUID().toString() class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val realtimeClient = RealtimeClient( + ClientOptions().apply { + key = BuildConfig.ABLY_KEY + clientId = randomClientId + logLevel = 2 + }, + ) + val chatApi = ChatApi(realtimeClient, randomClientId) enableEdgeToEdge() setContent { AblyChatExampleTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - Greeting( - name = "Android", + Chat( + chatApi, modifier = Modifier.padding(innerPadding), ) } @@ -31,17 +69,105 @@ class MainActivity : ComponentActivity() { } @Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { - Text( - text = "Hello $name!", - modifier = modifier, - ) +fun Chat(chatApi: ChatApi, modifier: Modifier = Modifier) { + var messageText by remember { mutableStateOf(TextFieldValue("")) } + var sending by remember { mutableStateOf(false) } + var messages by remember { mutableStateOf(listOf()) } + val coroutineScope = rememberCoroutineScope() + + val roomId = "my-room" + + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.SpaceBetween, + ) { + Button(modifier = modifier.align(Alignment.CenterHorizontally), onClick = { + coroutineScope.launch { + messages = chatApi.getMessages(roomId, QueryOptions(orderBy = OldestFirst)).items + } + }) { + Text("Load") + } + + LazyColumn( + modifier = Modifier.weight(1f).padding(16.dp), + userScrollEnabled = true, + ) { + items(messages.size) { index -> + MessageBubble(messages[index]) + } + } + + ChatInputField( + sending = sending, + messageInput = messageText, + onMessageChange = { messageText = it }, + ) { + sending = true + coroutineScope.launch { + chatApi.sendMessage( + roomId, + SendMessageParams( + text = messageText.text, + ), + ) + messageText = TextFieldValue("") + sending = false + } + } + } +} + +@Composable +fun MessageBubble(message: Message) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = if (message.clientId == randomClientId) Arrangement.End else Arrangement.Start, + ) { + Box( + modifier = Modifier + .background( + color = if (message.clientId != randomClientId) Color.Blue else Color.Gray, + shape = RoundedCornerShape(8.dp), + ) + .padding(12.dp), + ) { + Text( + text = message.text, + color = Color.White, + ) + } + } } -@Preview(showBackground = true) @Composable -fun GreetingPreview() { - AblyChatExampleTheme { - Greeting("Android") +fun ChatInputField( + sending: Boolean = false, + messageInput: TextFieldValue, + onMessageChange: (TextFieldValue) -> Unit, + onSendClick: () -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .imePadding(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + TextField( + value = messageInput, + onValueChange = onMessageChange, + readOnly = sending, + modifier = Modifier + .weight(1f) + .background(Color.White), + placeholder = { Text("Type a message...") }, + ) + Button(enabled = !sending, onClick = onSendClick) { + Text("Send") + } } } diff --git a/gradle.properties b/gradle.properties index 5a33dd5..5fd0d9f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,4 +14,6 @@ org.gradle.parallel=true # AndroidX package structure to make it clearer which packages are bundled with the # Android operating system, and which are packaged with your app's APK # https://developer.android.com/topic/libraries/support-library/androidx-rn -android.useAndroidX=true \ No newline at end of file +android.useAndroidX=true + +android.enableBuildConfigAsBytecode=true